diff --git a/Dockerfile b/Dockerfile index d98371d..b9be259 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN apt-get update \ libnss-winbind \ libpam-winbind \ python3 \ + p7zip-full \ rclone \ rsync \ samba \ diff --git a/README.md b/README.md index 23da7a4..6c99c0c 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,17 @@ This repository provides a production-oriented Samba file server container that - Startup resolves those SIDs to NSS group names via winbind, then uses those resolved groups in Samba `valid users` rules. - Samba `full_audit` is restricted to successful and failed reads, writes, renames, and deletions. - A collector normalizes those four actions and persists them in indexed SQLite tables; activity is never automatically deleted. -- A read-only HTTPS web console provides group membership trees, storage usage, searchable activity, live backup progress, and system health. +- A plain HTTPS administration console provides read-only statistics and logs plus narrowly scoped actions for manual backups and share reconciliation. It also includes a fully client-side Typst PDF report. - Web sign-in validates the submitted username/password with Kerberos, permits only users whose winbind group SID set contains `DOMAIN_ADMINS_SID`, and issues an expiring JWT in a Secure, HttpOnly, SameSite=Strict cookie. The browser does not use NTLM/SPNEGO or Kerberos negotiation. - HTTPS certificates are requested from a configured local Smallstep CA and renewed automatically. Pre-issued certificate files are also supported. -- Optional remote backups run when `BACKUP_DESTINATION` is configured. +- Optional remote backups run when `BACKUP_DESTINATION` and `BACKUP_ARCHIVE_PASSWORD` are configured; each active or archived group folder is uploaded as its own encrypted, non-solid 7z archive. - Private home creation skips well-known/service accounts by default (including `krbtgt`, `msol_*`, `FileShare_ServiceAcc`). - Reconciliation is executed: - once on startup - every 5 minutes via cron - Backup is executed: - - daily at `BACKUP_START_HOUR` in UTC (default: `2`, i.e. 02:00 UTC) + - manually through the Domain Admin web UI or CLI + - daily at `BACKUP_START_HOUR` in UTC when `BACKUP_AUTO_ENABLED=true` (default: `true`; the switch is environment-only) ## Data Folder Lifecycle @@ -155,6 +156,8 @@ Useful overrides: | `DEV_SEED_MB` | `8` | MiB per large seed file | | `DEV_ACTIVITY_INTERVAL_SECONDS` | `4` | Delay between SMB activity batches | | `DEV_BACKUP_INTERVAL_SECONDS` | `120` | Delay between completed preview backups | +| `DEV_ARCHIVE_PASSWORD` | `PreviewArchive123!` | Dummy password for encrypted group archives | +| `DEV_BACKUP_AUTO_ENABLED` | `true` | Environment-only automatic-backup setting passed to the file server | `DEV_REALM`, `DEV_DNS_DOMAIN`, and `DEV_BASE_DN` describe the same domain and must be changed together. @@ -175,8 +178,10 @@ The E2E suite verifies: - Data, Private, and FSLogix usage aggregation; - high-level `full_audit` ingestion for all four actions, service-account exclusion, filters, facets, and pagination; - shared SQLite schema, integrity, indexes, legacy-log removal, and ordered read deduplication; -- real rsync transfer progress, completed backup status, log output, and remote snapshot marker; -- overview and system-health aggregation. +- real rsync transfer progress, completed backup status, log output, remote snapshot marker, and per-group encrypted non-solid 7z archives; +- anonymous action rejection plus authenticated manual backup and reconciliation actions, terminal progress, and live reconciliation output; +- overview and system-health aggregation; +- the log-free PDF report snapshot plus local Typst wrapper, WebAssembly, font MIME types, and immutable caching. The runner returns non-zero on the first failed assertion, prints bounded logs from every run-scoped service, and always removes its containers, volumes, network, and temporary CA root. Set `DEV_SKIP_BUILD=1` for a fast rerun against existing local images. @@ -199,7 +204,9 @@ The runner returns non-zero on the first failed assertion, prints bounded logs f - `DOMAIN_ADMINS_SID` - optional `FSLOGIX_GROUP_SID` (defaults to `DOMAIN_USERS_SID`) - optional `BACKUP_DESTINATION` (empty disables backup) - - optional `BACKUP_START_HOUR` (0-23, default `2`) + - `BACKUP_ARCHIVE_PASSWORD` when a backup destination is configured + - optional environment-only `BACKUP_AUTO_ENABLED` (`true` or `false`, default `true`) + - optional `BACKUP_START_HOUR` (0-23, default `2`; used only when automatic backups are enabled) - optional `BACKUP_RETENTION_DAILY` (default `3`) - optional `BACKUP_RETENTION_WEEKLY` (default `2`) - optional `BACKUP_RETENTION_MONTHLY` (default `2`) @@ -275,7 +282,7 @@ The runner returns non-zero on the first failed assertion, prints bounded logs f - Semantics intentionally differ from `Data`: only the share root is reconciled (`03770` + ACL defaults), while user-created profile container folders/files are not recursively normalized. - Samba masks are profile-container oriented (`create mask = 0600`, `directory mask = 0700`) so profile payload stays user-private by default. -## Read-only Web Console +## Web Administration Console Open `https:///` after setup. Only members of the group identified by `DOMAIN_ADMINS_SID` can sign in. The form accepts `DOMAIN\username`, `username@realm`, or an unqualified username (which is qualified with `WORKGROUP`). @@ -286,10 +293,14 @@ The console is intentionally operational and plain: - **Data usage**: cached recursive size of every top-level `/Data` group folder. - **User usage**: per-user `/Private + /FSLogix` totals with component sizes. - **Activity**: dynamic date, user, share, action, result, and path filters with pagination. -- **Backups**: read-only live progress, active transfer rows, snapshot name, and recent backup output. +- **Share reconciliation**: manually force reconciliation and follow its phase, progress bar, current group, and live output. +- **Backups**: manually start a backup and follow live progress, active transfer rows, snapshot name, trigger, and recent output. +- **PDF report**: storage totals and every storage row, complete group/folder membership hierarchies, current backup state, system checks, and TLS certificate data. - **System**: domain trust, Samba configuration, TLS certificate, scanner, and activity database health. -The web API has no mutation endpoint other than session login/logout. Files, groups, ACLs, backup schedules, and retention cannot be changed from the console. +The PDF report deliberately excludes the activity log and backup log. Its dedicated snapshot endpoint removes those fields before returning data. Typst, its WebAssembly compiler, and the report fonts are shipped with the application; Typst source and PDF bytes are created only in the authenticated browser and are never uploaded to another service. The first export downloads roughly 22 MiB of compiler/font assets, which are then cached as immutable files. The CSP grants only `'wasm-unsafe-eval'` for WebAssembly compilation and does not enable JavaScript `'unsafe-eval'`. + +The only operational mutation endpoints start an immediate backup or share reconciliation, and both require the same Domain Admin JWT as every protected page. The console cannot edit files, groups, ACL rules, backup schedules, retention, or `BACKUP_AUTO_ENABLED`. Automatic backups can be enabled or disabled only through the environment and therefore require a redeployment/restart. ### Authentication and sessions @@ -298,7 +309,7 @@ The web API has no mutation endpoint other than session login/logout. Files, gro - Authentication is form-based; the browser never performs NTLM, SPNEGO, or Kerberos negotiation. - JWTs use HMAC-SHA256, default to eight hours, and are accepted from the protected cookie or an `Authorization: Bearer` header. - Login attempts are rate-limited per client address. -- Responses set HSTS, a restrictive Content Security Policy, clickjacking protection, MIME sniffing protection, and no-store caching. +- Responses set HSTS, a restrictive Content Security Policy, clickjacking protection, and MIME sniffing protection. APIs and ordinary UI assets use no-store caching; pinned Typst compiler/font assets use immutable long-term caching. ### TLS with an internal ACME CA @@ -393,12 +404,13 @@ Collection starts even when the web UI is disabled. On the first collector start ## Backups -- Backups are enabled only if `BACKUP_DESTINATION` is non-empty. +- Backups are available only if `BACKUP_DESTINATION` is non-empty and `BACKUP_ARCHIVE_PASSWORD` is set. - Each run creates a timestamped snapshot under `snapshots/YYYYMMDDTHHMMSSZ` at the destination. -- Backup job is scheduled daily at `BACKUP_START_HOUR` in UTC. The container and Compose service force `TZ=Etc/UTC`. +- `BACKUP_AUTO_ENABLED=true` schedules the job daily at `BACKUP_START_HOUR` in UTC. With `false`, no backup cron entry is installed; manual web and CLI runs remain available. This setting is environment-only and cannot be changed through the web API. +- The container and Compose service force `TZ=Etc/UTC`. - Sources synced to destination on each run: - `/data/private` -> `data/private` - - `/data/groups` -> `data/groups` + - `/data/groups` -> `data/groups`; direct group directories below `data` and `archive` become one `.7z` each - `/data/fslogix` -> `data/fslogix` - `/state` -> `state` (the live WAL database is replaced by a consistent SQLite online snapshot) - `/var/lib/samba/private` -> `samba/private` @@ -408,7 +420,11 @@ Collection starts even when the web UI is disabled. On the first collector start - `BACKUP_RETENTION_WEEKLY=2` - `BACKUP_RETENTION_DAILY=3` - The backup script writes directly to `BACKUP_LOG_FILE` (default: `/var/log/backup.log`). Cron does not redirect backup output into the logfile. -- Before uploading, the backup script creates a temporary, integrity-checked SQLite online snapshot of the shared state database, then measures all source files so it can report total upload progress. +- Before uploading, the backup script creates a temporary, integrity-checked SQLite online snapshot of the shared state database. +- Each direct active and archived group folder is then compressed with LZMA2 at level 5 and multithreading into one non-solid (`-ms=off`) 7z archive. Header/data encryption is enabled (`-mhe=on`); the password comes only from `BACKUP_ARCHIVE_PASSWORD` and is supplied to 7z through stdin, never as a process argument. Non-group entries below `/data/groups` are preserved unchanged. +- `BACKUP_ARCHIVE_TEMP_DIR` selects the local staging directory (default `/tmp`). It must have enough free space for the compressed group archives. Staging data is removed after success or failure. +- Losing `BACKUP_ARCHIVE_PASSWORD` makes the group archives unrecoverable; keep it in the same secret-management system as the remote-backup credentials. +- After staging, the script measures all upload sources so it can report total transfer progress. - Upload progress is logged per file every `BACKUP_PROGRESS_INTERVAL_SECONDS` seconds and again when a file reaches 100%, including percentage and transferred/remaining bytes with auto-scaled units. - `BACKUP_PROGRESS=auto` shows an interactive multi-line progress view only for TTY/manual runs. Current file uploads are shown as separate rows, capped at 12 rows, with the total progress row at the bottom. Use `always` to force it or `never` to suppress the bar. File and total progress are still logged. - Every run atomically updates `BACKUP_STATUS_FILE` (default `/state/backup-status.json`) with its state, current source, active files, byte totals, percentage, snapshot, and final result for the live web view. @@ -430,7 +446,10 @@ Collection starts even when the web UI is disabled. On the first collector start ```env BACKUP_DESTINATION=sftp://backupuser:StrongPassword@sftp.example.com/exports/samba + BACKUP_ARCHIVE_PASSWORD=use-a-long-random-secret + BACKUP_AUTO_ENABLED=true BACKUP_START_HOUR=2 + BACKUP_ARCHIVE_TEMP_DIR=/tmp BACKUP_RETENTION_DAILY=3 BACKUP_RETENTION_WEEKLY=2 BACKUP_RETENTION_MONTHLY=2 @@ -446,12 +465,14 @@ Collection starts even when the web UI is disabled. On the first collector start docker compose logs -f samba docker compose logs -f samba | grep -E '\\[web\\]|\\[audit\\]' docker compose exec samba python3 /app/reconcile_shares.py +docker compose exec samba python3 /app/backup_to_destination.py docker compose exec samba sqlite3 /state/shares.db 'PRAGMA quick_check;' docker compose exec samba sqlite3 /state/shares.db 'SELECT action, count(*) FROM audit_events GROUP BY action;' docker compose exec samba testparm -s docker compose exec samba sh -lc 'tail -n 200 /var/log/samba/log.*' docker compose exec samba sh -lc 'tail -n 200 /var/log/backup.log' docker compose exec samba python3 -m json.tool /state/backup-status.json +docker compose exec samba python3 -m json.tool /state/reconcile-status.json ``` ## Troubleshooting diff --git a/app/backup_to_destination.py b/app/backup_to_destination.py index 0975562..c64a33a 100644 --- a/app/backup_to_destination.py +++ b/app/backup_to_destination.py @@ -24,6 +24,9 @@ STATE_DB_PATH = os.getenv( LOCK_PATH = os.path.join(STATE_ROOT, "backup.lock") DEFAULT_BACKUP_LOG_FILE = "/var/log/backup.log" DEFAULT_BACKUP_STATUS_FILE = "/state/backup-status.json" +DEFAULT_ARCHIVE_TEMP_DIR = "/tmp" +GROUPS_SOURCE_PATH = "/data/groups" +GROUP_ARCHIVE_CATEGORIES = ("data", "archive") DEFAULT_PROGRESS_MODE = "auto" DEFAULT_PROGRESS_INTERVAL_SECONDS = 10 PROGRESS_BAR_WIDTH = 28 @@ -48,7 +51,7 @@ RSYNC_IGNORED_RECORD_PREFIXES = ( BACKUP_SOURCES: List[Tuple[str, str]] = [ ("/data/private", "data/private"), - ("/data/groups", "data/groups"), + (GROUPS_SOURCE_PATH, "data/groups"), ("/data/fslogix", "data/fslogix"), (STATE_ROOT, "state"), ("/var/lib/samba/private", "samba/private"), @@ -197,9 +200,10 @@ class BackupStatus: flush=True, ) - def begin(self, destination: str) -> None: + def begin(self, destination: str, trigger: str) -> None: self.write( state="starting", + trigger=trigger, startedAt=dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds"), finishedAt=None, destination=destination, @@ -226,7 +230,7 @@ class BackupStatus: ) self.write( state="running", - currentSource=reporter.source_path, + currentSource=reporter.destination_path, percent=round(reporter.overall_progress.percent, 2), transferredBytes=reporter.overall_progress.transferred_bytes, totalBytes=reporter.overall_progress.total_bytes, @@ -275,6 +279,7 @@ def run_command( env: Optional[Dict[str, str]] = None, input_text: Optional[str] = None, check: bool = True, + cwd: Optional[str] = None, ) -> subprocess.CompletedProcess: result = subprocess.run( command, @@ -282,6 +287,7 @@ def run_command( text=True, env=env, input=input_text, + cwd=cwd, ) if check and result.returncode != 0: output = result.stderr.strip() or result.stdout.strip() @@ -981,16 +987,141 @@ def prepare_state_snapshot( raise +def archive_password() -> str: + password = os.getenv("BACKUP_ARCHIVE_PASSWORD", "") + if not password: + raise RuntimeError( + "BACKUP_ARCHIVE_PASSWORD must be set when group data is backed up" + ) + if any(char in password for char in "\r\n\0"): + raise RuntimeError("BACKUP_ARCHIVE_PASSWORD contains unsupported characters") + return password + + +def create_group_archive(source_path: str, archive_path: str, password: str) -> None: + os.makedirs(os.path.dirname(archive_path), exist_ok=True) + source_parent = os.path.dirname(source_path) + source_name = os.path.basename(source_path) + result = run_command( + [ + "7z", + "a", + "-t7z", + "-m0=lzma2", + "-mx=5", + "-mmt=on", + "-ms=off", + "-mhe=on", + "-p", + "-y", + archive_path, + "--", + source_name, + ], + input_text=f"{password}\n", + check=False, + env=os.environ.copy(), + cwd=source_parent, + ) + if result.returncode != 0: + output = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"Unable to archive group {source_name}: {output}") + + +def copy_backup_entry(entry: os.DirEntry, destination: str) -> None: + if entry.is_symlink(): + os.symlink(os.readlink(entry.path), destination) + elif entry.is_dir(follow_symlinks=False): + shutil.copytree(entry.path, destination, symlinks=True) + else: + shutil.copy2(entry.path, destination, follow_symlinks=False) + + +def prepare_group_archives( + source_root: str = GROUPS_SOURCE_PATH, + password: Optional[str] = None, + temporary_parent: Optional[str] = None, +) -> Tuple[str, str, int]: + """Stage every active and archived group directory as one encrypted 7z.""" + archive_secret = password if password is not None else archive_password() + temp_parent = ( + temporary_parent + if temporary_parent is not None + else os.getenv("BACKUP_ARCHIVE_TEMP_DIR", DEFAULT_ARCHIVE_TEMP_DIR).strip() + ) + if temp_parent: + os.makedirs(temp_parent, exist_ok=True) + temporary_root = tempfile.mkdtemp( + prefix="backup-groups-", + dir=temp_parent or None, + ) + staged_root = os.path.join(temporary_root, "groups") + archive_count = 0 + try: + os.makedirs(staged_root, exist_ok=True) + entries = sorted(os.scandir(source_root), key=lambda entry: entry.name.casefold()) + by_name = {entry.name: entry for entry in entries} + for category in GROUP_ARCHIVE_CATEGORIES: + source_category = by_name.pop(category, None) + if source_category is None: + continue + target_category = os.path.join(staged_root, category) + if ( + source_category.is_symlink() + or not source_category.is_dir(follow_symlinks=False) + ): + copy_backup_entry(source_category, target_category) + continue + + os.makedirs(target_category, exist_ok=True) + members = sorted( + os.scandir(source_category.path), + key=lambda entry: entry.name.casefold(), + ) + for entry in members: + target = os.path.join(target_category, entry.name) + if entry.is_dir(follow_symlinks=False) and not entry.is_symlink(): + relative_name = f"{category}/{entry.name}" + log(f"Creating encrypted non-solid archive for {relative_name}") + if BACKUP_STATUS is not None: + BACKUP_STATUS.write( + state="running", + currentSource=relative_name, + message=f"Archiving group {relative_name}", + activeFiles=[], + ) + create_group_archive( + entry.path, + f"{target}.7z", + archive_secret, + ) + archive_count += 1 + else: + log(f"Preserving non-group entry {category}/{entry.name}") + copy_backup_entry(entry, target) + + for entry in sorted(by_name.values(), key=lambda value: value.name.casefold()): + log(f"Preserving additional groups source entry {entry.name}") + copy_backup_entry(entry, os.path.join(staged_root, entry.name)) + + return temporary_root, staged_root, archive_count + except Exception: + shutil.rmtree(temporary_root, ignore_errors=True) + raise + + def available_sources( state_snapshot: Optional[str] = None, + groups_snapshot: Optional[str] = None, ) -> List[Tuple[str, str]]: sources: List[Tuple[str, str]] = [] for source_path, destination_path in BACKUP_SOURCES: - effective_source = ( - state_snapshot - if destination_path == "state" and state_snapshot is not None - else source_path - ) + if destination_path == "state" and state_snapshot is not None: + effective_source = state_snapshot + elif destination_path == "data/groups" and groups_snapshot is not None: + effective_source = groups_snapshot + else: + effective_source = source_path if os.path.isdir(effective_source): sources.append((effective_source, destination_path)) else: @@ -1539,15 +1670,32 @@ def run_backup() -> int: BACKUP_STATUS = BackupStatus( os.getenv("BACKUP_STATUS_FILE", DEFAULT_BACKUP_STATUS_FILE).strip() ) - BACKUP_STATUS.begin(redact_destination(destination.raw_url)) + BACKUP_STATUS.begin( + redact_destination(destination.raw_url), + os.getenv("BACKUP_TRIGGER", "manual").strip() or "manual", + ) backend = None state_snapshot_root = None + group_archive_root = None try: state_snapshot = None if os.path.isdir(STATE_ROOT): BACKUP_STATUS.write(message="Creating consistent state database snapshot") state_snapshot_root, state_snapshot = prepare_state_snapshot() - sources = available_sources(state_snapshot) + + groups_snapshot = None + if os.path.isdir(GROUPS_SOURCE_PATH): + BACKUP_STATUS.write( + state="running", + currentSource="data/groups", + message="Creating encrypted group archives", + ) + group_archive_root, groups_snapshot, archive_count = prepare_group_archives( + password=archive_password() + ) + log(f"Created {archive_count} encrypted non-solid group archive(s)") + + sources = available_sources(state_snapshot, groups_snapshot) if not sources: log("No backup sources are available, skipping backup") return 0 @@ -1608,6 +1756,8 @@ def run_backup() -> int: backend.close() if state_snapshot_root is not None: shutil.rmtree(state_snapshot_root, ignore_errors=True) + if group_archive_root is not None: + shutil.rmtree(group_archive_root, ignore_errors=True) def with_lock() -> int: diff --git a/app/init.sh b/app/init.sh index c9354ed..206570a 100755 --- a/app/init.sh +++ b/app/init.sh @@ -217,6 +217,19 @@ write_runtime_env_file() { if [[ -n "${BACKUP_LOG_FILE:-}" ]]; then printf 'export BACKUP_LOG_FILE=%q\n' "$BACKUP_LOG_FILE" fi + printf 'export BACKUP_AUTO_ENABLED=%q\n' "${BACKUP_AUTO_ENABLED:-true}" + if [[ -n "${BACKUP_ARCHIVE_PASSWORD:-}" ]]; then + printf 'export BACKUP_ARCHIVE_PASSWORD=%q\n' "$BACKUP_ARCHIVE_PASSWORD" + fi + if [[ -n "${BACKUP_ARCHIVE_TEMP_DIR:-}" ]]; then + printf 'export BACKUP_ARCHIVE_TEMP_DIR=%q\n' "$BACKUP_ARCHIVE_TEMP_DIR" + fi + if [[ -n "${RECONCILE_LOG_FILE:-}" ]]; then + printf 'export RECONCILE_LOG_FILE=%q\n' "$RECONCILE_LOG_FILE" + fi + if [[ -n "${RECONCILE_STATUS_FILE:-}" ]]; then + printf 'export RECONCILE_STATUS_FILE=%q\n' "$RECONCILE_STATUS_FILE" + fi if [[ -n "${BACKUP_PROGRESS:-}" ]]; then printf 'export BACKUP_PROGRESS=%q\n' "$BACKUP_PROGRESS" fi @@ -542,7 +555,7 @@ start_observability_services() { ;; esac fi - log "Starting read-only web UI for https://${WEB_HOSTNAME}" + log "Starting administration web UI for https://${WEB_HOSTNAME}" python3 /app/web_ui.py & else log 'WEB_ENABLED is false; web UI disabled' @@ -553,12 +566,12 @@ install_cron_job() { cat > /etc/cron.d/reconcile-shares <<'EOF' SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -*/5 * * * * root source /app/runtime.env && /usr/bin/python3 /app/reconcile_shares.py >> /var/log/reconcile.log 2>&1 +*/5 * * * * root source /app/runtime.env && RECONCILE_TRIGGER=automatic /usr/bin/python3 /app/reconcile_shares.py EOF - if [[ -n "${BACKUP_DESTINATION:-}" ]]; then + if [[ -n "${BACKUP_DESTINATION:-}" ]] && env_is_true "${BACKUP_AUTO_ENABLED:-true}"; then cat >> /etc/cron.d/reconcile-shares < str: return dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds") +RECONCILE_LOG_HANDLE = None +RECONCILE_STATUS = None + + +class ReconcileStatus: + def __init__(self, path: str): + self.path = path + self.value: Dict[str, object] = { + "state": "starting", + "percent": 0.0, + "phase": "starting", + } + + def write(self, **changes: object) -> None: + self.value.update(changes) + self.value["updatedAt"] = now_utc() + try: + status_dir = os.path.dirname(self.path) + if status_dir: + os.makedirs(status_dir, exist_ok=True) + temp_path = f"{self.path}.tmp" + with open(temp_path, "w", encoding="utf-8") as handle: + json.dump(self.value, handle, separators=(",", ":"), sort_keys=True) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temp_path, self.path) + except OSError as exc: + print( + f"[reconcile] WARNING: unable to update status file {self.path}: {exc}", + file=sys.stderr, + flush=True, + ) + + def begin(self, trigger: str) -> None: + self.write( + state="starting", + trigger=trigger, + phase="starting", + percent=0.0, + startedAt=now_utc(), + finishedAt=None, + currentItem=None, + message="Starting reconciliation", + ) + + def progress( + self, + percent: float, + phase: str, + message: str, + current_item: Optional[str] = None, + ) -> None: + self.write( + state="running", + percent=max(0.0, min(99.0, float(percent))), + phase=phase, + message=message, + currentItem=current_item, + ) + + def complete(self, message: str) -> None: + self.write( + state="completed", + phase="completed", + percent=100.0, + finishedAt=now_utc(), + currentItem=None, + message=message, + ) + + def fail(self, message: str) -> None: + self.write( + state="failed", + phase="failed", + finishedAt=now_utc(), + currentItem=None, + message=message, + ) + + +def configure_logging() -> None: + global RECONCILE_LOG_HANDLE + path = os.getenv("RECONCILE_LOG_FILE", DEFAULT_RECONCILE_LOG_FILE).strip() + if not path: + return + try: + directory_name = os.path.dirname(path) + if directory_name: + os.makedirs(directory_name, exist_ok=True) + RECONCILE_LOG_HANDLE = open(path, "a", encoding="utf-8") + except OSError as exc: + print( + f"[reconcile] WARNING: unable to open log file {path}: {exc}", + file=sys.stderr, + flush=True, + ) + + +def close_logging() -> None: + global RECONCILE_LOG_HANDLE + if RECONCILE_LOG_HANDLE is not None: + RECONCILE_LOG_HANDLE.close() + RECONCILE_LOG_HANDLE = None + + def log(message: str) -> None: - print(f"[reconcile] {message}", flush=True) + line = f"[reconcile] {message}" + print(line, flush=True) + if RECONCILE_LOG_HANDLE is not None: + RECONCILE_LOG_HANDLE.write(f"{now_utc()} {line}\n") + RECONCILE_LOG_HANDLE.flush() + + +def status_progress( + percent: float, + phase: str, + message: str, + current_item: Optional[str] = None, +) -> None: + if RECONCILE_STATUS is not None: + RECONCILE_STATUS.progress(percent, phase, message, current_item) def ensure_required_env() -> None: @@ -1444,11 +1565,19 @@ def sync_dynamic_directory_permissions( if force_recursive_repair: log(f"Data ACL recursive repair is forced by {DATA_ACL_REPAIR_ENV}") - for row in rows: + row_count = len(rows) + for row_index, row in enumerate(rows): share_started = time.monotonic() guid = row["objectGUID"] sam = row["samAccountName"] path = row["path"] + progress = 34.0 + (40.0 * row_index / max(1, row_count)) + status_progress( + progress, + "data-permissions", + "Syncing data folder permissions", + str(sam), + ) ad_group = ad_groups_by_guid.get(guid) if ad_group is None: log(f"No AD data available for {sam}; leaving existing ACLs") @@ -1562,11 +1691,16 @@ def with_lock() -> bool: return False try: + if RECONCILE_STATUS is not None: + RECONCILE_STATUS.begin( + os.getenv("RECONCILE_TRIGGER", "manual").strip() or "manual" + ) ensure_required_env() os.makedirs(GROUP_ROOT, exist_ok=True) os.makedirs(GROUP_ARCHIVE_ROOT, exist_ok=True) reconcile_started = time.monotonic() + status_progress(2, "winbind", "Refreshing winbind cache") log("Refreshing winbind cache") phase_started = time.monotonic() refresh_winbind_cache() @@ -1577,6 +1711,7 @@ def with_lock() -> bool: conn = open_db() try: + status_progress(10, "directory", "Reading data folder groups from AD") phase_started = time.monotonic() groups = fetch_fileshare_groups() log( @@ -1584,6 +1719,7 @@ def with_lock() -> bool: f"in {format_duration(time.monotonic() - phase_started)}" ) + status_progress(22, "database", "Reconciling data folder database") log("Reconciling data folder DB") phase_started = time.monotonic() reconcile_db(conn, groups) @@ -1592,6 +1728,7 @@ def with_lock() -> bool: f"{format_duration(time.monotonic() - phase_started)}" ) + status_progress(32, "data-permissions", "Syncing data folder permissions") log("Syncing data folder permissions") phase_started = time.monotonic() sync_dynamic_directory_permissions(conn, groups) @@ -1602,6 +1739,7 @@ def with_lock() -> bool: finally: conn.close() + status_progress(76, "fslogix", "Syncing FSLogix root") log("Syncing FSLogix root") phase_started = time.monotonic() sync_fslogix_directory() @@ -1610,6 +1748,7 @@ def with_lock() -> bool: f"{format_duration(time.monotonic() - phase_started)}" ) + status_progress(84, "private", "Syncing private directories") log("Syncing private directories") phase_started = time.monotonic() sync_private_directories() @@ -1618,6 +1757,7 @@ def with_lock() -> bool: f"{format_duration(time.monotonic() - phase_started)}" ) + status_progress(96, "samba", "Reloading Samba config") log("Reloading Samba config") phase_started = time.monotonic() reload_samba() @@ -1625,22 +1765,31 @@ def with_lock() -> bool: f"Reloaded Samba config in " f"{format_duration(time.monotonic() - phase_started)}" ) - log( - f"Reconciliation completed in " - f"{format_duration(time.monotonic() - reconcile_started)}" - ) + elapsed = format_duration(time.monotonic() - reconcile_started) + log(f"Reconciliation completed in {elapsed}") + if RECONCILE_STATUS is not None: + RECONCILE_STATUS.complete(f"Reconciliation completed in {elapsed}") return True finally: lock_file.close() def main() -> int: + global RECONCILE_STATUS + configure_logging() + RECONCILE_STATUS = ReconcileStatus( + os.getenv("RECONCILE_STATUS_FILE", DEFAULT_RECONCILE_STATUS_FILE).strip() + ) try: - ok = with_lock() - return 0 if ok else 0 - except Exception as exc: # pylint: disable=broad-except - log(f"ERROR: {exc}") - return 1 + try: + ok = with_lock() + return 0 if ok else 0 + except Exception as exc: # pylint: disable=broad-except + log(f"ERROR: {exc}") + RECONCILE_STATUS.fail(str(exc)) + return 1 + finally: + close_logging() if __name__ == "__main__": diff --git a/app/web/app.js b/app/web/app.js index 4eb84d0..bd87218 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -29,8 +29,15 @@ const nodeTypeLabel = value => ({group: "Gruppe", user: "Benutzer", computer: "C const backupStateLabel = value => ({starting: "Startet", running: "Läuft", completed: "Abgeschlossen", failed: "Fehlgeschlagen", waiting: "Wartet", disabled: "Deaktiviert"}[value] || value || "Unbekannt"); function backupMessage(data) { const message = String(data.message || ""); - if (!message) return data.enabled ? `Täglich um ${String(data.scheduledHour).padStart(2, "0")}:00 UTC geplant.` : "Sicherungen sind nicht eingerichtet."; + if (!message) { + if (!data.enabled) return "Sicherungen sind nicht eingerichtet."; + if (!data.automaticEnabled) return "Automatische Sicherungen sind ausgeschaltet; manueller Start ist verfügbar."; + return `Täglich um ${String(data.scheduledHour).padStart(2, "0")}:00 UTC geplant.`; + } if (message === "Starting backup") return "Sicherung wird gestartet."; + if (message === "Creating consistent state database snapshot") return "Konsistenter Datenbankstand wird erstellt."; + if (message === "Creating encrypted group archives") return "Verschlüsselte Gruppenarchive werden erstellt."; + if (message.startsWith("Archiving group ")) return `Gruppenarchiv ${message.slice(16)} wird erstellt.`; if (message === "Measuring backup payload") return "Sicherungsumfang wird ermittelt."; if (message.startsWith("Backup payload: ")) return `Sicherungsumfang: ${message.slice(16)}`; if (message.startsWith("Syncing ")) return `${message.slice(8)} wird synchronisiert.`; @@ -94,8 +101,10 @@ function routeFor(path) { if (path.startsWith("/storage/data")) return "storage-data"; if (path.startsWith("/storage/users")) return "storage-users"; if (path.startsWith("/shares")) return "shares"; + if (path.startsWith("/reconciliation")) return "reconciliation"; if (path.startsWith("/activity")) return "activity"; if (path.startsWith("/backup")) return "backup"; + if (path.startsWith("/report")) return "report"; if (path.startsWith("/system")) return "system"; return "overview"; } @@ -111,10 +120,12 @@ async function navigate(path, replace = false) { try { if (route === "overview") await renderOverview(); if (route === "shares") await renderShares(); + if (route === "reconciliation") await renderReconciliation(); if (route === "storage-data") await renderStorage("data"); if (route === "storage-users") await renderStorage("users"); if (route === "activity") await renderActivity(); if (route === "backup") await renderBackup(); + if (route === "report") await renderReport(); if (route === "system") await renderSystem(); content.focus(); } catch (error) { @@ -270,33 +281,241 @@ async function renderActivity() { await load(false); } +function triggerLabel(value) { + return ({automatic: "Automatisch", web: "Weboberfläche", manual: "Befehlszeile", startup: "Systemstart"}[value] || value || "—"); +} + +function processIsActive(data) { + return ["starting", "running"].includes(data?.state); +} + function backupMarkup(data) { - const stateName = data.state || (data.enabled ? "waiting" : "disabled"); + const stateName = data.enabled ? (data.state || "waiting") : "disabled"; const active = data.activeFiles || []; + const automatic = data.automaticEnabled + ? `Täglich um ${String(data.scheduledHour).padStart(2, "0")}:00 UTC` + : data.enabled ? "Ausgeschaltet" : "Nicht eingerichtet"; return `
Status${esc(backupStateLabel(stateName))}
Fortschritt${decimal(data.percent)} %
Übertragen${bytes(data.transferredBytes)}
-
Gestartet${data.startedAt ? esc(utcTime(data.startedAt)) : "—"}
+
Automatik${esc(automatic)}

Aktueller Lauf

${badge(backupStateLabel(stateName), stateName === "failed" ? "error" : "")}

${esc(backupMessage(data))}

${bytes(data.transferredBytes)} / ${bytes(data.totalBytes)}${esc(data.currentSource || "")}${esc(data.snapshot || "")}
+
Auslöser
${esc(triggerLabel(data.trigger))}
Gestartet
${esc(utcTime(data.startedAt))}
Beendet
${esc(utcTime(data.finishedAt))}
${active.length ? `

Dateien in Bearbeitung

${active.map(file => ``).join("")}
DateiFortschrittÜbertragen
${esc(file.path)}${bytes(file.transferredBytes)} / ${bytes(file.totalBytes)}
` : ""}
-

Sicherungsprotokoll

Letzte ${data.log?.length || 0} Zeilen
${esc((data.log || []).join("\n") || "Noch keine Ausgabe vorhanden.")}
`; +

Sicherungsprotokoll

Letzte ${data.log?.length || 0} Zeilen
${esc((data.log || []).join("\n") || "Noch keine Ausgabe vorhanden.")}
`; } async function renderBackup() { - content.innerHTML = pageHead("Sicherungen", "Live-Fortschritt und letzte Sicherungsausgabe. Auf dieser Seite können Sicherungen weder gestartet noch geändert werden.") + '
'; - const refresh = async () => { - try { document.querySelector("#backup-body").innerHTML = backupMarkup(await api("/api/backup")); } - catch (error) { notice(error.message); } + content.innerHTML = pageHead( + "Sicherungen", + "Sicherungen manuell starten sowie Fortschritt und Ausgabe verfolgen.", + '' + ) + '
'; + const button = document.querySelector("#start-backup"); + let logLines = []; + let previousActive = false; + let polling = false; + const refresh = async includeLog => { + try { + const currentLog = document.querySelector("#backup-log"); + const previousScrollTop = currentLog?.scrollTop || 0; + const data = await api(includeLog ? "/api/backup" : "/api/backup?log=0"); + if (Array.isArray(data.log)) logLines = data.log; + data.log = logLines; + document.querySelector("#backup-body").innerHTML = backupMarkup(data); + button.disabled = !data.manualEnabled || processIsActive(data); + button.title = data.manualEnabled ? "" : "Es ist kein Sicherungsziel eingerichtet."; + const logView = document.querySelector("#backup-log"); + if (logView) { + logView.scrollTop = includeLog ? logView.scrollHeight : previousScrollTop; + } + return data; + } catch (error) { + notice(error.message); + return null; + } }; - await refresh(); - state.timer = window.setInterval(refresh, 2000); + const poll = async () => { + if (polling) return; + polling = true; + try { + const status = await refresh(false); + if (!status) return; + const active = processIsActive(status); + if (active || previousActive) { + const complete = await refresh(true); + previousActive = complete ? processIsActive(complete) : active; + } else { + previousActive = false; + } + } finally { + polling = false; + } + }; + button.addEventListener("click", async () => { + if (!window.confirm("Sicherung jetzt starten?")) return; + button.disabled = true; + try { + await api("/api/actions/backup", {method: "POST", body: "{}"}); + notice("Sicherung wurde gestartet."); + await poll(); + } catch (error) { + notice(error.message); + button.disabled = false; + } + }); + const initial = await refresh(true); + previousActive = processIsActive(initial); + state.timer = window.setInterval(() => { void poll(); }, 2000); +} + +const reconciliationStateLabel = value => ({ + starting: "Startet", + running: "Läuft", + completed: "Abgeschlossen", + failed: "Fehlgeschlagen", + waiting: "Wartet", +}[value] || value || "Unbekannt"); + +const reconciliationPhaseLabel = value => ({ + starting: "Vorbereitung", + winbind: "Domänenzwischenspeicher", + directory: "Active Directory", + database: "Freigabendatenbank", + "data-permissions": "Datenberechtigungen", + fslogix: "FSLogix", + private: "Private Ordner", + samba: "Samba-Konfiguration", + completed: "Abgeschlossen", + failed: "Fehlgeschlagen", + waiting: "Wartet", +}[value] || value || "—"); + +function reconciliationMarkup(data) { + const stateName = data.state || "waiting"; + const phase = reconciliationPhaseLabel(data.phase); + const current = data.currentItem ? ` · ${data.currentItem}` : ""; + const message = stateName === "failed" + ? (data.message || "Der Freigabenabgleich ist fehlgeschlagen.") + : `${phase}${current}`; + return ` +
+
Status${esc(reconciliationStateLabel(stateName))}
+
Fortschritt${decimal(data.percent)} %
+
Gestartet${esc(utcTime(data.startedAt))}
+
AutomatikAlle ${esc(data.scheduledIntervalMinutes || 5)} Minuten
+
+

Aktueller Abgleich

${badge(reconciliationStateLabel(stateName), stateName === "failed" ? "error" : "")}
+

${esc(message)}

+ +
Phase
${esc(phase)}
Auslöser
${esc(triggerLabel(data.trigger))}
Beendet
${esc(utcTime(data.finishedAt))}
+
+

Abgleichsprotokoll

Letzte ${data.log?.length || 0} Zeilen
${esc((data.log || []).join("\n") || "Noch keine Ausgabe vorhanden.")}
`; +} + +async function renderReconciliation() { + content.innerHTML = pageHead( + "Freigabenabgleich", + "Freigaben, Gruppenordner und Berechtigungen sofort mit Active Directory abgleichen.", + '' + ) + '
'; + const button = document.querySelector("#start-reconciliation"); + let logLines = []; + let previousActive = false; + let polling = false; + const refresh = async includeLog => { + try { + const currentLog = document.querySelector("#reconciliation-log"); + const previousScrollTop = currentLog?.scrollTop || 0; + const data = await api( + includeLog ? "/api/reconciliation" : "/api/reconciliation?log=0" + ); + if (Array.isArray(data.log)) logLines = data.log; + data.log = logLines; + document.querySelector("#reconciliation-body").innerHTML = reconciliationMarkup(data); + button.disabled = processIsActive(data); + const logView = document.querySelector("#reconciliation-log"); + if (logView) { + logView.scrollTop = includeLog ? logView.scrollHeight : previousScrollTop; + } + return data; + } catch (error) { + notice(error.message); + return null; + } + }; + const poll = async () => { + if (polling) return; + polling = true; + try { + const status = await refresh(false); + if (!status) return; + const active = processIsActive(status); + if (active || previousActive) { + const complete = await refresh(true); + previousActive = complete ? processIsActive(complete) : active; + } else { + previousActive = false; + } + } finally { + polling = false; + } + }; + button.addEventListener("click", async () => { + if (!window.confirm("Freigaben jetzt mit Active Directory abgleichen?")) return; + button.disabled = true; + try { + await api("/api/actions/reconciliation", {method: "POST", body: "{}"}); + notice("Freigabenabgleich wurde gestartet."); + await poll(); + } catch (error) { + notice(error.message); + button.disabled = false; + } + }); + const initial = await refresh(true); + previousActive = processIsActive(initial); + state.timer = window.setInterval(() => { void poll(); }, 1500); +} + +async function renderReport() { + content.innerHTML = pageHead("PDF-Bericht", "Vollständiger, druckbarer Stand der Dateiserver-Konfiguration und Belegung.") + ` +
+

Enthaltene Informationen

+
    +
  • Speicherbelegung von Daten-, Private- und FSLogix-Bereichen
  • +
  • Alle Dateifreigabegruppen mit vollständiger Mitgliedschaftshierarchie
  • +
  • Aktueller Sicherungsstatus und laufende Dateiübertragungen
  • +
  • Systemprüfungen und TLS-Zertifikatsdaten
  • +
+

Aktivitätsprotokoll und Sicherungsprotokoll sind ausdrücklich nicht enthalten.

+

+

Die Erstellung erfolgt vollständig in diesem Browser mit Typst.

+
`; + const button = document.querySelector("#create-report"); + const status = document.querySelector("#report-status"); + button.addEventListener("click", async () => { + button.disabled = true; + try { + status.textContent = "Berichtsdaten werden geladen…"; + const data = await api("/api/report"); + status.textContent = "Typst wird geladen…"; + const {createPdfReport} = await import("/assets/report.mjs"); + const result = await createPdfReport(data, message => { status.textContent = message; }); + status.textContent = `PDF erstellt: ${result.filename} (${bytes(result.size)})`; + } catch (error) { + status.textContent = `PDF konnte nicht erstellt werden: ${error.message}`; + } finally { + button.disabled = false; + } + }); } async function renderSystem() { diff --git a/app/web/index.html b/app/web/index.html index c962199..44d78da 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -28,10 +28,12 @@ diff --git a/app/web/report.mjs b/app/web/report.mjs new file mode 100644 index 0000000..fec2126 --- /dev/null +++ b/app/web/report.mjs @@ -0,0 +1,348 @@ +"use strict"; + +const TYPE_LABELS = {group: "Gruppe", user: "Benutzer", computer: "Computer", unknown: "Unbekannt"}; +const BACKUP_LABELS = {starting: "Startet", running: "Läuft", completed: "Abgeschlossen", failed: "Fehlgeschlagen", waiting: "Wartet", disabled: "Deaktiviert"}; + +function typstString(value) { + return '"' + String(value ?? "") + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/\r/g, "\\r") + .replace(/\n/g, "\\n") + .replace(/\t/g, "\\t") + '"'; +} + +function formatBytes(value) { + let number = Number(value || 0); + const units = ["B", "kB", "MB", "GB", "TB", "PB"]; + let unit = 0; + while (Math.abs(number) >= 1024 && unit < units.length - 1) { + number /= 1024; + unit += 1; + } + const digits = number >= 100 || unit === 0 ? 0 : 1; + return number.toLocaleString("de-DE", { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }) + " " + units[unit]; +} + +function formatDecimal(value, digits = 1) { + return Number(value || 0).toLocaleString("de-DE", { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); +} + +function formatUtc(value) { + if (!value) return "—"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "—"; + const pad = number => String(number).padStart(2, "0"); + return [ + pad(date.getUTCDate()), + ".", + pad(date.getUTCMonth() + 1), + ".", + date.getUTCFullYear(), + " ", + pad(date.getUTCHours()), + ":", + pad(date.getUTCMinutes()), + ":", + pad(date.getUTCSeconds()), + " UTC", + ].join(""); +} + +function fileTimestamp(value) { + const date = new Date(value); + const valid = Number.isNaN(date.getTime()) ? new Date() : date; + const pad = number => String(number).padStart(2, "0"); + return [ + valid.getUTCFullYear(), + "-", + pad(valid.getUTCMonth() + 1), + "-", + pad(valid.getUTCDate()), + "T", + pad(valid.getUTCHours()), + pad(valid.getUTCMinutes()), + pad(valid.getUTCSeconds()), + "Z", + ].join(""); +} + +function backupLabel(value, enabled) { + const state = value || (enabled ? "waiting" : "disabled"); + return BACKUP_LABELS[state] || state || "Unbekannt"; +} + +function backupMessage(data) { + const message = String(data.message || ""); + if (!message) { + if (!data.enabled) return "Sicherungen sind nicht eingerichtet."; + if (!data.automaticEnabled) { + return "Automatische Sicherungen sind ausgeschaltet; manueller Start ist verfügbar."; + } + return "Täglich um " + String(data.scheduledHour).padStart(2, "0") + ":00 UTC geplant."; + } + if (message === "Starting backup") return "Sicherung wird gestartet."; + if (message === "Creating consistent state database snapshot") { + return "Konsistenter Datenbankstand wird erstellt."; + } + if (message === "Creating encrypted group archives") { + return "Verschlüsselte Gruppenarchive werden erstellt."; + } + if (message.startsWith("Archiving group ")) { + return "Gruppenarchiv " + message.slice(16) + " wird erstellt."; + } + if (message === "Measuring backup payload") return "Sicherungsumfang wird ermittelt."; + if (message.startsWith("Backup payload: ")) return "Sicherungsumfang: " + message.slice(16); + if (message.startsWith("Syncing ")) return message.slice(8) + " wird synchronisiert."; + const completed = message.match(/^Backup completed; (\d+) snapshot\(s\) retained$/); + if (completed) return "Sicherung abgeschlossen; " + completed[1] + " Sicherungsstände werden aufbewahrt."; + if (data.state === "failed") return "Sicherung fehlgeschlagen."; + if (data.state === "completed") return "Sicherung abgeschlossen."; + if (data.state === "running") return "Sicherung läuft."; + if (data.state === "starting") return "Sicherung wird gestartet."; + return message; +} + +function textCell(value, weight = "") { + const option = weight ? 'weight: "' + weight + '", ' : ""; + return "[#text(" + option + typstString(value) + ")]"; +} + +function reportTable(columns, aligns, headers, rows) { + const cells = []; + for (const row of rows) { + for (const value of row) cells.push(textCell(value)); + } + return [ + "#table(", + " columns: (" + columns.join(", ") + ",),", + " align: (" + aligns.join(", ") + ",),", + " inset: (x: 4pt, y: 3pt),", + " stroke: 0.35pt + luma(75%),", + " table.header(" + headers.map(value => textCell(value, "semibold")).join(", ") + "),", + cells.length ? " " + cells.join(",\n ") + "," : "", + ")", + ].filter(Boolean).join("\n"); +} + +function keyValueTable(rows) { + return reportTable(["34%", "66%"], ["left", "left"], ["Angabe", "Wert"], rows); +} + +function treeRows(nodes, depth = 0, result = []) { + for (const node of nodes || []) { + const type = TYPE_LABELS[node.type] || node.type || "Unbekannt"; + result.push( + "#tree-row(" + + depth + ", " + + typstString(type) + ", " + + typstString(node.name || node.sam || "Unbekannt") + ", " + + typstString(node.sam || "") + ", " + + (node.cycle ? "true" : "false") + + ")" + ); + treeRows(node.members || [], depth + 1, result); + } + return result; +} + +export function buildReportSource(data) { + const generatedAt = data.generatedAt || new Date().toISOString(); + const storage = data.storage || {}; + const totals = storage.totals || {}; + const groupsSnapshot = data.groups || {}; + const groups = groupsSnapshot.groups || []; + const backup = data.backup || {}; + const system = data.system || {}; + const tls = system.tls || {}; + const checks = system.checks || {}; + const totalUserBytes = Number(totals.privateBytes || 0) + Number(totals.fslogixBytes || 0); + const totalBytes = Number(totals.dataBytes || 0) + totalUserBytes; + const sans = (tls.sans || []) + .map(value => Array.isArray(value) ? value[1] : value) + .filter(Boolean) + .join(", ") || "—"; + + const parts = [ + '#set document(title: "Dateiserver-Bericht", author: "Dateiserver")', + '#set page(paper: "a4", margin: (x: 15mm, top: 14mm, bottom: 17mm), footer: context align(center, text(size: 7pt, fill: luma(45%))[Seite #counter(page).display("1")]))', + '#set text(font: "Libertinus Serif", size: 9pt, lang: "de")', + "#set par(leading: 0.58em)", + "#set heading(numbering: none)", + '#let tree-row(depth, kind, name, sam, cycle) = block(width: 100%, above: 1.8pt, below: 1.8pt, breakable: false)[#grid(columns: (depth * 9pt + 8pt, 44pt, 1fr), column-gutter: 4pt, align: (left, left, left), [#text(size: 7.5pt, fill: luma(45%))[#if depth == 0 [•] else [>]]], [#text(size: 7.5pt, fill: luma(40%))[#kind]], [#name#if sam != "" and sam != name [#text(size: 7.5pt, fill: luma(40%))[ (#sam)]]#if cycle [#text(size: 7.5pt)[ (Zyklus)]]])]', + "", + '#text(size: 22pt, weight: "semibold")[Dateiserver-Bericht]', + "#v(5pt)", + keyValueTable([ + ["Server", system.hostname || "—"], + ["Erstellt", formatUtc(generatedAt)], + ["Verzeichnisstand", formatUtc(groupsSnapshot.fetchedAt)], + ["Dateifreigabegruppen", String(groups.length)], + ["Verzeichnisabfrage gekürzt", groupsSnapshot.truncated ? "Ja" : "Nein"], + ["Speicherstand", formatUtc(storage.scannedAt)], + ["Serverzeit", formatUtc(system.serverTime)], + ]), + "", + "#v(8pt)", + "#text(size: 8pt)[Aktivitätsprotokoll und Sicherungsprotokoll sind nicht Bestandteil dieses Berichts.]", + "", + "= Speicherbelegung", + keyValueTable([ + ["Daten", formatBytes(totals.dataBytes)], + ["Private", formatBytes(totals.privateBytes)], + ["FSLogix", formatBytes(totals.fslogixBytes)], + ["Private + FSLogix", formatBytes(totalUserBytes)], + ["Gesamt", formatBytes(totalBytes)], + ["Prüfdauer", formatDecimal(storage.scanSeconds, 3) + " s"], + ]), + "", + "== Datenbelegung je Gruppenordner", + (storage.groups || []).length + ? reportTable( + ["1fr", "auto"], + ["left", "right"], + ["Gruppenordner", "Belegung"], + storage.groups.map(row => [row.name || "—", formatBytes(row.bytes)]) + ) + : "Noch keine Speicherprüfung vorhanden.", + "", + "== Benutzerbelegung", + (storage.users || []).length + ? reportTable( + ["1fr", "auto", "auto", "auto"], + ["left", "right", "right", "right"], + ["Benutzer", "Private", "FSLogix", "Gesamt"], + storage.users.map(row => [ + row.name || "—", + formatBytes(row.privateBytes), + formatBytes(row.fslogixBytes), + formatBytes(row.totalBytes), + ]) + ) + : "Noch keine Speicherprüfung vorhanden.", + "", + "= Dateifreigaben und Mitgliedschaften", + groupsSnapshot.truncated + ? "#text(weight: \"semibold\")[Hinweis: Die Verzeichnisabfrage wurde gekürzt; die nachfolgende Hierarchie ist nicht vollständig.]" + : "Die nachfolgenden Hierarchien zeigen alle wirksamen verschachtelten Mitgliedschaften.", + ]; + + for (const group of groups) { + parts.push(""); + parts.push("#heading(level: 2, " + typstString(group.folder || group.name || group.sam || "Unbekannt") + ")"); + parts.push(keyValueTable([ + ["Freigabename", group.name || "—"], + ["Ordner", group.folder || "—"], + ["Ordnergruppe", group.sam || "—"], + ["Objekt-GUID", group.guid || "—"], + ["Status", group.active ? "Aktiv" : "Archiviert"], + ["Wirksame Benutzer", String(group.userCount || 0)], + ["Enthaltene Gruppen", String(group.groupCount || 0)], + ])); + const rows = treeRows(group.members || []); + parts.push(rows.length ? rows.join("\n") : "Keine direkten Mitglieder."); + } + + const backupState = backupLabel(backup.state, backup.enabled); + parts.push(""); + parts.push("= Sicherung"); + parts.push(keyValueTable([ + ["Eingerichtet", backup.enabled ? "Ja" : "Nein"], + ["Automatik", backup.automaticEnabled ? "Aktiv" : "Ausgeschaltet"], + [ + "Geplanter Start", + backup.automaticEnabled + ? String(backup.scheduledHour ?? 0).padStart(2, "0") + ":00 UTC" + : "—", + ], + ["Status", backupState], + ["Fortschritt", formatDecimal(backup.percent) + " %"], + ["Übertragen", formatBytes(backup.transferredBytes) + " / " + formatBytes(backup.totalBytes)], + ["Gestartet", formatUtc(backup.startedAt)], + ["Beendet", formatUtc(backup.finishedAt)], + ["Sicherungsstand", backup.snapshot || "—"], + ["Aktuelle Quelle", backup.currentSource || "—"], + ["Statusmeldung", backupMessage(backup)], + ])); + + if ((backup.activeFiles || []).length) { + parts.push(""); + parts.push("== Dateien in Bearbeitung"); + parts.push(reportTable( + ["1fr", "auto", "auto"], + ["left", "right", "right"], + ["Datei", "Fortschritt", "Übertragen"], + backup.activeFiles.map(file => [ + file.path || "—", + formatDecimal(file.percent) + " %", + formatBytes(file.transferredBytes) + " / " + formatBytes(file.totalBytes), + ]) + )); + } + + parts.push(""); + parts.push("= System"); + parts.push(keyValueTable([ + ["Domänenvertrauen", checks.domainTrust ? "In Ordnung" : "Fehlgeschlagen"], + ["Samba-Konfiguration", checks.sambaConfig ? "Gültig" : "Fehlgeschlagen"], + ])); + parts.push(""); + parts.push("== TLS-Zertifikat"); + parts.push(keyValueTable([ + ["Allgemeiner Name", (tls.subject || {}).commonName || "—"], + ["Aussteller", (tls.issuer || {}).commonName || "—"], + ["Gültig bis", formatUtc(tls.notAfter)], + ["Namen", sans], + ])); + parts.push(""); + parts.push("#text(size: 8pt)[Dieser Bericht wurde vollständig im Browser mit Typst erzeugt.]"); + + return parts.join("\n"); +} + +let compilerPromise; + +async function getCompiler() { + if (!compilerPromise) { + compilerPromise = (async () => { + const module = await import("/assets/vendor/typst/0.6.0-csp1/typst.mjs"); + module.$typst.setCompilerInitOptions({ + getModule: () => ({module_or_path: "/assets/vendor/typst/0.6.0-csp1/compiler.wasm"}), + }); + module.$typst.use( + module.TypstSnippet.disableDefaultFontAssets(), + module.TypstSnippet.preloadFontFromUrl("/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Regular.otf"), + module.TypstSnippet.preloadFontFromUrl("/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Semibold.otf") + ); + return module.$typst; + })(); + } + return compilerPromise; +} + +export async function createPdfReport(data, progress = () => {}) { + progress("Typst-Compiler wird im Browser geladen…"); + const compiler = await getCompiler(); + progress("PDF wird im Browser gesetzt…"); + const pdf = await compiler.pdf({mainContent: buildReportSource(data)}); + if (!(pdf instanceof Uint8Array) || pdf.length < 5) { + throw new Error("Typst hat keine gültige PDF-Datei erzeugt."); + } + + const filename = "dateiserver-bericht-" + fileTimestamp(data.generatedAt) + ".pdf"; + const url = URL.createObjectURL(new Blob([pdf], {type: "application/pdf"})); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + window.setTimeout(() => URL.revokeObjectURL(url), 1000); + return {filename, size: pdf.length}; +} diff --git a/app/web/vendor/typst/LICENSE-typst-assets b/app/web/vendor/typst/LICENSE-typst-assets new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/app/web/vendor/typst/LICENSE-typst-assets @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/app/web/vendor/typst/LICENSE-typst-ts b/app/web/vendor/typst/LICENSE-typst-ts new file mode 100644 index 0000000..b15fd19 --- /dev/null +++ b/app/web/vendor/typst/LICENSE-typst-ts @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023-2025 Myriad-Dreamin + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/app/web/vendor/typst/LibertinusSerif-Regular.otf b/app/web/vendor/typst/LibertinusSerif-Regular.otf new file mode 100644 index 0000000..508f3c0 Binary files /dev/null and b/app/web/vendor/typst/LibertinusSerif-Regular.otf differ diff --git a/app/web/vendor/typst/LibertinusSerif-Semibold.otf b/app/web/vendor/typst/LibertinusSerif-Semibold.otf new file mode 100644 index 0000000..45313ff Binary files /dev/null and b/app/web/vendor/typst/LibertinusSerif-Semibold.otf differ diff --git a/app/web/vendor/typst/NOTICE-fonts b/app/web/vendor/typst/NOTICE-fonts new file mode 100644 index 0000000..722468e --- /dev/null +++ b/app/web/vendor/typst/NOTICE-fonts @@ -0,0 +1,447 @@ +Licenses for third party components bundled by this project can be found below. + +================================================================================ +The SIL Open Font License Version 1.1 applies to: + +* Libertinus Serif fonts in files/fonts/LibertinusSerif-*.otf + Copyright © 2012-2024 The Libertinus Project Authors, + with Reserved Font Name "Linux Libertine", "Biolinum", "STIX Fonts". + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. +================================================================================ + +================================================================================ +The GUST Font License Version 1.0 applies to: + +* NewComputerModern fonts in files/fonts/NewCM*.otf + +% This is version 1.0, dated 22 June 2009, of the GUST Font License. +% (GUST is the Polish TeX Users Group, http://www.gust.org.pl) +% +% For the most recent version of this license see +% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt +% or +% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt +% +% This work may be distributed and/or modified under the conditions +% of the LaTeX Project Public License, either version 1.3c of this +% license or (at your option) any later version. +% +% Please also observe the following clause: +% 1) it is requested, but not legally required, that derived works be +% distributed only after changing the names of the fonts comprising this +% work and given in an accompanying "manifest", and that the +% files comprising the Work, as listed in the manifest, also be given +% new names. Any exceptions to this request are also given in the +% manifest. +% +% We recommend the manifest be given in a separate file named +% MANIFEST-.txt, where is some unique identification +% of the font family. If a separate "readme" file accompanies the Work, +% we recommend a name of the form README-.txt. +% +% The latest version of the LaTeX Project Public License is in +% http://www.latex-project.org/lppl.txt and version 1.3c or later +% is part of all distributions of LaTeX version 2006/05/20 or later. +================================================================================ + +================================================================================ +The terms below apply to: + +* DejaVu fonts in files/fonts/DejaVu*.ttf + (https://github.com/dejavu-fonts/dejavu-fonts) + +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +TeX Gyre DJV Math +----------------- +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski +(on behalf of TeX users groups) are in public domain. + +Letters imported from Euler Fraktur from AMSfonts are (c) American +Mathematical Society (see below). +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera +is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license (“Fonts”) and associated +documentation +files (the “Font Software”), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, +and/or sell copies of the Font Software, and to permit persons to whom +the Font Software is furnished to do so, subject to the following +conditions: + +The above copyright and trademark notices and this permission notice +shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional +glyphs or characters may be added to the Fonts, only if the fonts are +renamed +to names not containing either the words “Bitstream” or the word “Vera”. + +This License becomes null and void to the extent applicable to Fonts or +Font Software +that has been modified and is distributed under the “Bitstream Vera” +names. + +The Font Software may be sold as part of a larger software package but +no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, +SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN +ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE +THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. +Except as contained in this notice, the names of GNOME, the GNOME +Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote +the sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. +For further information, contact: fonts at gnome dot org. + +AMSFonts (v. 2.2) copyright + +The PostScript Type 1 implementation of the AMSFonts produced by and +previously distributed by Blue Sky Research and Y&Y, Inc. are now freely +available for general use. This has been accomplished through the +cooperation +of a consortium of scientific publishers with Blue Sky Research and Y&Y. +Members of this consortium include: + +Elsevier Science IBM Corporation Society for Industrial and Applied +Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) + +In order to assure the authenticity of these fonts, copyright will be +held by +the American Mathematical Society. This is not meant to restrict in any way +the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, +has requested that any alterations which yield different font metrics be +given a different name. + +$Id$ +================================================================================ + +================================================================================ +The Creative Commons Zero v1.0 Universal License applies to: +* The ICC profiles found in `files/icc/*-v4.icc` and + +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/app/web/vendor/typst/README.md b/app/web/vendor/typst/README.md new file mode 100644 index 0000000..46d2177 --- /dev/null +++ b/app/web/vendor/typst/README.md @@ -0,0 +1,29 @@ +# Vendored Typst client + +The PDF report is compiled entirely in the browser. These files are kept local so report data, Typst source, fonts, and PDF output never need a third-party service or CDN. + +Pinned upstream components: + +- `@myriaddreamin/typst.ts@0.6.0`, `dist/esm/contrib/all-in-one-lite.bundle.js` + - npm integrity: `sha512-IUpetG0NyF2H6eXRm4j+NsbanJHIvyrffHEijqYb6q128sLWQgT1FYJS+h7dtjXmrBEfnIl1mI80DyfDR6kB/w==` + - upstream file SHA-256: `e884db7b1dbb3d13b85a728509e4f0b65ca36b2ee36f950406a9f77d54461cd7` + - vendored file SHA-256: `24d3bec0e8bcf34666425c08859a1b0f15af45d5f5ad27f67132256d6ad0d958` +- `@myriaddreamin/typst-ts-web-compiler@0.6.0`, `pkg/typst_ts_web_compiler_bg.wasm` + - npm integrity: `sha512-P/eIJ5RnfElj0NYzn5PI296t/IwWtgqUyyTMi5Jm5X3V5kZfskkH+LI7mSQe8tEyxwgCvxbxvFe5adinA3K8Gg==` + - local SHA-256: `52995fcbcda9287b97b27996fe9b91057e4ca87fadb41b36d100bc45d33d9454` +- Libertinus Serif Regular and Semibold from `typst/typst-assets@v0.13.1` + - Regular SHA-256: `fcf06307a77367394fcb0ccb241e59eea70dba3d732be309647611224679c733` + - Semibold SHA-256: `a4b3f28e85881db34695c1f005e4c79233a6caf3a2bd286c9b418c025fb99308` + +The immutable browser URL is revisioned as `0.6.0-csp1`; change that URL revision whenever any vendored client asset changes. + +Local modification: `typst.mjs` replaces the generated `new Function` import helpers and the five fixed compiler-construction functions with native imports and explicit closures. Unknown function bodies fail closed. This lets the application keep JavaScript `'unsafe-eval'` disabled while granting only CSP `'wasm-unsafe-eval'` for the compiler itself. + +Upstream sources: + +- +- +- +- + +The Typst wrapper and compiler are Apache-2.0; see `LICENSE-typst-ts`. The asset repository license is in `LICENSE-typst-assets`. The Libertinus fonts are SIL Open Font License 1.1; the applicable copyright and full license text are retained in `NOTICE-fonts`. diff --git a/app/web/vendor/typst/compiler.wasm b/app/web/vendor/typst/compiler.wasm new file mode 100644 index 0000000..95aa840 Binary files /dev/null and b/app/web/vendor/typst/compiler.wasm differ diff --git a/app/web/vendor/typst/typst.mjs b/app/web/vendor/typst/typst.mjs new file mode 100644 index 0000000..359b488 --- /dev/null +++ b/app/web/vendor/typst/typst.mjs @@ -0,0 +1,6189 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY VITE +if you want to view the source, please visit the github repository https://github.com/Myriad-Dreamin/typst.ts/blob/main/packages/typst.ts +*/ +// Local CSP hardening: the generated wasm-bindgen glue requests only these +// trivial functions while constructing the Typst compiler. Keep JavaScript +// dynamic code generation disabled and fail closed if upstream adds another. +function createCspSafeFunction(...parts) { + const body = parts.pop(); + if (parts.length === 0 && body === "return 0") + return () => 0; + if (parts.length === 0 && body === "return true") + return () => true; + if (parts.length === 1 && parts[0] === "path" && body === "return path") + return (path) => path; + const errors = { + "throw new Error('Dummy AccessModel, please initialize compiler with withAccessModel()')": "Dummy AccessModel, please initialize compiler with withAccessModel()", + "throw new Error('Dummy Registry, please initialize compiler with withPackageRegistry()')": "Dummy Registry, please initialize compiler with withPackageRegistry()" + }; + if (parts.length === 0 && errors[body]) + return () => { + throw new Error(errors[body]); + }; + throw new Error("Unsupported dynamic function requested by the Typst compiler"); +} +var gr = Object.defineProperty; +var fr = (r, e, t) => e in r ? gr(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t; +var l = (r, e, t) => fr(r, typeof e != "symbol" ? e + "" : e, t); +const wr = [ + "DejaVuSansMono-Bold.ttf", + "DejaVuSansMono-BoldOblique.ttf", + "DejaVuSansMono-Oblique.ttf", + "DejaVuSansMono.ttf", + "LibertinusSerif-Bold.otf", + "LibertinusSerif-BoldItalic.otf", + "LibertinusSerif-Italic.otf", + "LibertinusSerif-Regular.otf", + "LibertinusSerif-Semibold.otf", + "LibertinusSerif-SemiboldItalic.otf", + "NewCM10-Bold.otf", + "NewCM10-BoldItalic.otf", + "NewCM10-Italic.otf", + "NewCM10-Regular.otf", + "NewCMMath-Bold.otf", + "NewCMMath-Book.otf", + "NewCMMath-Regular.otf" +], hr = [ + "InriaSerif-Bold.ttf", + "InriaSerif-BoldItalic.ttf", + "InriaSerif-Italic.ttf", + "InriaSerif-Regular.ttf", + "Roboto-Regular.ttf", + "NotoSerifCJKsc-Regular.otf" +], pr = ["TwitterColorEmoji.ttf", "NotoColorEmoji.ttf"]; +function br() { + return ne([], { assets: !1 }); +} +function mr(r) { + return ne([], r); +} +function yr(r) { + var t, n; + const e = []; + if (r && (r == null ? void 0 : r.assets) !== !1 && ((t = r == null ? void 0 : r.assets) != null && t.length) && ((n = r == null ? void 0 : r.assets) == null ? void 0 : n.length) > 0) { + let i = { + text: "https://cdn.jsdelivr.net/gh/typst/typst-assets@v0.13.1/files/fonts/", + _: "https://cdn.jsdelivr.net/gh/typst/typst-dev-assets@v0.13.1/files/fonts/" + }, s = r.assetUrlPrefix ?? i; + typeof s == "string" ? s = { _: s } : s = { ...i, ...s }; + for (const a of Object.keys(s)) { + const d = s[a]; + d[d.length - 1] !== "/" && (s[a] = d + "/"); + } + const o = (a, d) => d.map((_) => (s[a] || s._) + _); + for (const a of r.assets) + switch (a) { + case "text": + e.push(...o(a, wr)); + break; + case "cjk": + e.push(...o(a, hr)); + break; + case "emoji": + e.push(...o(a, pr)); + break; + } + } + return e; +} +function ne(r, e) { + const t = yr(e), n = async (i, { ref: s, builder: o }) => { + e != null && e.fetcher && s.setFetcher(e.fetcher), await s.loadFonts(o, [...r, ...t]); + }; + return n._preloadRemoteFontOptions = e, n._kind = "fontLoader", n; +} +function Ze({ byFamily: r }) { + return async (e, { builder: t }) => { + const n = performance.now(); + if ("queryLocalFonts" in window) { + const s = await window.queryLocalFonts(); + r = r ?? []; + for (const o of s) { + if (!r.includes(o.family)) + continue; + const a = await (await o.blob()).arrayBuffer(); + await t.add_raw_font(new Uint8Array(a)); + } + } + const i = performance.now(); + console.log("preload system font time used:", i - n); + }; +} +function fe(r) { + return async (e, { builder: t }) => new Promise((n) => { + t.set_package_registry(r, function(i) { + return r.resolve(i, this); + }), n(); + }); +} +function ze(r) { + return async (e, t) => { + var n; + if (t.alreadySetAccessModel) + throw new Error( + `already set some assess model before: ${(n = t.alreadySetAccessModel.constructor) == null ? void 0 : n.name}(${t.alreadySetAccessModel})` + ); + return t.alreadySetAccessModel = r, new Promise((i) => { + t.builder.set_access_model( + r, + (s) => { + const o = r.getMTime(s); + return o ? o.getTime() : 0; + }, + (s) => r.isFile(s) || !1, + (s) => r.getRealPath(s) || s, + (s) => r.readAll(s) + ), i(); + }); + }; +} +class vr { + constructor(e, t) { + l(this, "fullyCached"); + l(this, "mTimes", /* @__PURE__ */ new Map()); + l(this, "mRealPaths", /* @__PURE__ */ new Map()); + l(this, "mData", /* @__PURE__ */ new Map()); + this.root = e, e.endsWith("/") && (this.root = this.root.slice(0, this.root.length - 1)), t != null && t.polyfillHeadRequest, this.fullyCached = !!(t != null && t.fullyCached); + } + reset() { + this.mTimes.clear(), this.mRealPaths.clear(), this.mData.clear(); + } + resolvePath(e) { + return this.root + e; + } + insertFile(e, t, n) { + this.mTimes.set(e, n), this.mData.set(e, t); + } + removeFile(e) { + this.mTimes.delete(e), this.mData.delete(e); + } + async getPreloadScript() { + const e = []; + e.push("((async () => {"), e.push( + "const snapshot = { root: '', mTimes: new Map(), mRealPaths: new Map(), mData: [],};" + ), e.push("const runFetch = async (path) => {"), e.push(" const res = await fetch(snapshot.root + path);"), e.push(" const buffer = await res.arrayBuffer();"), e.push(" return [path, new Uint8Array(buffer)];"), e.push("};"), e.push(`snapshot.root = ${JSON.stringify(this.root)};`), e.push( + `snapshot.mTimes = new Map([${[...this.mTimes.entries()].map(([n, i]) => `[${JSON.stringify(n)}, ${(i == null ? void 0 : i.getTime()) || "undefined"}]`).join(", ")}]);` + ), e.push( + `snapshot.mRealPaths = new Map([${[...this.mRealPaths.entries()].map(([n, i]) => `[${JSON.stringify(n)}, ${JSON.stringify(i)}]`).join(", ")}]);` + ); + const t = await Promise.all( + [...this.mData.entries()].map(async ([n, i]) => (n = JSON.stringify(n), i ? `runFetch(${n})` : `Promise.resolve([${n}, undefined])`)) + ); + return e.push(`snapshot.mData = await Promise.all([${t.join(", ")}]);`), e.push("return snapshot;"), e.push("})())"), e.join(` +`); + } + getLastModified(e) { + const t = new XMLHttpRequest(); + return t.open("HEAD", e, !1), t.send(null), t.status === 200 ? t.getResponseHeader("Last-Modified") : null; + } + getMTimeInternal(e) { + const t = this.getLastModified(this.resolvePath(e)); + if (t) + return new Date(t); + } + getMTime(e) { + if (e.startsWith("/@memory/")) + return this.mTimes.has(e) ? this.mTimes.get(e) : void 0; + if (!this.fullyCached) + return this.getMTimeInternal(e); + if (this.mTimes.has(e)) + return this.mTimes.get(e); + const t = this.getMTimeInternal(e); + return this.mTimes.set(e, t), t; + } + // todo: isFile + isFile() { + return !0; + } + // todo: getRealPath + getRealPath(e) { + return e; + } + readAllInternal(e) { + const t = new XMLHttpRequest(); + if (t.overrideMimeType("text/plain; charset=x-user-defined"), t.open("GET", this.resolvePath(e), !1), t.send(null), t.status === 200 && (t.response instanceof String || typeof t.response == "string")) + return Uint8Array.from(t.response, (n) => n.charCodeAt(0)); + } + readAll(e) { + if (e.startsWith("/@memory/")) + return this.mData.has(e) ? this.mData.get(e) : void 0; + if (!this.fullyCached) + return this.readAllInternal(e); + if (this.mData.has(e)) + return this.mData.get(e); + const t = this.readAllInternal(e); + return this.mData.set(e, t), t; + } +} +class qe { + constructor() { + l(this, "mTimes", /* @__PURE__ */ new Map()); + l(this, "mData", /* @__PURE__ */ new Map()); + } + reset() { + this.mTimes.clear(), this.mData.clear(); + } + insertFile(e, t, n) { + this.mTimes.set(e, n), this.mData.set(e, t); + } + removeFile(e) { + this.mTimes.delete(e), this.mData.delete(e); + } + getMTime(e) { + if (e.startsWith("/@memory/") && this.mTimes.has(e)) + return this.mTimes.get(e); + } + isFile() { + return !0; + } + getRealPath(e) { + return e; + } + readAll(e) { + if (e.startsWith("/@memory/") && this.mData.has(e)) + return this.mData.get(e); + } +} +class Ne { + constructor(e) { + l(this, "cache", /* @__PURE__ */ new Map()); + this.am = e; + } + resolvePath(e) { + return `https://packages.typst.org/preview/${e.name}-${e.version}.tar.gz`; + } + pullPackageData(e) { + const t = new XMLHttpRequest(); + if (t.overrideMimeType("text/plain; charset=x-user-defined"), t.open("GET", this.resolvePath(e), !1), t.send(null), t.status === 200 && (t.response instanceof String || typeof t.response == "string")) + return Uint8Array.from(t.response, (n) => n.charCodeAt(0)); + } + resolve(e, t) { + if (e.namespace !== "preview") + return; + const n = this.resolvePath(e); + if (this.cache.has(n)) + return this.cache.get(n)(); + const i = this.pullPackageData(e); + if (!i) + return; + const s = `/@memory/fetch/packages/preview/${e.namespace}/${e.name}/${e.version}`, o = []; + t.untar(i, (d, _, f) => { + o.push([s + "/" + d, _, new Date(f)]); + }); + const a = () => { + for (const [d, _, f] of o) + this.am.insertFile(d, _, f); + return s; + }; + return this.cache.set(n, a), a(); + } +} +function Sr(r) { + return Math.random().toString(36).replace("0.", ""); +} +const Cr = ( + // @ts-ignore + typeof process < "u" && process.versions != null && process.versions.node != null +); +class U { + /** + * Create a new instance of {@link TypstSnippet}. + * @param cc the compiler instance, see {@link PromiseJust} and {@link TypstCompiler}. + * @param ex the renderer instance, see {@link PromiseJust} and {@link TypstRenderer}. + * + * @example + * + * Passes a global shared compiler instance that get initialized lazily: + * ```typescript + * const $typst = new TypstSnippet(() => { + * return createGlobalCompiler(createTypstCompiler, initOptions); + * }); + * + */ + constructor(e) { + /** @internal */ + l(this, "mainFilePath"); + /** @internal */ + l(this, "cc"); + /** @internal */ + l(this, "ex"); + l(this, "providers"); + /** @internal */ + l(this, "ccOptions"); + /** @internal */ + l(this, "exOptions"); + this.cc = (e == null ? void 0 : e.compiler) || U.buildLocalCompiler, this.ex = (e == null ? void 0 : e.renderer) || U.buildLocalRenderer, this.mainFilePath = "/main.typ", this.providers = []; + } + /** + * Set lazy initialized compiler instance for the utility instance. + * @param cc the compiler instance, see {@link PromiseJust} and {@link TypstCompiler}. + */ + setCompiler(e) { + this.cc = e; + } + /** + * Get an initialized compiler instance from the utility instance. + */ + async getCompiler() { + return typeof this.cc == "function" ? this.cc = await this.cc() : this.cc; + } + /** + * Set lazy initialized renderer instance for the utility instance. + * @param ex the renderer instance, see {@link PromiseJust} and {@link TypstRenderer}. + */ + setRenderer(e) { + this.ex = e; + } + /** + * Get an initialized renderer instance from the utility instance. + */ + async getRenderer() { + return typeof this.ex == "function" ? this.ex = await this.ex() : this.ex; + } + /** + * add providers for bullding the compiler or renderer component. + */ + use(...e) { + if (!this.providers) + throw new Error("already prepare uses for instances"); + this.providers.push(...e); + } + /** + * todo: add docs + */ + static preloadFontFromUrl(e) { + return U.preloadFonts([e]); + } + /** + * todo: add docs + */ + static preloadFontData(e) { + return U.preloadFonts([e]); + } + /** + * todo: add docs + */ + static preloadFonts(e) { + return { + key: "access-model", + forRoles: ["compiler"], + provides: [ne(e)] + }; + } + /** + * don't load any default font assets. + * todo: add docs + */ + static disableDefaultFontAssets() { + return { + key: "access-model", + forRoles: ["compiler"], + provides: [br()] + }; + } + /** + * todo: add docs + */ + static preloadFontAssets(e) { + return { + key: "access-model", + forRoles: ["compiler"], + provides: [mr(e)] + }; + } + /** + * Set accessl model for the compiler instance + * @example + * + * use memory access model + * + * ```typescript + * const m = new MemoryAccessModel(); + * $typst.use(TypstSnippet.withAccessModel(m)); + * ``` + */ + static withAccessModel(e) { + return { + key: "access-model", + forRoles: ["compiler"], + provides: [ze(e)] + }; + } + /** + * Set package registry for the compiler instance + * @example + * + * use a customized package registry + * + * ```typescript + * const n = new NodeFetchPackageRegistry(); + * $typst.use(TypstSnippet.withPackageRegistry(n)); + * ``` + */ + static withPackageRegistry(e) { + return { + key: "package-registry", + forRoles: ["compiler"], + provides: [fe(e)] + }; + } + /** + * Retrieve an access model to store the data of fetched files. + * Provide a PackageRegistry instance for the compiler instance. + * + * @example + * + * use default (memory) access model + * + * ```typescript + * $typst.use(await TypstSnippet.fetchPackageRegistry()); + * ``` + * + * @example + * + * use external access model + * + * ```typescript + * const m = new MemoryAccessModel(); + * $typst.use(TypstSnippet.withAccessModel(m), await TypstSnippet.fetchPackageRegistry(m)); + * ``` + */ + static fetchPackageRegistry(e) { + const t = e || new qe(), n = [ + ...e ? [] : [ze(t)], + fe(new Ne(t)) + ]; + return { + key: "package-registry$fetch", + forRoles: ["compiler"], + provides: n + }; + } + /** + * Retrieve a fetcher for fetching package data. + * Provide a PackageRegistry instance for the compiler instance. + * @example + * + * use a customized fetcher + * + * ```typescript + * import request from 'sync-request-curl'; + * const m = new MemoryAccessModel(); + * $typst.use(TypstSnippet.withAccessModel(m), await TypstSnippet.fetchPackageBy(m, (_, httpUrl) => { + * const response = request('GET', this.resolvePath(path), { + * insecure: true, + * }); + * + * if (response.statusCode === 200) { + * return response.getBody(undefined); + * } + * return undefined; + * })); + * ``` + */ + static fetchPackageBy(e, t) { + class n extends Ne { + pullPackageData(s) { + return t(s, this.resolvePath(s)); + } + } + return { + key: "package-registry$lambda", + forRoles: ["compiler"], + provides: [fe(new n(e))] + }; + } + /** + * Set compiler init options for initializing global instance {@link $typst}. + * See {@link InitOptions}. + */ + setCompilerInitOptions(e) { + this.requireIsUninitialized("compiler", this.cc), this.ccOptions = e; + } + /** + * Set renderer init options for initializing global instance {@link $typst}. + * See {@link InitOptions}. + */ + setRendererInitOptions(e) { + this.requireIsUninitialized("renderer", this.ex), this.exOptions = e; + } + /** + * Set shared main file path. + */ + setMainFilePath(e) { + this.mainFilePath = e; + } + /** + * Get shared main file path. + */ + getMainFilePath() { + return this.mainFilePath; + } + removeTmp(e) { + return e.mainFilePath.startsWith("/tmp/") ? this.unmapShadow(e.mainFilePath) : Promise.resolve(); + } + /** + * Add a source file to the compiler. + * See {@link TypstCompiler#addSource}. + */ + async addSource(e, t) { + (await this.getCompiler()).addSource(e, t); + } + /** + * Reset the shadow files. + * Note: this function is independent to the {@link reset} function. + * See {@link TypstCompiler#resetShadow}. + */ + async resetShadow() { + (await this.getCompiler()).resetShadow(); + } + /** + * Add a shadow file to the compiler. + * See {@link TypstCompiler#mapShadow}. + */ + async mapShadow(e, t) { + (await this.getCompiler()).mapShadow(e, t); + } + /** + * Remove a shadow file from the compiler. + * See {@link TypstCompiler#unmapShadow}. + */ + async unmapShadow(e) { + (await this.getCompiler()).unmapShadow(e); + } + /** + * Compile the document to vector (IR) format. + * See {@link SweetCompileOptions}. + */ + async vector(e) { + const t = await this.getCompileOptions(e); + return (await this.getCompiler()).compile(t).then((n) => n.result).finally(() => this.removeTmp(t)); + } + /** + * Compile the document to PDF format. + * See {@link SweetCompileOptions}. + */ + async pdf(e) { + const t = await this.getCompileOptions(e); + return t.format = "pdf", (await this.getCompiler()).compile(t).then((n) => n.result).finally(() => this.removeTmp(t)); + } + /** + * Compile the document to SVG format. + * See {@link SweetRenderOptions} and {@link RenderSvgOptions}. + */ + async svg(e) { + return this.transientRender( + e, + (t, n) => t.renderSvg({ + ...e, + renderSession: n + }) + ); + } + /** + * Compile the document to canvas operations. + * See {@link SweetRenderOptions} and {@link RenderToCanvasOptions}. + */ + async canvas(e, t) { + return this.transientRender( + t, + (n, i) => n.renderToCanvas({ + container: e, + ...t, + renderSession: i + }) + ); + } + /** + * Get semantic tokens for the document. + */ + async query(e) { + const t = await this.getCompileOptions(e); + return (await this.getCompiler()).query({ + ...e, + ...t + }).finally(() => this.removeTmp(t)); + } + /** + * Get token legend for semantic tokens. + */ + async getSemanticTokenLegend() { + return (await this.getCompiler()).getSemanticTokenLegend(); + } + /** + * Get semantic tokens for the document. + * See {@link SweetCompileOptions}. + * See {@link TypstCompiler#getSemanticTokens}. + */ + async getSemanticTokens(e) { + const t = await this.getCompileOptions(e); + return (await this.getCompiler()).getSemanticTokens({ + mainFilePath: t.mainFilePath, + resultId: e.resultId + }).finally(() => this.removeTmp(t)); + } + async getCompileOptions(e) { + if (e === void 0) + return { mainFilePath: this.mainFilePath, diagnostics: "none" }; + if (typeof e == "string") + throw new Error("please specify opts as {mainContent: '...'} or {mainFilePath: '...'}"); + if ("mainFilePath" in e) + return { ...e, diagnostics: "none" }; + { + const t = `/tmp/${Sr()}.typ`; + return await this.addSource(t, e.mainContent), { mainFilePath: t, inputs: e.inputs, diagnostics: "none" }; + } + } + async getVector(e) { + if (e && "vectorData" in e) + return e.vectorData; + const t = await this.getCompileOptions(e); + return (await this.getCompiler()).compile(t).then((n) => n.result).finally(() => this.removeTmp(t)); + } + async transientRender(e, t) { + const n = await this.getRenderer(); + if (!n) + throw new Error("does not provide renderer instance"); + const i = await this.getVector(e); + return await n.runWithSession(async (s) => (n.manipulateData({ + renderSession: s, + action: "reset", + data: i + }), t(n, s))); + } + async prepareUse() { + if (!this.providers) + return; + const e = await Promise.all( + this.providers.map((a) => typeof a == "function" ? a() : a) + ); + if (this.providers = [], we == this && !e.some((a) => a.key.includes("package-registry") || a.key.includes("access-model"))) + if (Cr) { + const a = (m) => import(m); + try { + const d = new qe(), { default: _ } = await a("sync-request"); + we.use( + U.withAccessModel(d), + U.fetchPackageBy(d, (f, w) => { + const P = _("GET", w); + if (P.statusCode === 200) + return P.getBody(void 0); + }) + ); + } catch { + } + } else + we.use(U.fetchPackageRegistry()); + const t = await Promise.all( + this.providers.map((a) => typeof a == "function" ? a() : a) + ), n = this.ccOptions || (this.ccOptions = {}), i = n.beforeBuild || (n.beforeBuild = []), s = this.exOptions || (this.exOptions = {}), o = s.beforeBuild || (s.beforeBuild = []); + for (const a of [...e, ...t]) + a.forRoles.includes("compiler") && (this.requireIsUninitialized("compiler", this.cc), i.push(...a.provides)), a.forRoles.includes("renderer") && (this.requireIsUninitialized("renderer", this.ex), o.push(...a.provides)); + this.providers = void 0; + } + requireIsUninitialized(e, t, n) { + if (t && typeof t != "function") + throw new Error(`${e} has been initialized: ${t}`); + } + /** @internal */ + static async buildLocalCompiler() { + const { createTypstCompiler: e } = await Promise.resolve().then(() => at); + await this.prepareUse(); + const t = e(); + return await t.init(this.ccOptions), t; + } + /** @internal */ + static async buildGlobalCompiler() { + const { createGlobalCompiler: e } = await Promise.resolve().then(() => rn), { createTypstCompiler: t } = await Promise.resolve().then(() => at); + return await this.prepareUse(), e(t, this.ccOptions); + } + /** @internal */ + static async buildLocalRenderer() { + const { createTypstRenderer: e } = await Promise.resolve().then(() => ut); + await this.prepareUse(); + const t = e(); + return await t.init(this.exOptions), t; + } + /** @internal */ + static async buildGlobalRenderer() { + const { createGlobalRenderer: e } = await Promise.resolve().then(() => fn), { createTypstRenderer: t } = await Promise.resolve().then(() => ut); + return await this.prepareUse(), e(t, this.exOptions); + } +} +const we = new U({ + compiler: U.buildGlobalCompiler, + renderer: U.buildGlobalRenderer +}), R = Symbol.for("reflexo-obj"); +var Ge = /* @__PURE__ */ ((r) => (r[r.PIXEL_PER_PT = 3] = "PIXEL_PER_PT", r))(Ge || {}); +let Ot = class { + constructor(e, t, n) { + l(this, "loadPageCount"); + l(this, "imageScaleFactor"); + l(this, "container"); + l(this, "canvasList"); + l(this, "textLayerList"); + l(this, "commonList"); + l(this, "textLayerParentList"); + l(this, "semanticLayerList"); + this.pageInfos = e, this.imageScaleFactor = n.pixelPerPt ?? Ge.PIXEL_PER_PT, t.innerHTML = "", t.style.width = "100%", this.container = t, this.canvasList = new Array(this.loadPageCount), this.textLayerList = new Array(this.loadPageCount), this.commonList = new Array(this.loadPageCount), this.textLayerParentList = new Array(this.loadPageCount), this.semanticLayerList = new Array(this.loadPageCount); + const i = (s, o, a) => { + const d = Math.ceil(o.width) * this.imageScaleFactor, _ = Math.ceil(o.height) * this.imageScaleFactor, f = this.canvasList[s] = document.createElement("canvas"), w = this.semanticLayerList[s] = document.createElement("div"), P = this.textLayerList[s] = document.createElement("div"), C = this.textLayerParentList[s] = document.createElement("div"); + if (f.getContext("2d")) { + const A = document.createElement("div"); + f.width = d, f.height = _, A.appendChild(f), a.appendChild(A), A.style.position = "absolute"; + } + { + C.appendChild(P), C.className = "typst-html-semantics"; + const A = t.offsetWidth, $ = A / o.width; + C.style.width = `${A}px`, C.style.height = `${o.height * $}px`, C.style.setProperty("--data-text-width", `${$}px`), C.style.setProperty("--data-text-height", `${$}px`), a.classList.add("typst-page"), a.classList.add("canvas"), a.style.width = `${A}px`, a.style.height = `${_ * $}px`, a.style.position = "relative", w.appendChild(C), a.appendChild(w); + } + }; + for (let s = 0; s < this.pageInfos.length; s++) { + const o = this.pageInfos[s]; + let a; + a = this.commonList[s] = document.createElement("div"), t.appendChild(a), i(s, o, a); + } + } + resetLayout() { + for (let e = 0; e < this.pageInfos.length; e++) { + const t = this.pageInfos[e], n = Math.ceil(t.width) * this.imageScaleFactor, i = Math.ceil(t.height) * this.imageScaleFactor, s = this.canvasList[e].parentElement; + if (!s) + throw new Error( + `canvasDiv is null for page ${e}, canvas list length ${this.canvasList.length}` + ); + const o = this.commonList[e], a = this.textLayerParentList[e], d = this.container.offsetWidth, _ = d / n; + a.style.width = `${d}px`, a.style.height = `${i * _}px`, o.style.width = `${d}px`, o.style.height = `${i * _}px`; + const f = this.container.offsetWidth / n; + s.style.transformOrigin = "0px 0px", s.style.transform = `scale(${f})`; + } + } +}; +const xr = (r) => { + let e = !1, t; + return () => e ? t : (e = !0, t = r()); +}; +let Ft = class { + constructor(e) { + l(this, "wasmBin"); + l(this, "initOnce"); + if (typeof e != "function") + throw new Error("initFn is not a function"); + this.initOnce = xr(async () => { + await e(this.wasmBin); + }); + } + async init(e) { + this.wasmBin = e, await this.initOnce(); + } +}; +const kr = (r, e) => e.some((t) => r instanceof t); +let et, tt; +function Rr() { + return et || (et = [ + IDBDatabase, + IDBObjectStore, + IDBIndex, + IDBCursor, + IDBTransaction + ]); +} +function Er() { + return tt || (tt = [ + IDBCursor.prototype.advance, + IDBCursor.prototype.continue, + IDBCursor.prototype.continuePrimaryKey + ]); +} +const Dt = /* @__PURE__ */ new WeakMap(), Ue = /* @__PURE__ */ new WeakMap(), $t = /* @__PURE__ */ new WeakMap(), Le = /* @__PURE__ */ new WeakMap(), Qe = /* @__PURE__ */ new WeakMap(); +function Pr(r) { + const e = new Promise((t, n) => { + const i = () => { + r.removeEventListener("success", s), r.removeEventListener("error", o); + }, s = () => { + t(ae(r.result)), i(); + }, o = () => { + n(r.error), i(); + }; + r.addEventListener("success", s), r.addEventListener("error", o); + }); + return e.then((t) => { + t instanceof IDBCursor && Dt.set(t, r); + }).catch(() => { + }), Qe.set(e, r), e; +} +function Ir(r) { + if (Ue.has(r)) + return; + const e = new Promise((t, n) => { + const i = () => { + r.removeEventListener("complete", s), r.removeEventListener("error", o), r.removeEventListener("abort", o); + }, s = () => { + t(), i(); + }, o = () => { + n(r.error || new DOMException("AbortError", "AbortError")), i(); + }; + r.addEventListener("complete", s), r.addEventListener("error", o), r.addEventListener("abort", o); + }); + Ue.set(r, e); +} +let He = { + get(r, e, t) { + if (r instanceof IDBTransaction) { + if (e === "done") + return Ue.get(r); + if (e === "objectStoreNames") + return r.objectStoreNames || $t.get(r); + if (e === "store") + return t.objectStoreNames[1] ? void 0 : t.objectStore(t.objectStoreNames[0]); + } + return ae(r[e]); + }, + set(r, e, t) { + return r[e] = t, !0; + }, + has(r, e) { + return r instanceof IDBTransaction && (e === "done" || e === "store") ? !0 : e in r; + } +}; +function Mr(r) { + He = r(He); +} +function Lr(r) { + return r === IDBDatabase.prototype.transaction && !("objectStoreNames" in IDBTransaction.prototype) ? function(e, ...t) { + const n = r.call(Ae(this), e, ...t); + return $t.set(n, e.sort ? e.sort() : [e]), ae(n); + } : Er().includes(r) ? function(...e) { + return r.apply(Ae(this), e), ae(Dt.get(this)); + } : function(...e) { + return ae(r.apply(Ae(this), e)); + }; +} +function Ar(r) { + return typeof r == "function" ? Lr(r) : (r instanceof IDBTransaction && Ir(r), kr(r, Rr()) ? new Proxy(r, He) : r); +} +function ae(r) { + if (r instanceof IDBRequest) + return Pr(r); + if (Le.has(r)) + return Le.get(r); + const e = Ar(r); + return e !== r && (Le.set(r, e), Qe.set(e, r)), e; +} +const Ae = (r) => Qe.get(r), Tr = ["get", "getKey", "getAll", "getAllKeys", "count"], Or = ["put", "add", "delete", "clear"], Te = /* @__PURE__ */ new Map(); +function rt(r, e) { + if (!(r instanceof IDBDatabase && !(e in r) && typeof e == "string")) + return; + if (Te.get(e)) + return Te.get(e); + const t = e.replace(/FromIndex$/, ""), n = e !== t, i = Or.includes(t); + if ( + // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. + !(t in (n ? IDBIndex : IDBObjectStore).prototype) || !(i || Tr.includes(t)) + ) + return; + const s = async function(o, ...a) { + const d = this.transaction(o, i ? "readwrite" : "readonly"); + let _ = d.store; + return n && (_ = _.index(a.shift())), (await Promise.all([ + _[t](...a), + i && d.done + ]))[0]; + }; + return Te.set(e, s), s; +} +Mr((r) => ({ + ...r, + get: (e, t, n) => rt(e, t) || r.get(e, t, n), + has: (e, t) => !!rt(e, t) || r.has(e, t) +})); +let Fr = class { + constructor() { + l(this, "loadedFonts", /* @__PURE__ */ new Set()); + l(this, "fetcher", fetch); + } + setFetcher(e) { + this.fetcher = e; + } + async loadFonts(e, t) { + const n = (m) => import(m), i = this.fetcher || (this.fetcher = await async function() { + const { fetchBuilder: a, FileSystemCache: d } = await n("node-fetch-cache"), _ = new d({ + /// By default, we don't have a complicated cache policy. + cacheDirectory: ".cache/typst/fonts" + }), f = a.withCache(_); + return function(w, P) { + const C = setTimeout(() => { + console.warn("font fetching is stucking:", w); + }, 15e3); + return f(w, P).finally(() => { + clearTimeout(C); + }); + }; + }()), s = t.filter((a) => a instanceof Uint8Array ? !0 : this.loadedFonts.has(a) ? !1 : (this.loadedFonts.add(a), !0)), o = await Promise.all( + s.map(async (a) => { + if (a instanceof Uint8Array) { + await e.add_raw_font(a); + return; + } + return new Uint8Array(await (await i(a)).arrayBuffer()); + }) + ); + for (const a of o) + a && await e.add_raw_font(a); + } + async build(e, t, n) { + const i = { ref: this, builder: t, hooks: n }; + for (const o of (e == null ? void 0 : e.beforeBuild) ?? []) + await o(void 0, i); + return n.latelyBuild && n.latelyBuild(i), await t.build(); + } +}; +async function jt(r, e, t, n) { + var i; + return await e.init((i = r == null ? void 0 : r.getModule) == null ? void 0 : i.call(r)), await new Fr().build(r, new t(), n); +} +let Dr = class Bt { + constructor(e) { + l(this, "hookedElem"); + l(this, "kModule"); + l(this, "opts"); + l(this, "modes", []); + /// Configuration fields + /// enable partial rendering + l(this, "partialRendering", !0); + /// underlying renderer + l(this, "renderMode", "svg"); + l(this, "r"); + /// preview mode + l(this, "previewMode", 0); + /// whether this is a content preview + l(this, "isContentPreview", !1); + /// whether this content preview will mix outline titles + l(this, "isMixinOutline", !1); + /// background color + l(this, "backgroundColor", "black"); + /// default page color (empty string means transparent) + l(this, "pageColor", "white"); + /// pixel per pt + l(this, "pixelPerPt", 3); + /// customized way to retrieving dom state + l(this, "retrieveDOMState"); + /// State fields + /// whether svg is updating (in triggerSvgUpdate) + l(this, "isRendering", !1); + /// whether kModule is initialized + l(this, "moduleInitialized", !1); + /// patch queue for updating data. + l(this, "patchQueue", []); + /// resources to dispose + l(this, "disposeList", []); + /// canvas render ctoken + l(this, "canvasRenderCToken"); + /// There are two scales in this class: The real scale is to adjust the size + /// of `hookedElem` to fit the svg. The virtual scale (scale ratio) is to let + /// user zoom in/out the svg. For example: + /// + the default value of virtual scale is 1, which means the svg is totally + /// fit in `hookedElem`. + /// + if user set virtual scale to 0.5, then the svg will be zoomed out to fit + /// in half width of `hookedElem`. "real" current scale of `hookedElem` + l(this, "currentRealScale", 1); + /// "virtual" current scale of `hookedElem` + l(this, "currentScaleRatio", 1); + /// timeout for delayed viewport change + l(this, "vpTimeout"); + /// sampled by last render time. + l(this, "sampledRenderTime", 0); + /// page to partial render + l(this, "partialRenderPage", 0); + /// outline data + l(this, "outline"); + /// cursor position in form of [page, x, y] + l(this, "cursorPosition"); + // id: number = rnd++; + /// Cache fields + /// cached state of container, default to retrieve state from `this.hookedElem` + l(this, "cachedDOMState", { + width: 0, + height: 0, + window: { + innerWidth: 0, + innerHeight: 0 + }, + boundingRect: { + left: 0, + top: 0, + right: 0 + } + }); + var t, n; + this.hookedElem = e.hookedElem, this.kModule = e.kModule, this.opts = e || {}; + { + const { renderMode: i, previewMode: s, isContentPreview: o, retrieveDOMState: a } = e || {}; + this.partialRendering = !1, this.renderMode = i ?? this.renderMode, this.previewMode = s ?? this.previewMode, this.isContentPreview = o || !1, this.retrieveDOMState = a ?? (() => ({ + width: this.hookedElem.offsetWidth, + height: this.hookedElem.offsetHeight, + window: { + innerWidth: window.innerWidth, + innerHeight: window.innerHeight + }, + boundingRect: this.hookedElem.getBoundingClientRect() + })), this.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue( + "--typst-preview-background-color" + ); + } + this.hookedElem.classList.add("hide-scrollbar-x"), (t = this.hookedElem.parentElement) == null || t.classList.add("hide-scrollbar-x"), this.previewMode === 1 && (this.hookedElem.classList.add("hide-scrollbar-y"), (n = this.hookedElem.parentElement) == null || n.classList.add("hide-scrollbar-y")), this.installCtrlWheelHandler(); + } + reset() { + this.kModule.reset(), this.moduleInitialized = !1; + } + dispose() { + const e = this.disposeList; + this.disposeList = [], e.forEach((t) => t()); + } + static derive(e, t) { + return ["rescale", "rerender", "postRender"].reduce((n, i) => (n[i] = e[`${i}$${t}`].bind(e), console.assert(n[i] !== void 0, `${i}$${t} is undefined`), n), {}); + } + registerMode(e) { + const t = Bt.derive(this, e); + this.modes.push([e, t]), e === this.renderMode && (this.r = t); + } + installCtrlWheelHandler() { + const e = [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1, + 1.1, + 1.3, + 1.5, + 1.7, + 1.9, + 2.1, + 2.4, + 2.7, + 3, + 3.3, + 3.7, + 4.1, + 4.6, + 5.1, + 5.7, + 6.3, + 7, + 7.7, + 8.5, + 9.4, + 10 + ], t = (n) => { + var i, s, o, a; + if (n.ctrlKey) { + n.preventDefault(), this.cachedDOMState = this.retrieveDOMState(), window.onresize !== null && (window.onresize = null); + const d = this.currentScaleRatio; + if (n.deltaY < 0) { + if (this.currentScaleRatio >= e.at(-1)) + return; + this.currentScaleRatio = e.filter((C) => C > this.currentScaleRatio).at(0); + } else if (n.deltaY > 0) { + if (this.currentScaleRatio <= e.at(0)) + return; + this.currentScaleRatio = e.filter((C) => C < this.currentScaleRatio).at(-1); + } else + return; + const _ = this.currentScaleRatio / d, f = n.pageX * (_ - 1), w = n.pageY * (_ - 1); + Math.abs(this.currentScaleRatio - 1) < 1e-5 ? (this.hookedElem.classList.add("hide-scrollbar-x"), (i = this.hookedElem.parentElement) == null || i.classList.add("hide-scrollbar-x"), this.previewMode === 1 && (this.hookedElem.classList.add("hide-scrollbar-y"), (s = this.hookedElem.parentElement) == null || s.classList.add("hide-scrollbar-y"))) : (this.hookedElem.classList.remove("hide-scrollbar-x"), (o = this.hookedElem.parentElement) == null || o.classList.remove("hide-scrollbar-x"), this.previewMode === 1 && (this.hookedElem.classList.remove("hide-scrollbar-y"), (a = this.hookedElem.parentElement) == null || a.classList.remove("hide-scrollbar-y"))); + const P = this.hookedElem.firstElementChild; + if (P) { + const C = this.getSvgScaleRatio(), F = Number.parseFloat(P.getAttribute("data-height")), A = Math.ceil(F * C); + this.hookedElem.style.height = `${A * 2}px`; + } + return window.scrollBy(f, w), this.addViewportChange(), !1; + } + }; + this.renderMode !== "dom" && (typeof acquireVsCodeApi < "u" ? (window.addEventListener("wheel", t, { + passive: !1 + }), this.disposeList.push(() => { + window.removeEventListener("wheel", t); + })) : (document.body.addEventListener("wheel", t, { + passive: !1 + }), this.disposeList.push(() => { + document.body.removeEventListener("wheel", t); + }))); + } + /// Get current scale from html to svg + // Note: one should retrieve dom state before rescale + getSvgScaleRatio() { + const e = this.hookedElem.firstElementChild; + if (!e) + return 0; + const t = this.cachedDOMState, n = Number.parseFloat( + e.getAttribute("data-width") || e.getAttribute("width") || "1" + ), i = Number.parseFloat( + e.getAttribute("data-height") || e.getAttribute("height") || "1" + ); + return this.currentRealScale = this.previewMode === 1 ? Math.min(t.width / n, t.height / i) : t.width / n, this.currentRealScale * this.currentScaleRatio; + } + processQueue(e) { + const t = e[0]; + switch (t) { + case "new": + case "diff-v1": + return t === "new" && this.reset(), this.kModule.manipulateData({ + action: "merge", + data: e[1] + }), this.moduleInitialized = !0, !0; + case "viewport-change": + return this.moduleInitialized ? !0 : (console.log("viewport-change before initialization"), !1); + default: + return console.log("svgUpdateEvent", e), !1; + } + } + triggerUpdate() { + if (this.isRendering) + return; + this.isRendering = !0; + const e = async () => { + if (this.cachedDOMState = this.retrieveDOMState(), this.patchQueue.length === 0) { + this.isRendering = !1, this.postprocessChanges(); + return; + } + try { + let t = performance.now(); + const n = this.canvasRenderCToken; + n && (await n.cancel(), await n.wait(), this.canvasRenderCToken = void 0, console.log("cancel canvas rendering")); + let i = !1; + for (; this.patchQueue.length > 0; ) + i = this.processQueue(this.patchQueue.shift()) || i; + let s = performance.now(); + i && (this.r.rescale(), await this.r.rerender(), this.r.rescale()); + let o = performance.now(); + const a = (d, _, f) => `${d} ${(f - _).toFixed(2)} ms`; + this.sampledRenderTime = o - t, console.log([a("parse", t, s), a("rerender", s, o), a("total", t, o)].join(", ")), requestAnimationFrame(e); + } catch (t) { + console.error(t), this.isRendering = !1, this.postprocessChanges(); + } + }; + requestAnimationFrame(e); + } + postprocessChanges() { + this.r.postRender(), this.previewMode === 1 && document.querySelectorAll(".typst-page-number-indicator").forEach((e) => { + e.textContent = `${this.kModule.retrievePagesInfo().length}`; + }); + } + addChangement(e) { + e[0] === "new" && this.patchQueue.splice(0, this.patchQueue.length); + const t = () => { + this.vpTimeout = void 0, this.patchQueue.push(e), this.triggerUpdate(); + }; + this.vpTimeout !== void 0 && clearTimeout(this.vpTimeout), e[0] === "viewport-change" && this.isRendering ? this.vpTimeout = setTimeout(t, this.sampledRenderTime || 100) : t(); + } + addViewportChange() { + this.addChangement(["viewport-change", ""]); + } +}; +function $r(r) { + return class { + constructor(t) { + l(this, "impl"); + l(this, "kModule"); + if (t.isContentPreview && (t.renderMode = "canvas"), this.kModule = t.kModule, this.impl = new r(t), !this.impl.r) + throw new Error(`mode is not supported, ${t == null ? void 0 : t.renderMode}`); + t.isContentPreview && (this.impl.partialRendering = !0, this.impl.pixelPerPt = 1, this.impl.isMixinOutline = !0); + } + dispose() { + this.impl.dispose(); + } + reset() { + this.impl.reset(); + } + addChangement(t) { + this.impl.addChangement(t); + } + addViewportChange() { + this.impl.addViewportChange(); + } + setPageColor(t) { + this.impl.pageColor = t, this.addViewportChange(); + } + setPartialRendering(t) { + this.impl.partialRendering = t; + } + setCursor(t, n, i) { + this.impl.cursorPosition = [t, n, i]; + } + setPartialPageNumber(t) { + return t <= 0 || t > this.kModule.retrievePagesInfo().length ? !1 : (this.impl.partialRenderPage = t - 1, this.addViewportChange(), !0); + } + getPartialPageNumber() { + return this.impl.partialRenderPage + 1; + } + setOutineData(t) { + this.impl.outline = t, this.addViewportChange(); + } + }; +} +function jr(r, ...e) { + return e.reduce((t, n) => n(t), r); +} +let Br = class { + constructor() { + l(this, "isCancellationRequested", !1); + l(this, "_onCancelled"); + l(this, "_onCancelledResolveResolved"); + let e, t; + this._onCancelled = new Promise((n) => { + e = n, t && t(n); + }), this._onCancelledResolveResolved = new Promise((n) => { + t = n, e && n(e); + }); + } + async cancel() { + await this._onCancelledResolveResolved, this.isCancellationRequested = !0; + } + isCancelRequested() { + return this.isCancellationRequested; + } + async consume() { + (await this._onCancelledResolveResolved)(); + } + wait() { + return this._onCancelled; + } +}; +const Wr = () => new Promise((r) => requestAnimationFrame(r)); +function zr(r) { + return class extends r { + constructor(...n) { + super(...n); + /// The template element for creating DOM by string. + l(this, "tmpl", document.createElement("template")); + /// The stub element for replacing an invisible element. + l(this, "stub", this.createElement("")); + /// Typescript side of lib. + l(this, "plugin"); + /// Rust side of kernel. + l(this, "docKernel"); + /// The element to track. + l(this, "resourceHeader"); + /// Expected exact state of the current DOM. + /// Initially it is empty meaning no any page is rendered. + l(this, "pages", []); + /// The virtual scale of the document. + l(this, "domScale", 1); + /// Track mode. + l(this, "track_mode", 0); + /// Current executing task. + l(this, "current_task"); + /// The currently maintained viewport. + l(this, "viewport"); + if (this.registerMode("dom"), this.disposeList.push(() => { + this.dispose(); + }), this.plugin = this.opts.renderer, this.opts.domScale !== void 0) { + if (this.opts.domScale <= 0) + throw new Error("domScale must be positive"); + this.domScale = this.opts.domScale; + } + } + dispose() { + for (const n of this.pages) + n.dispose(); + this.docKernel && this.docKernel.free(); + } + createElement(n) { + return this.tmpl.innerHTML = n, this.tmpl.content.firstElementChild; + } + async mountDom(n) { + if (console.log("mountDom", n), this.docKernel) + throw new Error("already mounted"); + this.hookedElem.innerHTML = '', this.resourceHeader = this.hookedElem.querySelector(".typst-svg-resources"), this.docKernel = await this.plugin.renderer.mount_dom(this.kModule[R], this.hookedElem), this.docKernel.bind_functions({ + populateGlyphs: (i) => { + let s = this.createElement(i); + console.log("populateGlyphs", s); + let o = s.firstElementChild; + this.resourceHeader.append(o); + } + }); + } + async cancelAnyway$dom() { + if (console.log("cancelAnyway$dom"), this.current_task) { + const n = this.current_task; + this.current_task = void 0, await n.cancel(); + } + } + retrieveDOMPages() { + return Array.from(this.hookedElem.querySelectorAll(".typst-dom-page")); + } + // doesn't need to postRender + postRender$dom() { + } + // doesn't need to rescale + rescale$dom() { + } + getDomViewport(n, i) { + const s = i.left, o = -i.top, a = i.right, d = n.innerHeight - i.top, _ = { + x: 0, + y: o / this.domScale, + width: Math.max(a - s, 0) / this.domScale, + height: Math.max(d - o, 0) / this.domScale + }; + return (_.width <= 0 || _.height <= 0) && (_.x = _.y = _.width = _.height = 0), _; + } + // fast mode + async rerender$dom() { + const n = this.retrieveDOMState(), { x: i, y: s, width: o, height: a } = this.getDomViewport(n.window, n.boundingRect); + if (!await this.docKernel.relayout(i, s, o, a)) + return; + const _ = new Br(); + this.doRender$dom(_), this.current_task = _; + } + async doRender$dom(n) { + const i = (d, _) => { + if (d && !n.isCancelRequested() && _) + return _(); + }, s = this.retrieveDOMPages().map((d) => { + const { innerWidth: _, innerHeight: f } = window, w = d.getBoundingClientRect(); + return { + inWindow: !(w.left > _ || w.right < 0 || w.top > f || w.bottom < 0), + page: d + }; + }), o = async (d) => { + if (await Wr(), n.isCancelRequested()) { + console.log("cancel stage", 0, d); + return; + } + const _ = s[d].page, f = _.getBoundingClientRect(), w = this.getDomViewport(window, f), P = (D) => this.docKernel.need_repaint(d, w.x, w.y, w.width, w.height, D), C = (D) => this.docKernel.repaint(d, w.x, w.y, w.width, w.height, D), F = (D) => { + if (!n.isCancelRequested()) + return i(P(D), () => C(D)); + }; + await F( + 0 + /* Layout */ + ); + const A = (f.width ? Number.parseFloat(_.getAttribute("data-width")) / f.width : 1) * this.domScale, $ = (f.height ? Number.parseFloat(_.getAttribute("data-height")) / f.height : 1) * this.domScale; + if (w.x *= A, w.y *= $, w.y -= 100, w.width *= A, w.height *= $, w.height += 200, await F( + 1 + /* Svg */ + ), await F( + 2 + /* Semantics */ + ), n.isCancelRequested()) { + console.log("cancel stage", 2, d); + return; + } + P( + 3 + /* PrepareCanvas */ + ) ? (async () => { + if (await C( + 3 + /* PrepareCanvas */ + ), !n.isCancelRequested()) + return F( + 4 + /* Canvas */ + ); + })() : await F( + 4 + /* Canvas */ + ); + }, a = async (d) => { + for (let _ = 0; _ < s.length; ++_) { + if (n.isCancelRequested()) { + console.log("cancel page", 0, _); + return; + } + s[_].inWindow === d && await o(_); + } + }; + this.cancelAnyway$dom(), await a(!0), await a(!1), !n.isCancelRequested() && console.log( + "finished", + 0 + /* Layout */ + ); + } + }; +} +let qr = class extends $r( + jr( + Dr, + zr + ) +) { +}; +var Pt; +let Oe = (Pt = R, class { + /** + * @internal + */ + constructor(e, t) { + /** + * @internal + */ + l(this, Pt); + this.plugin = e, this[R] = t; + } + /** + * @deprecated set in {@link RenderToCanvasOptions} instead + * + * Set the background color of the Typst document. + * @param {string} t - The background color in format of `^#?[0-9a-f]{6}$` + * + * Note: Default to `#ffffff`. + * + * Note: Only available in canvas rendering mode. + */ + set backgroundColor(e) { + e !== void 0 && (this[R].background_color = e); + } + /** + * Get the background color of the Typst document. + * + * Note: Default to `#ffffff`. + * + * Note: Only available in canvas rendering mode. + */ + get backgroundColor() { + return this[R].background_color; + } + /** + * Set the pixel per point scale up the canvas panel. + * + * Note: Default to `3`. + * + * Note: Only available in canvas rendering mode. + */ + set pixelPerPt(e) { + e !== void 0 && (this[R].pixel_per_pt = e); + } + /** + * @deprecated set in {@link RenderToCanvasOptions} instead + * + * Get the pixel per point scale up the canvas panel. + * + * Note: Default to `3`. + * + * Note: Only available in canvas rendering mode. + */ + get pixelPerPt() { + return this[R].pixel_per_pt; + } + /** + * Reset state + */ + reset() { + this.plugin.resetSession(this); + } + /** + * @deprecated + * use {@link docWidth} instead + */ + get doc_width() { + return this[R].doc_width; + } + get docWidth() { + return this[R].doc_width; + } + /** + * @deprecated + * use {@link docHeight} instead + */ + get doc_height() { + return this[R].doc_height; + } + get docHeight() { + return this[R].doc_height; + } + retrievePagesInfo() { + const e = this[R].pages_info, t = [], n = e.page_count; + for (let i = 0; i < n; i++) { + const s = e.page(i); + t.push({ + pageOffset: s.page_off, + width: s.width_pt, + height: s.height_pt + }); + } + return t; + } + getSourceLoc(e) { + return this[R].source_span(e); + } + /** + * See {@link TypstRenderer#renderSvg} for more details. + */ + renderSvg(e) { + return this.plugin.renderSvg({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderToSvg} for more details. + */ + renderToSvg(e) { + return this.plugin.renderToSvg({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderCanvas} for more details. + */ + renderCanvas(e) { + return this.plugin.renderCanvas({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#manipulateData} for more details. + */ + manipulateData(e) { + this.plugin.manipulateData({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderSvgDiff} for more details. + */ + renderSvgDiff(e) { + return this.plugin.renderSvgDiff({ + renderSession: this, + ...e + }); + } + /** + * @deprecated + * use {@link getSourceLoc} instead + */ + get_source_loc(e) { + return this[R].source_span(e); + } + /** + * @deprecated + * use {@link renderSvgDiff} instead + */ + render_in_window(e, t, n, i) { + return this[R].render_in_window(e, t, n, i); + } + /** + * @deprecated + * use {@link manipulateData} instead + */ + merge_delta(e) { + this.plugin.manipulateData({ + renderSession: this, + action: "merge", + data: e + }); + } +}); +var It; +let Nr = (It = R, class { + /** + * @internal + */ + constructor(e, t) { + /** + * @internal + */ + l(this, It); + /** + * @internal + */ + l(this, "managedCanvasElemList", /* @__PURE__ */ new Map()); + /** + * @internal + */ + l(this, "canvasCounter", Math.random()); + this.plugin = e, this[R] = t; + } + /** + * See {@link TypstRenderer#manipulateData} for more details. + */ + manipulateData(e, t) { + return this[R].manipulate_data(e, t); + } + /** + * You must submit all canvas in pages to ensure synchronization with the background worker + * + * See {@link TypstRenderer#renderCanvas} for more details. + */ + renderCanvas(e) { + const t = this.managedCanvasElemList; + for (const [a, d] of t) + d[0] = 0; + for (const a of e) { + const d = a.canvas; + let _ = d.dataset.manageId, f = 2; + _ || (_ = this.canvasCounter.toFixed(5), this.canvasCounter += 1, d.dataset.manageId = _, f = 1); + let w = t.get(_); + if (w && w[0] !== 0) + throw new Error("cannot update a canvas for two times in batch"); + t.set(_, [f, { ...a }]); + } + const n = Array.from(t.entries()), i = new Uint8Array(n.length), s = new Array(n.length), o = n.map(([a, [d, _]], f) => (d || t.delete(a), i[f] = d, s[f] = _.canvas, this.plugin.canvasOptionsToRust(_))); + return this[R].render_canvas(i, s, o); + } + async retrievePagesInfo() { + const e = await this[R].get_pages_info(); + console.log(e); + const t = [], n = e.page_count; + for (let i = 0; i < n; i++) { + const s = e.page(i); + t.push({ + pageOffset: s.page_off, + width: s.width_pt, + height: s.height_pt + }); + } + return t; + } +}); +const Ur = (r) => new Ft(async (e) => await r.default(e)); +function nt() { + return new Wt(); +} +function ni() { + return new Wt(); +} +async function ii() { + return (await Promise.resolve().then(() => Ye)).renderer_build_info(); +} +let it = !0, Wt = class { + constructor() { + l(this, "renderer"); + l(this, "rendererJs"); + } + async init(e) { + var n; + this.rendererJs = await (((n = e == null ? void 0 : e.getWrapper) == null ? void 0 : n.call(e)) || Promise.resolve().then(() => Ye)); + const t = this.rendererJs.TypstRendererBuilder; + this.renderer = await jt( + e, + Ur(this.rendererJs), + t, + {} + ); + } + loadGlyphPack(e) { + return Promise.resolve(); + } + createOptionsToRust(e) { + const t = new this.rendererJs.CreateSessionOptions(); + return e.format !== void 0 && (t.format = e.format), e.artifactContent !== void 0 && (t.artifact_content = e.artifactContent), t; + } + canvasOptionsToRust(e) { + const t = new this.rendererJs.RenderPageImageOptions(); + if (e.pageOffset === void 0) + throw new Error("pageOffset is required in reflexo v0.5.0"); + if (t.page_off = e.pageOffset, e.cacheKey !== void 0 && (t.cache_key = e.cacheKey), e.backgroundColor !== void 0 && (t.background_color = e.backgroundColor), e.pixelPerPt !== void 0 && (t.pixel_per_pt = e.pixelPerPt), e.dataSelection !== void 0) { + let n = 0; + e.dataSelection.body ? n |= 1 : e.canvas && it && (it = !1, console.warn("dataSelection.body is not set but providing canvas for body")), (e.dataSelection.text || e.dataSelection.annotation) && console.error("dataSelection.text and dataSelection.annotation are deprecated"), e.dataSelection.semantics && (n |= 8), t.data_selection = n; + } + return t; + } + retrievePagesInfoFromSession(e) { + return e.retrievePagesInfo(); + } + /** + * Render a Typst document to canvas. + */ + renderCanvas(e) { + return this.withinOptionSession(e, async (t) => this.renderer.render_page_to_canvas( + t[R], + e.canvas || void 0, + this.canvasOptionsToRust(e) + )); + } + // async renderPdf(artifactContent: string): Promise { + // return this.renderer.render_to_pdf(artifactContent); + // } + async inAnimationFrame(e) { + return new Promise((t, n) => { + requestAnimationFrame(() => { + try { + t(e()); + } catch (i) { + n(i); + } + }); + }); + } + async renderDisplayLayer(e, t, n) { + const s = e[R].pages_info.page_count, o = async (f, w) => { + const C = t[f].getContext("2d"); + if (!C) + throw new Error("canvas context is null"); + return await this.renderCanvas({ + ...n, + canvas: C, + renderSession: e, + pageOffset: w + }); + }, a = performance.now(), d = await (async () => { + const f = []; + for (let w = 0; w < s; w++) + f.push(await this.inAnimationFrame(() => o(w, w))); + return f; + })(), _ = performance.now(); + return console.log(`display layer used: render = ${(_ - a).toFixed(1)}ms`), d; + } + renderTextLayer(e, t) { + const n = performance.now(); + e.forEach((s, o) => { + s.innerHTML = t[o].htmlSemantics[0]; + }); + const i = performance.now(); + console.log(`text layer used: render = ${(i - n).toFixed(1)}ms`); + } + async render(e) { + if ("format" in e && e.format !== "vector" && ["serde_json", "js", "ir"].includes(e.format)) + throw new Error(`deprecated format ${e.format}, please use vector format`); + return this.renderToCanvas(e); + } + async renderDom(e) { + if ("format" in e && e.format !== "vector" && ["serde_json", "js", "ir"].includes(e.format)) + throw new Error(`deprecated format ${e.format}, please use vector format`); + return this.withinOptionSession(e, async (t) => { + const n = new qr({ + ...e, + renderMode: "dom", + hookedElem: e.container, + kModule: t, + renderer: this + }); + return await n.impl.mountDom(e.pixelPerPt), n; + }); + } + async renderToCanvas(e) { + let t, n; + const i = e.container; + i.style.visibility = "hidden"; + const s = async (o, a) => { + try { + n = await this.renderDisplayLayer(t, o, e), a(); + } finally { + i.style.visibility = "visible"; + } + }; + return this.withinOptionSession(e, async (o) => { + if (t = o, t[R].pages_info.page_count === 0) + throw new Error("No page found in session"); + if (e.pixelPerPt !== void 0 && e.pixelPerPt <= 0) + throw new Error( + "Invalid typst.RenderOptions.pixelPerPt, should be a positive number " + e.pixelPerPt + ); + let a = e.backgroundColor; + if (a !== void 0 && !/^#[0-9a-f]{6}$/.test(a)) + throw new Error( + "Invalid typst.backgroundColor color for matching ^#?[0-9a-f]{6}$ " + a + ); + t.pixelPerPt = e.pixelPerPt ?? Ge.PIXEL_PER_PT, t.backgroundColor = a ?? "#ffffff"; + const d = performance.now(), _ = new Ot( + this.retrievePagesInfoFromSession(t), + i, + e + ), f = performance.now(); + console.log(`layer used: retrieve = ${(f - d).toFixed(1)}ms`), await s(_.canvasList, () => _.resetLayout()), this.renderTextLayer(_.textLayerList, n); + }); + } + createModule(e) { + return Promise.resolve( + new Oe( + this, + this.renderer.create_session( + e && this.createOptionsToRust({ + format: "vector", + artifactContent: e + }) + ) + ) + ); + } + async createWorkerV0(e) { + return new Nr(this, await this.renderer.create_worker(e)); + } + workerBridge() { + return this.renderer.create_worker_bridge(); + } + renderSvg(e, t) { + if (e instanceof Oe || t) + throw new Error("removed api, please use renderToSvg({ renderSession, container }) instead"); + return this.withinOptionSession(e, async (n) => { + let i; + return e.data_selection && (i = 0, e.data_selection.body && (i |= 1), e.data_selection.defs && (i |= 2), e.data_selection.css && (i |= 4), e.data_selection.js && (i |= 8)), Promise.resolve(this.renderer.svg_data(n[R], i)); + }); + } + renderSvgDiff(e) { + return e.window ? this.renderer.render_svg_diff( + e.renderSession[R], + e.window.lo.x, + e.window.lo.y, + e.window.hi.x, + e.window.hi.y + ) : this.renderer.render_svg_diff( + e.renderSession[R], + 0, + 0, + 1e33, + 1e33 + ); + } + renderToSvg(e) { + return this.withinOptionSession(e, async (t) => Promise.resolve(this.renderer.render_svg(t[R], e.container))); + } + getCustomV1(e) { + return Promise.resolve(this.renderer.get_customs(e.renderSession[R])); + } + resetSession(e) { + return this.renderer.reset(e[R]); + } + manipulateData(e) { + return this.renderer.manipulate_data( + e.renderSession[R], + e.action ?? "reset", + e.data + ); + } + withinOptionSession(e, t) { + function n(i) { + return "artifactContent" in i; + } + if ("renderSession" in e) + return t(e.renderSession); + if (n(e)) + return this.runWithSession(e, t); + throw new Error( + "Invalid render options, should be one of RenderByContentOptions|RenderBySessionOptions" + ); + } + async runWithSession(e, t) { + let n = e, i = t; + t || (n = void 0, i = e); + const s = this.renderer.create_session( + /* moved */ + n && this.createOptionsToRust(n) + ); + try { + const o = await i(new Oe(this, s)); + return s.free(), o; + } catch (o) { + throw s.free(), o; + } + } +}; +var Mt; +let Hr = (Mt = R, class { + /** + * @internal + */ + constructor(e) { + /** + * @internal + */ + l(this, Mt); + this[R] = e; + } + /** + * Reset the incremental server to the initial state. + */ + reset() { + this[R].reset(); + } + /** + * Return current result. + */ + current() { + return this[R].current(); + } + /** + * Also attach the debug info to the result. + */ + setAttachDebugInfo(e) { + this[R].set_attach_debug_info(e); + } +}); +const Vr = new Ft(async (r) => await (await Promise.resolve().then(() => Me)).default(r)); +function zt() { + return new qt(); +} +var Y; +let qt = (Y = class { + constructor() { + l(this, "compiler"); + l(this, "compilerJs"); + } + async init(e) { + this.compilerJs = await Promise.resolve().then(() => Me); + const t = this.compilerJs.TypstCompilerBuilder, n = { ...e || {} }, i = n.beforeBuild ?? (n.beforeBuild = []), s = i.some( + (_) => _._preloadRemoteFontOptions !== void 0 + ), o = i.some( + (_) => { + var f; + return ((f = _._preloadRemoteFontOptions) == null ? void 0 : f.assets) !== void 0; + } + ), a = i.some( + (_) => { + var f; + return ((f = _._preloadRemoteFontOptions) == null ? void 0 : f.assets) === !1; + } + ); + if ((!s || !o && !a) && i.push(ne([], { assets: Y.defaultAssets })), !i.some((_) => _._kind === "fontLoader")) + throw new Error( + "TypstCompiler: no font loader found, please use font loaders, e.g. preloadRemoteFonts or preloadSystemFonts" + ); + this.compiler = await jt(e, Vr, t, {}); + } + compile(e) { + return new Promise((t) => { + if ("incrementalServer" in e) { + t( + this.compiler.incr_compile( + e.mainFilePath, + Fe(e.inputs), + e.incrementalServer[R], + st(e.diagnostics) + ) + ); + return; + } + t( + this.compiler.compile( + e.mainFilePath, + Fe(e.inputs), + e.format || "vector", + st(e.diagnostics) + ) + ); + }); + } + query(e) { + return new Promise((t) => { + t( + JSON.parse( + this.compiler.query( + e.mainFilePath, + Fe(e.inputs), + e.selector, + e.field + ) + ) + ); + }); + } + getSemanticTokenLegend() { + return new Promise((e) => { + e(this.compiler.get_semantic_token_legend()); + }); + } + getSemanticTokens(e) { + return new Promise((t) => { + this.compiler.reset(), t( + this.compiler.get_semantic_tokens( + e.offsetEncoding || "utf-16", + e.mainFilePath, + e.resultId + ) + ); + }); + } + async withIncrementalServer(e) { + const t = new Hr(this.compiler.create_incr_server()); + try { + return await e(t); + } finally { + t[R].free(); + } + } + async getAst(e) { + return this.compiler.get_ast(e); + } + async reset() { + await new Promise((e) => { + this.compiler.reset(), e(void 0); + }); + } + addSource(e, t) { + if (arguments.length > 2) + throw new Error( + "use of addSource(path, source, isMain) is deprecated, please use addSource(path, source) instead" + ); + this.compiler.add_source(e, t); + } + mapShadow(e, t) { + this.compiler.map_shadow(e, t); + } + unmapShadow(e) { + this.compiler.unmap_shadow(e); + } + resetShadow() { + this.compiler.reset_shadow(); + } + renderPageToCanvas() { + throw new Error("Please use the api TypstRenderer.renderToCanvas in v0.4.0"); + } +}, l(Y, "defaultAssets", ["text"]), Y); +zt._impl = qt; +function Fe(r) { + return r ? Object.entries(r) : void 0; +} +function st(r) { + switch (r) { + case "none": + return 1; + case "unix": + return 2; + case "full": + default: + return 3; + } +} +typeof window < "u" && (window.TypstRenderModule = { + RenderView: Ot, + createTypstRenderer: nt, + createTypstSvgRenderer: nt, + preloadRemoteFonts: ne, + preloadSystemFonts: Ze +}, window.TypstCompileModule = { + createTypstCompiler: zt, + preloadRemoteFonts: ne, + preloadSystemFonts: Ze, + FetchAccessModel: vr, + MemoryAccessModel: qe, + FetchPackageRegistry: Ne, + withAccessModel: ze, + withPackageRegistry: fe +}); +window.$typst = we; +window.TypstSnippet = U; +class Jr { + constructor() { + l(this, "loadedFonts", /* @__PURE__ */ new Set()); + l(this, "fetcher", fetch); + } + setFetcher(e) { + this.fetcher = e; + } + async loadFonts(e, t) { + const n = (m) => import(m), i = this.fetcher || (this.fetcher = await async function() { + const { fetchBuilder: a, FileSystemCache: d } = await n("node-fetch-cache"), _ = new d({ + /// By default, we don't have a complicated cache policy. + cacheDirectory: ".cache/typst/fonts" + }), f = a.withCache(_); + return function(w, P) { + const C = setTimeout(() => { + console.warn("font fetching is stucking:", w); + }, 15e3); + return f(w, P).finally(() => { + clearTimeout(C); + }); + }; + }()), s = t.filter((a) => a instanceof Uint8Array ? !0 : this.loadedFonts.has(a) ? !1 : (this.loadedFonts.add(a), !0)), o = await Promise.all(s.map(async (a) => { + if (a instanceof Uint8Array) { + await e.add_raw_font(a); + return; + } + return new Uint8Array(await (await i(a)).arrayBuffer()); + })); + for (const a of o) + a && await e.add_raw_font(a); + } + async build(e, t, n) { + const i = { ref: this, builder: t, hooks: n }; + for (const o of (e == null ? void 0 : e.beforeBuild) ?? []) + await o(void 0, i); + return n.latelyBuild && n.latelyBuild(i), await t.build(); + } +} +async function Nt(r, e, t, n) { + var i; + return await e.init((i = r == null ? void 0 : r.getModule) == null ? void 0 : i.call(r)), await new Jr().build(r, new t(), n); +} +const E = Symbol.for("reflexo-obj"); +var me; +(function(r) { + r[r.PIXEL_PER_PT = 3] = "PIXEL_PER_PT"; +})(me || (me = {})); +const Kr = [ + "DejaVuSansMono-Bold.ttf", + "DejaVuSansMono-BoldOblique.ttf", + "DejaVuSansMono-Oblique.ttf", + "DejaVuSansMono.ttf", + "LibertinusSerif-Bold.otf", + "LibertinusSerif-BoldItalic.otf", + "LibertinusSerif-Italic.otf", + "LibertinusSerif-Regular.otf", + "LibertinusSerif-Semibold.otf", + "LibertinusSerif-SemiboldItalic.otf", + "NewCM10-Bold.otf", + "NewCM10-BoldItalic.otf", + "NewCM10-Italic.otf", + "NewCM10-Regular.otf", + "NewCMMath-Bold.otf", + "NewCMMath-Book.otf", + "NewCMMath-Regular.otf" +], Gr = [ + "InriaSerif-Bold.ttf", + "InriaSerif-BoldItalic.ttf", + "InriaSerif-Italic.ttf", + "InriaSerif-Regular.ttf", + "Roboto-Regular.ttf", + "NotoSerifCJKsc-Regular.otf" +], Qr = ["TwitterColorEmoji.ttf", "NotoColorEmoji.ttf"]; +function Xr(r) { + var t, n; + const e = []; + if (r && (r == null ? void 0 : r.assets) !== !1 && ((t = r == null ? void 0 : r.assets) != null && t.length) && ((n = r == null ? void 0 : r.assets) == null ? void 0 : n.length) > 0) { + let i = { + text: "https://cdn.jsdelivr.net/gh/typst/typst-assets@v0.13.1/files/fonts/", + _: "https://cdn.jsdelivr.net/gh/typst/typst-dev-assets@v0.13.1/files/fonts/" + }, s = r.assetUrlPrefix ?? i; + typeof s == "string" ? s = { _: s } : s = { ...i, ...s }; + for (const a of Object.keys(s)) { + const d = s[a]; + d[d.length - 1] !== "/" && (s[a] = d + "/"); + } + const o = (a, d) => d.map((_) => (s[a] || s._) + _); + for (const a of r.assets) + switch (a) { + case "text": + e.push(...o(a, Kr)); + break; + case "cjk": + e.push(...o(a, Gr)); + break; + case "emoji": + e.push(...o(a, Qr)); + break; + } + } + return e; +} +function Yr(r, e) { + const t = Xr(e), n = async (i, { ref: s, builder: o }) => { + e != null && e.fetcher && s.setFetcher(e.fetcher), await s.loadFonts(o, [...r, ...t]); + }; + return n._preloadRemoteFontOptions = e, n._kind = "fontLoader", n; +} +const Zr = (r) => { + let e = !1, t; + return () => e ? t : (e = !0, t = r()); +}; +class Ut { + constructor(e) { + l(this, "wasmBin"); + l(this, "initOnce"); + if (typeof e != "function") + throw new Error("initFn is not a function"); + this.initOnce = Zr(async () => { + await e(this.wasmBin); + }); + } + async init(e) { + this.wasmBin = e, await this.initOnce(); + } +} +var Lt; +Lt = E; +class Ht { + /** + * @internal + */ + constructor(e) { + /** + * @internal + */ + l(this, Lt); + this[E] = e; + } + /** + * Reset the incremental server to the initial state. + */ + reset() { + this[E].reset(); + } + /** + * Return current result. + */ + current() { + return this[E].current(); + } + /** + * Also attach the debug info to the result. + */ + setAttachDebugInfo(e) { + this[E].set_attach_debug_info(e); + } +} +const en = new Ut(async (r) => await (await Promise.resolve().then(() => Me)).default(r)); +function Vt() { + return new ye(); +} +const Ce = class Ce { + constructor() { + l(this, "compiler"); + l(this, "compilerJs"); + } + async init(e) { + this.compilerJs = await Promise.resolve().then(() => Me); + const t = this.compilerJs.TypstCompilerBuilder, n = { ...e || {} }, i = n.beforeBuild ?? (n.beforeBuild = []), s = i.some((_) => _._preloadRemoteFontOptions !== void 0), o = i.some((_) => { + var f; + return ((f = _._preloadRemoteFontOptions) == null ? void 0 : f.assets) !== void 0; + }), a = i.some((_) => { + var f; + return ((f = _._preloadRemoteFontOptions) == null ? void 0 : f.assets) === !1; + }); + if ((!s || !o && !a) && i.push(Yr([], { assets: Ce.defaultAssets })), !i.some((_) => _._kind === "fontLoader")) + throw new Error("TypstCompiler: no font loader found, please use font loaders, e.g. preloadRemoteFonts or preloadSystemFonts"); + this.compiler = await Nt(e, en, t, {}); + } + compile(e) { + return new Promise((t) => { + if ("incrementalServer" in e) { + t(this.compiler.incr_compile(e.mainFilePath, De(e.inputs), e.incrementalServer[E], ot(e.diagnostics))); + return; + } + t(this.compiler.compile(e.mainFilePath, De(e.inputs), e.format || "vector", ot(e.diagnostics))); + }); + } + query(e) { + return new Promise((t) => { + t(JSON.parse(this.compiler.query(e.mainFilePath, De(e.inputs), e.selector, e.field))); + }); + } + getSemanticTokenLegend() { + return new Promise((e) => { + e(this.compiler.get_semantic_token_legend()); + }); + } + getSemanticTokens(e) { + return new Promise((t) => { + this.compiler.reset(), t(this.compiler.get_semantic_tokens(e.offsetEncoding || "utf-16", e.mainFilePath, e.resultId)); + }); + } + async withIncrementalServer(e) { + const t = new Ht(this.compiler.create_incr_server()); + try { + return await e(t); + } finally { + t[E].free(); + } + } + async getAst(e) { + return this.compiler.get_ast(e); + } + async reset() { + await new Promise((e) => { + this.compiler.reset(), e(void 0); + }); + } + addSource(e, t) { + if (arguments.length > 2) + throw new Error("use of addSource(path, source, isMain) is deprecated, please use addSource(path, source) instead"); + this.compiler.add_source(e, t); + } + mapShadow(e, t) { + this.compiler.map_shadow(e, t); + } + unmapShadow(e) { + this.compiler.unmap_shadow(e); + } + resetShadow() { + this.compiler.reset_shadow(); + } + renderPageToCanvas() { + throw new Error("Please use the api TypstRenderer.renderToCanvas in v0.4.0"); + } +}; +l(Ce, "defaultAssets", ["text"]); +let ye = Ce; +Vt._impl = ye; +function De(r) { + return r ? Object.entries(r) : void 0; +} +function ot(r) { + switch (r) { + case "none": + return 1; + case "unix": + return 2; + case "full": + default: + return 3; + } +} +const at = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + IncrementalServer: Ht, + createTypstCompiler: Vt +}, Symbol.toStringTag, { value: "Module" })); +let ct, $e; +function tn(r, e) { + const t = ct || r(); + return $e !== void 0 ? $e : $e = (async () => (await t.init(e), ct = t))(); +} +const rn = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + createGlobalCompiler: tn +}, Symbol.toStringTag, { value: "Module" })); +class nn { + constructor(e, t, n) { + l(this, "pageInfos"); + l(this, "loadPageCount"); + l(this, "imageScaleFactor"); + l(this, "container"); + l(this, "canvasList"); + l(this, "textLayerList"); + l(this, "commonList"); + l(this, "textLayerParentList"); + l(this, "semanticLayerList"); + this.pageInfos = e, this.imageScaleFactor = n.pixelPerPt ?? me.PIXEL_PER_PT, t.innerHTML = "", t.style.width = "100%", this.container = t, this.canvasList = new Array(this.loadPageCount), this.textLayerList = new Array(this.loadPageCount), this.commonList = new Array(this.loadPageCount), this.textLayerParentList = new Array(this.loadPageCount), this.semanticLayerList = new Array(this.loadPageCount); + const i = (s, o, a) => { + const d = Math.ceil(o.width) * this.imageScaleFactor, _ = Math.ceil(o.height) * this.imageScaleFactor, f = this.canvasList[s] = document.createElement("canvas"), w = this.semanticLayerList[s] = document.createElement("div"), P = this.textLayerList[s] = document.createElement("div"), C = this.textLayerParentList[s] = document.createElement("div"); + if (f.getContext("2d")) { + const A = document.createElement("div"); + f.width = d, f.height = _, A.appendChild(f), a.appendChild(A), A.style.position = "absolute"; + } + { + C.appendChild(P), C.className = "typst-html-semantics"; + const A = t.offsetWidth, $ = A / o.width; + C.style.width = `${A}px`, C.style.height = `${o.height * $}px`, C.style.setProperty("--data-text-width", `${$}px`), C.style.setProperty("--data-text-height", `${$}px`), a.classList.add("typst-page"), a.classList.add("canvas"), a.style.width = `${A}px`, a.style.height = `${_ * $}px`, a.style.position = "relative", w.appendChild(C), a.appendChild(w); + } + }; + for (let s = 0; s < this.pageInfos.length; s++) { + const o = this.pageInfos[s]; + let a; + a = this.commonList[s] = document.createElement("div"), t.appendChild(a), i(s, o, a); + } + } + resetLayout() { + for (let e = 0; e < this.pageInfos.length; e++) { + const t = this.pageInfos[e], n = Math.ceil(t.width) * this.imageScaleFactor, i = Math.ceil(t.height) * this.imageScaleFactor, s = this.canvasList[e].parentElement; + if (!s) + throw new Error(`canvasDiv is null for page ${e}, canvas list length ${this.canvasList.length}`); + const o = this.commonList[e], a = this.textLayerParentList[e], d = this.container.offsetWidth, _ = d / n; + a.style.width = `${d}px`, a.style.height = `${i * _}px`, o.style.width = `${d}px`, o.style.height = `${i * _}px`; + const f = this.container.offsetWidth / n; + s.style.transformOrigin = "0px 0px", s.style.transform = `scale(${f})`; + } + } +} +var G; +(function(r) { + r[r.Doc = 0] = "Doc", r[r.Slide = 1] = "Slide"; +})(G || (G = {})); +class Xe { + constructor(e) { + l(this, "hookedElem"); + l(this, "kModule"); + l(this, "opts"); + l(this, "modes", []); + /// Configuration fields + /// enable partial rendering + l(this, "partialRendering", !0); + /// underlying renderer + l(this, "renderMode", "svg"); + l(this, "r"); + /// preview mode + l(this, "previewMode", G.Doc); + /// whether this is a content preview + l(this, "isContentPreview", !1); + /// whether this content preview will mix outline titles + l(this, "isMixinOutline", !1); + /// background color + l(this, "backgroundColor", "black"); + /// default page color (empty string means transparent) + l(this, "pageColor", "white"); + /// pixel per pt + l(this, "pixelPerPt", 3); + /// customized way to retrieving dom state + l(this, "retrieveDOMState"); + /// State fields + /// whether svg is updating (in triggerSvgUpdate) + l(this, "isRendering", !1); + /// whether kModule is initialized + l(this, "moduleInitialized", !1); + /// patch queue for updating data. + l(this, "patchQueue", []); + /// resources to dispose + l(this, "disposeList", []); + /// canvas render ctoken + l(this, "canvasRenderCToken"); + /// There are two scales in this class: The real scale is to adjust the size + /// of `hookedElem` to fit the svg. The virtual scale (scale ratio) is to let + /// user zoom in/out the svg. For example: + /// + the default value of virtual scale is 1, which means the svg is totally + /// fit in `hookedElem`. + /// + if user set virtual scale to 0.5, then the svg will be zoomed out to fit + /// in half width of `hookedElem`. "real" current scale of `hookedElem` + l(this, "currentRealScale", 1); + /// "virtual" current scale of `hookedElem` + l(this, "currentScaleRatio", 1); + /// timeout for delayed viewport change + l(this, "vpTimeout"); + /// sampled by last render time. + l(this, "sampledRenderTime", 0); + /// page to partial render + l(this, "partialRenderPage", 0); + /// outline data + l(this, "outline"); + /// cursor position in form of [page, x, y] + l(this, "cursorPosition"); + // id: number = rnd++; + /// Cache fields + /// cached state of container, default to retrieve state from `this.hookedElem` + l(this, "cachedDOMState", { + width: 0, + height: 0, + window: { + innerWidth: 0, + innerHeight: 0 + }, + boundingRect: { + left: 0, + top: 0, + right: 0 + } + }); + var t, n; + this.hookedElem = e.hookedElem, this.kModule = e.kModule, this.opts = e || {}; + { + const { renderMode: i, previewMode: s, isContentPreview: o, retrieveDOMState: a } = e || {}; + this.partialRendering = !1, this.renderMode = i ?? this.renderMode, this.previewMode = s ?? this.previewMode, this.isContentPreview = o || !1, this.retrieveDOMState = a ?? (() => ({ + width: this.hookedElem.offsetWidth, + height: this.hookedElem.offsetHeight, + window: { + innerWidth: window.innerWidth, + innerHeight: window.innerHeight + }, + boundingRect: this.hookedElem.getBoundingClientRect() + })), this.backgroundColor = getComputedStyle(document.documentElement).getPropertyValue("--typst-preview-background-color"); + } + this.hookedElem.classList.add("hide-scrollbar-x"), (t = this.hookedElem.parentElement) == null || t.classList.add("hide-scrollbar-x"), this.previewMode === G.Slide && (this.hookedElem.classList.add("hide-scrollbar-y"), (n = this.hookedElem.parentElement) == null || n.classList.add("hide-scrollbar-y")), this.installCtrlWheelHandler(); + } + reset() { + this.kModule.reset(), this.moduleInitialized = !1; + } + dispose() { + const e = this.disposeList; + this.disposeList = [], e.forEach((t) => t()); + } + static derive(e, t) { + return ["rescale", "rerender", "postRender"].reduce((n, i) => (n[i] = e[`${i}$${t}`].bind(e), console.assert(n[i] !== void 0, `${i}$${t} is undefined`), n), {}); + } + registerMode(e) { + const t = Xe.derive(this, e); + this.modes.push([e, t]), e === this.renderMode && (this.r = t); + } + installCtrlWheelHandler() { + const e = [ + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1, + 1.1, + 1.3, + 1.5, + 1.7, + 1.9, + 2.1, + 2.4, + 2.7, + 3, + 3.3, + 3.7, + 4.1, + 4.6, + 5.1, + 5.7, + 6.3, + 7, + 7.7, + 8.5, + 9.4, + 10 + ], t = (n) => { + var i, s, o, a; + if (n.ctrlKey) { + n.preventDefault(), this.cachedDOMState = this.retrieveDOMState(), window.onresize !== null && (window.onresize = null); + const d = this.currentScaleRatio; + if (n.deltaY < 0) { + if (this.currentScaleRatio >= e.at(-1)) + return; + this.currentScaleRatio = e.filter((C) => C > this.currentScaleRatio).at(0); + } else if (n.deltaY > 0) { + if (this.currentScaleRatio <= e.at(0)) + return; + this.currentScaleRatio = e.filter((C) => C < this.currentScaleRatio).at(-1); + } else + return; + const _ = this.currentScaleRatio / d, f = n.pageX * (_ - 1), w = n.pageY * (_ - 1); + Math.abs(this.currentScaleRatio - 1) < 1e-5 ? (this.hookedElem.classList.add("hide-scrollbar-x"), (i = this.hookedElem.parentElement) == null || i.classList.add("hide-scrollbar-x"), this.previewMode === G.Slide && (this.hookedElem.classList.add("hide-scrollbar-y"), (s = this.hookedElem.parentElement) == null || s.classList.add("hide-scrollbar-y"))) : (this.hookedElem.classList.remove("hide-scrollbar-x"), (o = this.hookedElem.parentElement) == null || o.classList.remove("hide-scrollbar-x"), this.previewMode === G.Slide && (this.hookedElem.classList.remove("hide-scrollbar-y"), (a = this.hookedElem.parentElement) == null || a.classList.remove("hide-scrollbar-y"))); + const P = this.hookedElem.firstElementChild; + if (P) { + const C = this.getSvgScaleRatio(), F = Number.parseFloat(P.getAttribute("data-height")), A = Math.ceil(F * C); + this.hookedElem.style.height = `${A * 2}px`; + } + return window.scrollBy(f, w), this.addViewportChange(), !1; + } + }; + this.renderMode !== "dom" && (typeof acquireVsCodeApi < "u" ? (window.addEventListener("wheel", t, { + passive: !1 + }), this.disposeList.push(() => { + window.removeEventListener("wheel", t); + })) : (document.body.addEventListener("wheel", t, { + passive: !1 + }), this.disposeList.push(() => { + document.body.removeEventListener("wheel", t); + }))); + } + /// Get current scale from html to svg + // Note: one should retrieve dom state before rescale + getSvgScaleRatio() { + const e = this.hookedElem.firstElementChild; + if (!e) + return 0; + const t = this.cachedDOMState, n = Number.parseFloat(e.getAttribute("data-width") || e.getAttribute("width") || "1"), i = Number.parseFloat(e.getAttribute("data-height") || e.getAttribute("height") || "1"); + return this.currentRealScale = this.previewMode === G.Slide ? Math.min(t.width / n, t.height / i) : t.width / n, this.currentRealScale * this.currentScaleRatio; + } + processQueue(e) { + const t = e[0]; + switch (t) { + case "new": + case "diff-v1": + return t === "new" && this.reset(), this.kModule.manipulateData({ + action: "merge", + data: e[1] + }), this.moduleInitialized = !0, !0; + case "viewport-change": + return this.moduleInitialized ? !0 : (console.log("viewport-change before initialization"), !1); + default: + return console.log("svgUpdateEvent", e), !1; + } + } + triggerUpdate() { + if (this.isRendering) + return; + this.isRendering = !0; + const e = async () => { + if (this.cachedDOMState = this.retrieveDOMState(), this.patchQueue.length === 0) { + this.isRendering = !1, this.postprocessChanges(); + return; + } + try { + let t = performance.now(); + const n = this.canvasRenderCToken; + n && (await n.cancel(), await n.wait(), this.canvasRenderCToken = void 0, console.log("cancel canvas rendering")); + let i = !1; + for (; this.patchQueue.length > 0; ) + i = this.processQueue(this.patchQueue.shift()) || i; + let s = performance.now(); + i && (this.r.rescale(), await this.r.rerender(), this.r.rescale()); + let o = performance.now(); + const a = (d, _, f) => `${d} ${(f - _).toFixed(2)} ms`; + this.sampledRenderTime = o - t, console.log([a("parse", t, s), a("rerender", s, o), a("total", t, o)].join(", ")), requestAnimationFrame(e); + } catch (t) { + console.error(t), this.isRendering = !1, this.postprocessChanges(); + } + }; + requestAnimationFrame(e); + } + postprocessChanges() { + this.r.postRender(), this.previewMode === G.Slide && document.querySelectorAll(".typst-page-number-indicator").forEach((e) => { + e.textContent = `${this.kModule.retrievePagesInfo().length}`; + }); + } + addChangement(e) { + e[0] === "new" && this.patchQueue.splice(0, this.patchQueue.length); + const t = () => { + this.vpTimeout = void 0, this.patchQueue.push(e), this.triggerUpdate(); + }; + this.vpTimeout !== void 0 && clearTimeout(this.vpTimeout), e[0] === "viewport-change" && this.isRendering ? this.vpTimeout = setTimeout(t, this.sampledRenderTime || 100) : t(); + } + addViewportChange() { + this.addChangement(["viewport-change", ""]); + } +} +function sn(r) { + return class { + constructor(t) { + l(this, "impl"); + l(this, "kModule"); + if (t.isContentPreview && (t.renderMode = "canvas"), this.kModule = t.kModule, this.impl = new r(t), !this.impl.r) + throw new Error(`mode is not supported, ${t == null ? void 0 : t.renderMode}`); + t.isContentPreview && (this.impl.partialRendering = !0, this.impl.pixelPerPt = 1, this.impl.isMixinOutline = !0); + } + dispose() { + this.impl.dispose(); + } + reset() { + this.impl.reset(); + } + addChangement(t) { + this.impl.addChangement(t); + } + addViewportChange() { + this.impl.addViewportChange(); + } + setPageColor(t) { + this.impl.pageColor = t, this.addViewportChange(); + } + setPartialRendering(t) { + this.impl.partialRendering = t; + } + setCursor(t, n, i) { + this.impl.cursorPosition = [t, n, i]; + } + setPartialPageNumber(t) { + return t <= 0 || t > this.kModule.retrievePagesInfo().length ? !1 : (this.impl.partialRenderPage = t - 1, this.addViewportChange(), !0); + } + getPartialPageNumber() { + return this.impl.partialRenderPage + 1; + } + setOutineData(t) { + this.impl.outline = t, this.addViewportChange(); + } + }; +} +function on(r, ...e) { + return e.reduce((t, n) => n(t), r); +} +class an { + constructor() { + l(this, "isCancellationRequested", !1); + l(this, "_onCancelled"); + l(this, "_onCancelledResolveResolved"); + let e, t; + this._onCancelled = new Promise((n) => { + e = n, t && t(n); + }), this._onCancelledResolveResolved = new Promise((n) => { + t = n, e && n(e); + }); + } + async cancel() { + await this._onCancelledResolveResolved, this.isCancellationRequested = !0; + } + isCancelRequested() { + return this.isCancellationRequested; + } + async consume() { + (await this._onCancelledResolveResolved)(); + } + wait() { + return this._onCancelled; + } +} +const cn = () => new Promise((r) => requestAnimationFrame(r)); +var Ve; +(function(r) { + r[r.Doc = 0] = "Doc", r[r.Pages = 1] = "Pages"; +})(Ve || (Ve = {})); +var q; +(function(r) { + r[r.Layout = 0] = "Layout", r[r.Svg = 1] = "Svg", r[r.Semantics = 2] = "Semantics", r[r.PrepareCanvas = 3] = "PrepareCanvas", r[r.Canvas = 4] = "Canvas"; +})(q || (q = {})); +function _n(r) { + return class extends r { + constructor(...n) { + super(...n); + /// The template element for creating DOM by string. + l(this, "tmpl", document.createElement("template")); + /// The stub element for replacing an invisible element. + l(this, "stub", this.createElement("")); + /// Typescript side of lib. + l(this, "plugin"); + /// Rust side of kernel. + l(this, "docKernel"); + /// The element to track. + l(this, "resourceHeader"); + /// Expected exact state of the current DOM. + /// Initially it is empty meaning no any page is rendered. + l(this, "pages", []); + /// The virtual scale of the document. + l(this, "domScale", 1); + /// Track mode. + l(this, "track_mode", Ve.Doc); + /// Current executing task. + l(this, "current_task"); + /// The currently maintained viewport. + l(this, "viewport"); + if (this.registerMode("dom"), this.disposeList.push(() => { + this.dispose(); + }), this.plugin = this.opts.renderer, this.opts.domScale !== void 0) { + if (this.opts.domScale <= 0) + throw new Error("domScale must be positive"); + this.domScale = this.opts.domScale; + } + } + dispose() { + for (const n of this.pages) + n.dispose(); + this.docKernel && this.docKernel.free(); + } + createElement(n) { + return this.tmpl.innerHTML = n, this.tmpl.content.firstElementChild; + } + async mountDom(n) { + if (console.log("mountDom", n), this.docKernel) + throw new Error("already mounted"); + this.hookedElem.innerHTML = '', this.resourceHeader = this.hookedElem.querySelector(".typst-svg-resources"), this.docKernel = await this.plugin.renderer.mount_dom(this.kModule[E], this.hookedElem), this.docKernel.bind_functions({ + populateGlyphs: (i) => { + let s = this.createElement(i); + console.log("populateGlyphs", s); + let o = s.firstElementChild; + this.resourceHeader.append(o); + } + }); + } + async cancelAnyway$dom() { + if (console.log("cancelAnyway$dom"), this.current_task) { + const n = this.current_task; + this.current_task = void 0, await n.cancel(); + } + } + retrieveDOMPages() { + return Array.from(this.hookedElem.querySelectorAll(".typst-dom-page")); + } + // doesn't need to postRender + postRender$dom() { + } + // doesn't need to rescale + rescale$dom() { + } + getDomViewport(n, i) { + const s = i.left, o = -i.top, a = i.right, d = n.innerHeight - i.top, _ = { + x: 0, + y: o / this.domScale, + width: Math.max(a - s, 0) / this.domScale, + height: Math.max(d - o, 0) / this.domScale + }; + return (_.width <= 0 || _.height <= 0) && (_.x = _.y = _.width = _.height = 0), _; + } + // fast mode + async rerender$dom() { + const n = this.retrieveDOMState(), { x: i, y: s, width: o, height: a } = this.getDomViewport(n.window, n.boundingRect); + if (!await this.docKernel.relayout(i, s, o, a)) + return; + const _ = new an(); + this.doRender$dom(_), this.current_task = _; + } + async doRender$dom(n) { + const i = (d, _) => { + if (d && !n.isCancelRequested() && _) + return _(); + }, s = this.retrieveDOMPages().map((d) => { + const { innerWidth: _, innerHeight: f } = window, w = d.getBoundingClientRect(); + return { + inWindow: !(w.left > _ || w.right < 0 || w.top > f || w.bottom < 0), + page: d + }; + }), o = async (d) => { + if (await cn(), n.isCancelRequested()) { + console.log("cancel stage", q.Layout, d); + return; + } + const _ = s[d].page, f = _.getBoundingClientRect(), w = this.getDomViewport(window, f), P = (D) => this.docKernel.need_repaint(d, w.x, w.y, w.width, w.height, D), C = (D) => this.docKernel.repaint(d, w.x, w.y, w.width, w.height, D), F = (D) => { + if (!n.isCancelRequested()) + return i(P(D), () => C(D)); + }; + await F(q.Layout); + const A = (f.width ? Number.parseFloat(_.getAttribute("data-width")) / f.width : 1) * this.domScale, $ = (f.height ? Number.parseFloat(_.getAttribute("data-height")) / f.height : 1) * this.domScale; + if (w.x *= A, w.y *= $, w.y -= 100, w.width *= A, w.height *= $, w.height += 200, await F(q.Svg), await F(q.Semantics), n.isCancelRequested()) { + console.log("cancel stage", q.Semantics, d); + return; + } + P(q.PrepareCanvas) ? (async () => { + if (await C(q.PrepareCanvas), !n.isCancelRequested()) + return F(q.Canvas); + })() : await F(q.Canvas); + }, a = async (d) => { + for (let _ = 0; _ < s.length; ++_) { + if (n.isCancelRequested()) { + console.log("cancel page", q.Layout, _); + return; + } + s[_].inWindow === d && await o(_); + } + }; + this.cancelAnyway$dom(), await a(!0), await a(!1), !n.isCancelRequested() && console.log("finished", q.Layout); + } + }; +} +class un extends sn(on(Xe, _n)) { +} +var At; +let he = (At = E, class { + /** + * @internal + */ + constructor(e, t) { + l(this, "plugin"); + /** + * @internal + */ + l(this, At); + this.plugin = e, this[E] = t; + } + /** + * @deprecated set in {@link RenderToCanvasOptions} instead + * + * Set the background color of the Typst document. + * @param {string} t - The background color in format of `^#?[0-9a-f]{6}$` + * + * Note: Default to `#ffffff`. + * + * Note: Only available in canvas rendering mode. + */ + set backgroundColor(e) { + e !== void 0 && (this[E].background_color = e); + } + /** + * Get the background color of the Typst document. + * + * Note: Default to `#ffffff`. + * + * Note: Only available in canvas rendering mode. + */ + get backgroundColor() { + return this[E].background_color; + } + /** + * Set the pixel per point scale up the canvas panel. + * + * Note: Default to `3`. + * + * Note: Only available in canvas rendering mode. + */ + set pixelPerPt(e) { + e !== void 0 && (this[E].pixel_per_pt = e); + } + /** + * @deprecated set in {@link RenderToCanvasOptions} instead + * + * Get the pixel per point scale up the canvas panel. + * + * Note: Default to `3`. + * + * Note: Only available in canvas rendering mode. + */ + get pixelPerPt() { + return this[E].pixel_per_pt; + } + /** + * Reset state + */ + reset() { + this.plugin.resetSession(this); + } + /** + * @deprecated + * use {@link docWidth} instead + */ + get doc_width() { + return this[E].doc_width; + } + get docWidth() { + return this[E].doc_width; + } + /** + * @deprecated + * use {@link docHeight} instead + */ + get doc_height() { + return this[E].doc_height; + } + get docHeight() { + return this[E].doc_height; + } + retrievePagesInfo() { + const e = this[E].pages_info, t = [], n = e.page_count; + for (let i = 0; i < n; i++) { + const s = e.page(i); + t.push({ + pageOffset: s.page_off, + width: s.width_pt, + height: s.height_pt + }); + } + return t; + } + getSourceLoc(e) { + return this[E].source_span(e); + } + /** + * See {@link TypstRenderer#renderSvg} for more details. + */ + renderSvg(e) { + return this.plugin.renderSvg({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderToSvg} for more details. + */ + renderToSvg(e) { + return this.plugin.renderToSvg({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderCanvas} for more details. + */ + renderCanvas(e) { + return this.plugin.renderCanvas({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#manipulateData} for more details. + */ + manipulateData(e) { + this.plugin.manipulateData({ + renderSession: this, + ...e + }); + } + /** + * See {@link TypstRenderer#renderSvgDiff} for more details. + */ + renderSvgDiff(e) { + return this.plugin.renderSvgDiff({ + renderSession: this, + ...e + }); + } + /** + * @deprecated + * use {@link getSourceLoc} instead + */ + get_source_loc(e) { + return this[E].source_span(e); + } + /** + * @deprecated + * use {@link renderSvgDiff} instead + */ + render_in_window(e, t, n, i) { + return this[E].render_in_window(e, t, n, i); + } + /** + * @deprecated + * use {@link manipulateData} instead + */ + merge_delta(e) { + this.plugin.manipulateData({ + renderSession: this, + action: "merge", + data: e + }); + } +}); +var ee; +(function(r) { + r[r.Delete = 0] = "Delete", r[r.New = 1] = "New", r[r.Update = 2] = "Update"; +})(ee || (ee = {})); +var Tt; +let Jt = (Tt = E, class { + /** + * @internal + */ + constructor(e, t) { + l(this, "plugin"); + /** + * @internal + */ + l(this, Tt); + /** + * @internal + */ + l(this, "managedCanvasElemList", /* @__PURE__ */ new Map()); + /** + * @internal + */ + l(this, "canvasCounter", Math.random()); + this.plugin = e, this[E] = t; + } + /** + * See {@link TypstRenderer#manipulateData} for more details. + */ + manipulateData(e, t) { + return this[E].manipulate_data(e, t); + } + /** + * You must submit all canvas in pages to ensure synchronization with the background worker + * + * See {@link TypstRenderer#renderCanvas} for more details. + */ + renderCanvas(e) { + const t = this.managedCanvasElemList; + for (const [a, d] of t) + d[0] = ee.Delete; + for (const a of e) { + const d = a.canvas; + let _ = d.dataset.manageId, f = ee.Update; + _ || (_ = this.canvasCounter.toFixed(5), this.canvasCounter += 1, d.dataset.manageId = _, f = ee.New); + let w = t.get(_); + if (w && w[0] !== ee.Delete) + throw new Error("cannot update a canvas for two times in batch"); + t.set(_, [f, { ...a }]); + } + const n = Array.from(t.entries()), i = new Uint8Array(n.length), s = new Array(n.length), o = n.map(([a, [d, _]], f) => (d || t.delete(a), i[f] = d, s[f] = _.canvas, this.plugin.canvasOptionsToRust(_))); + return this[E].render_canvas(i, s, o); + } + async retrievePagesInfo() { + const e = await this[E].get_pages_info(); + console.log(e); + const t = [], n = e.page_count; + for (let i = 0; i < n; i++) { + const s = e.page(i); + t.push({ + pageOffset: s.page_off, + width: s.width_pt, + height: s.height_pt + }); + } + return t; + } +}); +const dn = (r) => new Ut(async (e) => await r.default(e)); +function ln() { + return new Kt(); +} +let _t = !0; +class Kt { + constructor() { + l(this, "renderer"); + l(this, "rendererJs"); + } + async init(e) { + var n; + this.rendererJs = await (((n = e == null ? void 0 : e.getWrapper) == null ? void 0 : n.call(e)) || Promise.resolve().then(() => Ye)); + const t = this.rendererJs.TypstRendererBuilder; + this.renderer = await Nt(e, dn(this.rendererJs), t, {}); + } + loadGlyphPack(e) { + return Promise.resolve(); + } + createOptionsToRust(e) { + const t = new this.rendererJs.CreateSessionOptions(); + return e.format !== void 0 && (t.format = e.format), e.artifactContent !== void 0 && (t.artifact_content = e.artifactContent), t; + } + canvasOptionsToRust(e) { + const t = new this.rendererJs.RenderPageImageOptions(); + if (e.pageOffset === void 0) + throw new Error("pageOffset is required in reflexo v0.5.0"); + if (t.page_off = e.pageOffset, e.cacheKey !== void 0 && (t.cache_key = e.cacheKey), e.backgroundColor !== void 0 && (t.background_color = e.backgroundColor), e.pixelPerPt !== void 0 && (t.pixel_per_pt = e.pixelPerPt), e.dataSelection !== void 0) { + let n = 0; + e.dataSelection.body ? n |= 1 : e.canvas && _t && (_t = !1, console.warn("dataSelection.body is not set but providing canvas for body")), (e.dataSelection.text || e.dataSelection.annotation) && console.error("dataSelection.text and dataSelection.annotation are deprecated"), e.dataSelection.semantics && (n |= 8), t.data_selection = n; + } + return t; + } + retrievePagesInfoFromSession(e) { + return e.retrievePagesInfo(); + } + /** + * Render a Typst document to canvas. + */ + renderCanvas(e) { + return this.withinOptionSession(e, async (t) => this.renderer.render_page_to_canvas(t[E], e.canvas || void 0, this.canvasOptionsToRust(e))); + } + // async renderPdf(artifactContent: string): Promise { + // return this.renderer.render_to_pdf(artifactContent); + // } + async inAnimationFrame(e) { + return new Promise((t, n) => { + requestAnimationFrame(() => { + try { + t(e()); + } catch (i) { + n(i); + } + }); + }); + } + async renderDisplayLayer(e, t, n) { + const s = e[E].pages_info.page_count, o = async (f, w) => { + const C = t[f].getContext("2d"); + if (!C) + throw new Error("canvas context is null"); + return await this.renderCanvas({ + ...n, + canvas: C, + renderSession: e, + pageOffset: w + }); + }, a = performance.now(), d = await (async () => { + const f = []; + for (let w = 0; w < s; w++) + f.push(await this.inAnimationFrame(() => o(w, w))); + return f; + })(), _ = performance.now(); + return console.log(`display layer used: render = ${(_ - a).toFixed(1)}ms`), d; + } + renderTextLayer(e, t) { + const n = performance.now(); + e.forEach((s, o) => { + s.innerHTML = t[o].htmlSemantics[0]; + }); + const i = performance.now(); + console.log(`text layer used: render = ${(i - n).toFixed(1)}ms`); + } + async render(e) { + if ("format" in e && e.format !== "vector" && ["serde_json", "js", "ir"].includes(e.format)) + throw new Error(`deprecated format ${e.format}, please use vector format`); + return this.renderToCanvas(e); + } + async renderDom(e) { + if ("format" in e && e.format !== "vector" && ["serde_json", "js", "ir"].includes(e.format)) + throw new Error(`deprecated format ${e.format}, please use vector format`); + return this.withinOptionSession(e, async (t) => { + const n = new un({ + ...e, + renderMode: "dom", + hookedElem: e.container, + kModule: t, + renderer: this + }); + return await n.impl.mountDom(e.pixelPerPt), n; + }); + } + async renderToCanvas(e) { + let t, n; + const i = e.container; + i.style.visibility = "hidden"; + const s = async (o, a) => { + try { + n = await this.renderDisplayLayer(t, o, e), a(); + } finally { + i.style.visibility = "visible"; + } + }; + return this.withinOptionSession(e, async (o) => { + if (t = o, t[E].pages_info.page_count === 0) + throw new Error("No page found in session"); + if (e.pixelPerPt !== void 0 && e.pixelPerPt <= 0) + throw new Error("Invalid typst.RenderOptions.pixelPerPt, should be a positive number " + e.pixelPerPt); + let a = e.backgroundColor; + if (a !== void 0 && !/^#[0-9a-f]{6}$/.test(a)) + throw new Error("Invalid typst.backgroundColor color for matching ^#?[0-9a-f]{6}$ " + a); + t.pixelPerPt = e.pixelPerPt ?? me.PIXEL_PER_PT, t.backgroundColor = a ?? "#ffffff"; + const d = performance.now(), _ = new nn(this.retrievePagesInfoFromSession(t), i, e), f = performance.now(); + console.log(`layer used: retrieve = ${(f - d).toFixed(1)}ms`), await s(_.canvasList, () => _.resetLayout()), this.renderTextLayer(_.textLayerList, n); + }); + } + createModule(e) { + return Promise.resolve(new he(this, this.renderer.create_session(e && this.createOptionsToRust({ + format: "vector", + artifactContent: e + })))); + } + async createWorkerV0(e) { + return new Jt(this, await this.renderer.create_worker(e)); + } + workerBridge() { + return this.renderer.create_worker_bridge(); + } + renderSvg(e, t) { + if (e instanceof he || t) + throw new Error("removed api, please use renderToSvg({ renderSession, container }) instead"); + return this.withinOptionSession(e, async (n) => { + let i; + return e.data_selection && (i = 0, e.data_selection.body && (i |= 1), e.data_selection.defs && (i |= 2), e.data_selection.css && (i |= 4), e.data_selection.js && (i |= 8)), Promise.resolve(this.renderer.svg_data(n[E], i)); + }); + } + renderSvgDiff(e) { + return e.window ? this.renderer.render_svg_diff(e.renderSession[E], e.window.lo.x, e.window.lo.y, e.window.hi.x, e.window.hi.y) : this.renderer.render_svg_diff(e.renderSession[E], 0, 0, 1e33, 1e33); + } + renderToSvg(e) { + return this.withinOptionSession(e, async (t) => Promise.resolve(this.renderer.render_svg(t[E], e.container))); + } + getCustomV1(e) { + return Promise.resolve(this.renderer.get_customs(e.renderSession[E])); + } + resetSession(e) { + return this.renderer.reset(e[E]); + } + manipulateData(e) { + return this.renderer.manipulate_data(e.renderSession[E], e.action ?? "reset", e.data); + } + withinOptionSession(e, t) { + function n(i) { + return "artifactContent" in i; + } + if ("renderSession" in e) + return t(e.renderSession); + if (n(e)) + return this.runWithSession(e, t); + throw new Error("Invalid render options, should be one of RenderByContentOptions|RenderBySessionOptions"); + } + async runWithSession(e, t) { + let n = e, i = t; + t || (n = void 0, i = e); + const s = this.renderer.create_session( + /* moved */ + n && this.createOptionsToRust(n) + ); + try { + const o = await i(new he(this, s)); + return s.free(), o; + } catch (o) { + throw s.free(), o; + } + } +} +const ut = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + RenderSession: he, + TypstRendererDriver: Kt, + TypstWorker: Jt, + createTypstRenderer: ln +}, Symbol.toStringTag, { value: "Module" })); +let dt, je; +function gn(r, e) { + const t = dt || r(); + return je !== void 0 ? je : je = (async () => (await t.init(e), dt = t))(); +} +const fn = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + createGlobalRenderer: gn +}, Symbol.toStringTag, { value: "Module" })); +let c; +const J = new Array(128).fill(void 0); +J.push(void 0, null, !0, !1); +function u(r) { + return J[r]; +} +let te = J.length; +function y(r) { + te === J.length && J.push(J.length + 1); + const e = te; + if (te = J[e], typeof te != "number") throw new Error("corrupt heap"); + return J[e] = r, e; +} +function x(r, e) { + try { + return r.apply(this, e); + } catch (t) { + c.__wbindgen_export_0(y(t)); + } +} +function h(r, e) { + try { + return r.apply(this, e); + } catch (t) { + let n = function() { + try { + return t instanceof Error ? `${t.message} + +Stack: +${t.stack}` : t.toString(); + } catch { + return ""; + } + }(); + throw console.error("wasm-bindgen: imported JS function that was not marked as `catch` threw an error:", n), t; + } +} +function p(r) { + if (typeof r != "number") throw new Error(`expected a number argument, found ${typeof r}`); +} +const Gt = typeof TextDecoder < "u" ? new TextDecoder("utf-8", { ignoreBOM: !0, fatal: !0 }) : { decode: () => { + throw Error("TextDecoder not available"); +} }; +typeof TextDecoder < "u" && Gt.decode(); +let ie = null; +function ce() { + return (ie === null || ie.byteLength === 0) && (ie = new Uint8Array(c.memory.buffer)), ie; +} +function k(r, e) { + return r = r >>> 0, Gt.decode(ce().subarray(r, r + e)); +} +let O = 0; +const pe = typeof TextEncoder < "u" ? new TextEncoder("utf-8") : { encode: () => { + throw Error("TextEncoder not available"); +} }, wn = typeof pe.encodeInto == "function" ? function(r, e) { + return pe.encodeInto(r, e); +} : function(r, e) { + const t = pe.encode(r); + return e.set(t), { + read: r.length, + written: t.length + }; +}; +function W(r, e, t) { + if (typeof r != "string") throw new Error(`expected a string argument, found ${typeof r}`); + if (t === void 0) { + const a = pe.encode(r), d = e(a.length, 1) >>> 0; + return ce().subarray(d, d + a.length).set(a), O = a.length, d; + } + let n = r.length, i = e(n, 1) >>> 0; + const s = ce(); + let o = 0; + for (; o < n; o++) { + const a = r.charCodeAt(o); + if (a > 127) break; + s[i + o] = a; + } + if (o !== n) { + o !== 0 && (r = r.slice(o)), i = t(i, n, n = o + r.length * 3, 1) >>> 0; + const a = ce().subarray(i + o, i + n), d = wn(r, a); + if (d.read !== r.length) throw new Error("failed to pass whole string"); + o += d.written, i = t(i, n, o, 1) >>> 0; + } + return O = o, i; +} +let Q = null; +function m() { + return (Q === null || Q.buffer.detached === !0 || Q.buffer.detached === void 0 && Q.buffer !== c.memory.buffer) && (Q = new DataView(c.memory.buffer)), Q; +} +function L(r) { + return r == null; +} +function j(r) { + if (typeof r != "boolean") + throw new Error(`expected a boolean argument, found ${typeof r}`); +} +function hn(r) { + r < 132 || (J[r] = te, te = r); +} +function M(r) { + const e = u(r); + return hn(r), e; +} +const ve = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => { + c.__wbindgen_export_4.get(r.dtor)(r.a, r.b); +}); +function lt(r, e, t, n) { + const i = { a: r, b: e, cnt: 1, dtor: t }, s = (...o) => { + i.cnt++; + try { + return n(i.a, i.b, ...o); + } finally { + --i.cnt === 0 && (c.__wbindgen_export_4.get(i.dtor)(i.a, i.b), i.a = 0, ve.unregister(i)); + } + }; + return s.original = i, ve.register(s, i, i), s; +} +function pn(r, e, t, n) { + const i = { a: r, b: e, cnt: 1, dtor: t }, s = (...o) => { + i.cnt++; + const a = i.a; + i.a = 0; + try { + return n(a, i.b, ...o); + } finally { + --i.cnt === 0 ? (c.__wbindgen_export_4.get(i.dtor)(a, i.b), ve.unregister(i)) : i.a = a; + } + }; + return s.original = i, ve.register(s, i, i), s; +} +function Je(r) { + const e = typeof r; + if (e == "number" || e == "boolean" || r == null) + return `${r}`; + if (e == "string") + return `"${r}"`; + if (e == "symbol") { + const i = r.description; + return i == null ? "Symbol" : `Symbol(${i})`; + } + if (e == "function") { + const i = r.name; + return typeof i == "string" && i.length > 0 ? `Function(${i})` : "Function"; + } + if (Array.isArray(r)) { + const i = r.length; + let s = "["; + i > 0 && (s += Je(r[0])); + for (let o = 1; o < i; o++) + s += ", " + Je(r[o]); + return s += "]", s; + } + const t = /\[object ([^\]]+)\]/.exec(toString.call(r)); + let n; + if (t && t.length > 1) + n = t[1]; + else + return toString.call(r); + if (n == "Object") + try { + return "Object(" + JSON.stringify(r) + ")"; + } catch { + return "Object"; + } + return r instanceof Error ? `${r.name}: ${r.message} +${r.stack}` : n; +} +function Se(r, e) { + const t = e(r.length * 1, 1) >>> 0; + return ce().set(r, t / 1), O = r.length, t; +} +function gt(r, e) { + const t = e(r.length * 4, 4) >>> 0, n = m(); + for (let i = 0; i < r.length; i++) + n.setUint32(t + 4 * i, y(r[i]), !0); + return O = r.length, t; +} +function H(r, e) { + if (!(r instanceof e)) + throw new Error(`expected instance of ${e.name}`); +} +let se = null; +function bn() { + return (se === null || se.byteLength === 0) && (se = new Uint32Array(c.memory.buffer)), se; +} +function mn(r, e) { + const t = e(r.length * 4, 4) >>> 0; + return bn().set(r, t / 4), O = r.length, t; +} +function yn() { + const r = c.renderer_build_info(); + return M(r); +} +function vn(r, e) { + p(r), p(e), c.__wbindgen_export_5(r, e); +} +function Sn(r, e, t) { + p(r), p(e), c.__wbindgen_export_6(r, e, y(t)); +} +function Cn(r, e, t) { + p(r), p(e), c.__wbindgen_export_7(r, e, y(t)); +} +function xn(r, e, t, n) { + p(r), p(e), c.__wbindgen_export_8(r, e, y(t), y(n)); +} +const ft = ["nonzero", "evenodd"], wt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_createsessionoptions_free(r >>> 0, 1)); +class Qt { + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, wt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_createsessionoptions_free(e, 0); + } + constructor() { + const e = c.createsessionoptions_new(); + return this.__wbg_ptr = e >>> 0, wt.register(this, this.__wbg_ptr, this), this; + } + /** + * @param {string} format + */ + set format(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.createsessionoptions_set_format(this.__wbg_ptr, t, n); + } + /** + * @param {Uint8Array} artifact_content + */ + set artifact_content(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = Se(e, c.__wbindgen_export_1), n = O; + c.createsessionoptions_set_artifact_content(this.__wbg_ptr, t, n); + } +} +const ht = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_incrdomdocclient_free(r >>> 0, 1)); +class xe { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + static __wrap(e) { + e = e >>> 0; + const t = Object.create(xe.prototype); + return t.__wbg_ptr = e, ht.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, ht.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_incrdomdocclient_free(e, 0); + } + /** + * @param {any} functions + */ + bind_functions(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), c.incrdomdocclient_bind_functions(this.__wbg_ptr, y(e)); + } + /** + * Relayout the document in the given window. + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + * @returns {Promise} + */ + relayout(e, t, n, i) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const s = c.incrdomdocclient_relayout(this.__wbg_ptr, e, t, n, i); + return M(s); + } + /** + * @param {number} page_num + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + * @param {number} stage + * @returns {boolean} + */ + need_repaint(e, t, n, i, s, o) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const f = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), p(e), p(o), c.incrdomdocclient_need_repaint(f, this.__wbg_ptr, e, t, n, i, s, o); + var a = m().getInt32(f + 4 * 0, !0), d = m().getInt32(f + 4 * 1, !0), _ = m().getInt32(f + 4 * 2, !0); + if (_) + throw M(d); + return a !== 0; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {number} page_num + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + * @param {number} stage + * @returns {any} + */ + repaint(e, t, n, i, s, o) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const f = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), p(e), p(o), c.incrdomdocclient_repaint(f, this.__wbg_ptr, e, t, n, i, s, o); + var a = m().getInt32(f + 4 * 0, !0), d = m().getInt32(f + 4 * 1, !0), _ = m().getInt32(f + 4 * 2, !0); + if (_) + throw M(d); + return M(a); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } +} +const pt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_pageinfo_free(r >>> 0, 1)); +class ue { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + static __wrap(e) { + e = e >>> 0; + const t = Object.create(ue.prototype); + return t.__wbg_ptr = e, pt.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, pt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_pageinfo_free(e, 0); + } + /** + * @returns {number} + */ + get page_off() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pageinfo_page_off(this.__wbg_ptr) >>> 0; + } + /** + * @returns {number} + */ + get width_pt() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pageinfo_width_pt(this.__wbg_ptr); + } + /** + * @returns {number} + */ + get height_pt() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pageinfo_height_pt(this.__wbg_ptr); + } +} +const bt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_pagesinfo_free(r >>> 0, 1)); +class ke { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + static __wrap(e) { + e = e >>> 0; + const t = Object.create(ke.prototype); + return t.__wbg_ptr = e, bt.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, bt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_pagesinfo_free(e, 0); + } + /** + * @returns {number} + */ + get page_count() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pagesinfo_page_count(this.__wbg_ptr) >>> 0; + } + /** + * @param {number} num + * @returns {PageInfo | undefined} + */ + page_by_number(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), p(e); + const t = c.pagesinfo_page_by_number(this.__wbg_ptr, e); + return t === 0 ? void 0 : ue.__wrap(t); + } + /** + * @param {number} i + * @returns {PageInfo} + */ + page(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), p(e); + const t = c.pagesinfo_page(this.__wbg_ptr, e); + return ue.__wrap(t); + } + /** + * @returns {number} + */ + width() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pagesinfo_width(this.__wbg_ptr); + } + /** + * @returns {number} + */ + height() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.pagesinfo_height(this.__wbg_ptr); + } +} +const mt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_renderpageimageoptions_free(r >>> 0, 1)); +class le { + static __unwrap(e) { + return e instanceof le ? e.__destroy_into_raw() : 0; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, mt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_renderpageimageoptions_free(e, 0); + } + constructor() { + const e = c.renderpageimageoptions_new(); + return this.__wbg_ptr = e >>> 0, mt.register(this, this.__wbg_ptr, this), this; + } + /** + * @returns {number | undefined} + */ + get pixel_per_pt() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.renderpageimageoptions_pixel_per_pt(this.__wbg_ptr); + return e === 4294967297 ? void 0 : e; + } + /** + * @param {number | null} [pixel_per_pt] + */ + set pixel_per_pt(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), L(e) || p(e), c.renderpageimageoptions_set_pixel_per_pt(this.__wbg_ptr, L(e) ? 4294967297 : Math.fround(e)); + } + /** + * @returns {string | undefined} + */ + get background_color() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const n = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.renderpageimageoptions_background_color(n, this.__wbg_ptr); + var e = m().getInt32(n + 4 * 0, !0), t = m().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = k(e, t).slice(), c.__wbindgen_export_3(e, t * 1, 1)), i; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string | null} [background_color] + */ + set background_color(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + var t = L(e) ? 0 : W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.renderpageimageoptions_set_background_color(this.__wbg_ptr, t, n); + } + /** + * @returns {number} + */ + get page_off() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.renderpageimageoptions_page_off(this.__wbg_ptr) >>> 0; + } + /** + * @param {number} page_off + */ + set page_off(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), p(e), c.renderpageimageoptions_set_page_off(this.__wbg_ptr, e); + } + /** + * @returns {string | undefined} + */ + get cache_key() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const n = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.renderpageimageoptions_cache_key(n, this.__wbg_ptr); + var e = m().getInt32(n + 4 * 0, !0), t = m().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = k(e, t).slice(), c.__wbindgen_export_3(e, t * 1, 1)), i; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string | null} [cache_key] + */ + set cache_key(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + var t = L(e) ? 0 : W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.renderpageimageoptions_set_cache_key(this.__wbg_ptr, t, n); + } + /** + * @returns {number | undefined} + */ + get data_selection() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.renderpageimageoptions_data_selection(this.__wbg_ptr); + return e === 4294967297 ? void 0 : e; + } + /** + * @param {number | null} [data_selection] + */ + set data_selection(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), L(e) || p(e), c.renderpageimageoptions_set_data_selection(this.__wbg_ptr, L(e) ? 4294967297 : e >>> 0); + } +} +const yt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_rendersession_free(r >>> 0, 1)); +class N { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + static __wrap(e) { + e = e >>> 0; + const t = Object.create(N.prototype); + return t.__wbg_ptr = e, yt.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, yt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_rendersession_free(e, 0); + } + /** + * @param {number} rect_lo_x + * @param {number} rect_lo_y + * @param {number} rect_hi_x + * @param {number} rect_hi_y + * @returns {string} + */ + render_in_window(e, t, n, i) { + let s, o; + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const _ = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.rendersession_render_in_window(_, this.__wbg_ptr, e, t, n, i); + var a = m().getInt32(_ + 4 * 0, !0), d = m().getInt32(_ + 4 * 1, !0); + return s = a, o = d, k(a, d); + } finally { + c.__wbindgen_add_to_stack_pointer(16), c.__wbindgen_export_3(s, o, 1); + } + } + /** + * @returns {number | undefined} + */ + get pixel_per_pt() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.renderpageimageoptions_pixel_per_pt(this.__wbg_ptr); + return e === 4294967297 ? void 0 : e; + } + /** + * @param {number | null} [pixel_per_pt] + */ + set pixel_per_pt(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), L(e) || p(e), c.renderpageimageoptions_set_pixel_per_pt(this.__wbg_ptr, L(e) ? 4294967297 : Math.fround(e)); + } + /** + * @returns {string | undefined} + */ + get background_color() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const n = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.rendersession_background_color(n, this.__wbg_ptr); + var e = m().getInt32(n + 4 * 0, !0), t = m().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = k(e, t).slice(), c.__wbindgen_export_3(e, t * 1, 1)), i; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string | null} [background_color] + */ + set background_color(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + var t = L(e) ? 0 : W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.rendersession_set_background_color(this.__wbg_ptr, t, n); + } + /** + * @returns {PagesInfo} + */ + get pages_info() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.rendersession_pages_info(this.__wbg_ptr); + return ke.__wrap(e); + } + /** + * @returns {number} + */ + get doc_width() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.rendersession_doc_width(this.__wbg_ptr); + } + /** + * @returns {number} + */ + get doc_height() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + return p(this.__wbg_ptr), c.rendersession_doc_height(this.__wbg_ptr); + } + /** + * @param {Uint32Array} path + * @returns {string | undefined} + */ + source_span(e) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const o = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr); + const a = mn(e, c.__wbindgen_export_1), d = O; + c.rendersession_source_span(o, this.__wbg_ptr, a, d); + var t = m().getInt32(o + 4 * 0, !0), n = m().getInt32(o + 4 * 1, !0), i = m().getInt32(o + 4 * 2, !0), s = m().getInt32(o + 4 * 3, !0); + if (s) + throw M(i); + let _; + return t !== 0 && (_ = k(t, n).slice(), c.__wbindgen_export_3(t, n * 1, 1)), _; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } +} +const vt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_rendersessionoptions_free(r >>> 0, 1)); +class kn { + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, vt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_rendersessionoptions_free(e, 0); + } + constructor() { + const e = c.rendersessionoptions_new(); + return this.__wbg_ptr = e >>> 0, vt.register(this, this.__wbg_ptr, this), this; + } + /** + * @returns {number | undefined} + */ + get pixel_per_pt() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.renderpageimageoptions_pixel_per_pt(this.__wbg_ptr); + return e === 4294967297 ? void 0 : e; + } + /** + * @param {number | null} [pixel_per_pt] + */ + set pixel_per_pt(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr), L(e) || p(e), c.renderpageimageoptions_set_pixel_per_pt(this.__wbg_ptr, L(e) ? 4294967297 : Math.fround(e)); + } + /** + * @returns {string | undefined} + */ + get background_color() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const n = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.rendersessionoptions_background_color(n, this.__wbg_ptr); + var e = m().getInt32(n + 4 * 0, !0), t = m().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = k(e, t).slice(), c.__wbindgen_export_3(e, t * 1, 1)), i; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string | null} [background_color] + */ + set background_color(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + var t = L(e) ? 0 : W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.rendersessionoptions_set_background_color(this.__wbg_ptr, t, n); + } + /** + * @returns {string | undefined} + */ + get format() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const n = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.rendersessionoptions_format(n, this.__wbg_ptr); + var e = m().getInt32(n + 4 * 0, !0), t = m().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = k(e, t).slice(), c.__wbindgen_export_3(e, t * 1, 1)), i; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string | null} [format] + */ + set format(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + var t = L(e) ? 0 : W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), n = O; + c.rendersessionoptions_set_format(this.__wbg_ptr, t, n); + } +} +const Be = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_typstrenderer_free(r >>> 0, 1)); +class Re { + static __wrap(e) { + e = e >>> 0; + const t = Object.create(Re.prototype); + return t.__wbg_ptr = e, Be.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, Be.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_typstrenderer_free(e, 0); + } + /** + * @param {any} _w + * @returns {Promise} + */ + create_worker(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = c.typstrenderer_create_worker(this.__wbg_ptr, y(e)); + return M(t); + } + /** + * @returns {WorkerBridge} + */ + create_worker_bridge() { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const i = this.__destroy_into_raw(), s = c.__wbindgen_add_to_stack_pointer(-16); + p(i), c.typstrenderer_create_worker_bridge(s, i); + var e = m().getInt32(s + 4 * 0, !0), t = m().getInt32(s + 4 * 1, !0), n = m().getInt32(s + 4 * 2, !0); + if (n) + throw M(t); + return Ee.__wrap(e); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {any} _v + */ + load_glyph_pack(e) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const i = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr), c.typstrenderer_load_glyph_pack(i, this.__wbg_ptr, y(e)); + var t = m().getInt32(i + 4 * 0, !0), n = m().getInt32(i + 4 * 1, !0); + if (n) + throw M(t); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {RenderSession} session + * @param {number} rect_lo_x + * @param {number} rect_lo_y + * @param {number} rect_hi_x + * @param {number} rect_hi_y + * @returns {string} + */ + render_svg_diff(e, t, n, i, s) { + let o, a; + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const f = c.__wbindgen_add_to_stack_pointer(-16); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + c.typstrenderer_render_svg_diff(f, this.__wbg_ptr, e.__wbg_ptr, t, n, i, s); + var d = m().getInt32(f + 4 * 0, !0), _ = m().getInt32(f + 4 * 1, !0); + return o = d, a = _, k(d, _); + } finally { + c.__wbindgen_add_to_stack_pointer(16), c.__wbindgen_export_3(o, a, 1); + } + } + /** + * @param {RenderSession} session + * @param {number | null} [parts] + * @returns {string} + */ + svg_data(e, t) { + let n, i; + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const w = c.__wbindgen_add_to_stack_pointer(-16); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + L(t) || p(t), c.typstrenderer_svg_data(w, this.__wbg_ptr, e.__wbg_ptr, L(t) ? 4294967297 : t >>> 0); + var s = m().getInt32(w + 4 * 0, !0), o = m().getInt32(w + 4 * 1, !0), a = m().getInt32(w + 4 * 2, !0), d = m().getInt32(w + 4 * 3, !0), _ = s, f = o; + if (d) + throw _ = 0, f = 0, M(a); + return n = _, i = f, k(_, f); + } finally { + c.__wbindgen_add_to_stack_pointer(16), c.__wbindgen_export_3(n, i, 1); + } + } + /** + * @param {RenderSession} session + * @returns {Array | undefined} + */ + get_customs(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + const t = c.typstrenderer_get_customs(this.__wbg_ptr, e.__wbg_ptr); + return M(t); + } + /** + * @param {RenderSession} session + * @param {HTMLElement} root + * @returns {boolean} + */ + render_svg(e, t) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const o = c.__wbindgen_add_to_stack_pointer(-16); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + c.typstrenderer_render_svg(o, this.__wbg_ptr, e.__wbg_ptr, y(t)); + var n = m().getInt32(o + 4 * 0, !0), i = m().getInt32(o + 4 * 1, !0), s = m().getInt32(o + 4 * 2, !0); + if (s) + throw M(i); + return n !== 0; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {RenderSession} ses + * @param {HTMLElement} elem + * @returns {Promise} + */ + mount_dom(e, t) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + const n = c.typstrenderer_mount_dom(this.__wbg_ptr, e.__wbg_ptr, y(t)); + return M(n); + } + /** + * @param {RenderSession} ses + * @param {any} canvas + * @param {RenderPageImageOptions | null} [options] + * @returns {Promise} + */ + render_page_to_canvas(e, t, n) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + let i = 0; + if (!L(n)) { + if (H(n, le), n.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + i = n.__destroy_into_raw(); + } + const s = c.typstrenderer_render_page_to_canvas(this.__wbg_ptr, e.__wbg_ptr, y(t), i); + return M(s); + } + constructor() { + const e = c.typstrenderer_new(); + return this.__wbg_ptr = e >>> 0, Be.register(this, this.__wbg_ptr, this), this; + } + /** + * @param {CreateSessionOptions | null} [options] + * @returns {RenderSession} + */ + create_session(e) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const s = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr); + let o = 0; + if (!L(e)) { + if (H(e, Qt), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + o = e.__destroy_into_raw(); + } + c.typstrenderer_create_session(s, this.__wbg_ptr, o); + var t = m().getInt32(s + 4 * 0, !0), n = m().getInt32(s + 4 * 1, !0), i = m().getInt32(s + 4 * 2, !0); + if (i) + throw M(n); + return N.__wrap(t); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {RenderSession} session + */ + reset(e) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const i = c.__wbindgen_add_to_stack_pointer(-16); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + c.typstrenderer_reset(i, this.__wbg_ptr, e.__wbg_ptr); + var t = m().getInt32(i + 4 * 0, !0), n = m().getInt32(i + 4 * 1, !0); + if (n) + throw M(t); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {RenderSession} session + * @param {string} action + * @param {Uint8Array} data + */ + manipulate_data(e, t, n) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const o = c.__wbindgen_add_to_stack_pointer(-16); + if (p(this.__wbg_ptr), H(e, N), e.__wbg_ptr === 0) + throw new Error("Attempt to use a moved value"); + const a = W(t, c.__wbindgen_export_1, c.__wbindgen_export_2), d = O, _ = Se(n, c.__wbindgen_export_1), f = O; + c.typstrenderer_manipulate_data(o, this.__wbg_ptr, e.__wbg_ptr, a, d, _, f); + var i = m().getInt32(o + 4 * 0, !0), s = m().getInt32(o + 4 * 1, !0); + if (s) + throw M(i); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {Uint8Array} artifact_content + * @param {string} decoder + * @returns {RenderSession} + */ + session_from_artifact(e, t) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const o = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr); + const a = Se(e, c.__wbindgen_export_1), d = O, _ = W(t, c.__wbindgen_export_1, c.__wbindgen_export_2), f = O; + c.typstrenderer_session_from_artifact(o, this.__wbg_ptr, a, d, _, f); + var n = m().getInt32(o + 4 * 0, !0), i = m().getInt32(o + 4 * 1, !0), s = m().getInt32(o + 4 * 2, !0); + if (s) + throw M(i); + return N.__wrap(n); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } +} +const St = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_typstrendererbuilder_free(r >>> 0, 1)); +class Rn { + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, St.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_typstrendererbuilder_free(e, 0); + } + constructor() { + try { + const i = c.__wbindgen_add_to_stack_pointer(-16); + c.typstrendererbuilder_new(i); + var e = m().getInt32(i + 4 * 0, !0), t = m().getInt32(i + 4 * 1, !0), n = m().getInt32(i + 4 * 2, !0); + if (n) + throw M(t); + return this.__wbg_ptr = e >>> 0, St.register(this, this.__wbg_ptr, this), this; + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @returns {Promise} + */ + build() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const e = this.__destroy_into_raw(); + p(e); + const t = c.typstrendererbuilder_build(e); + return M(t); + } + /** + * @param {any} _pack + * @returns {Promise} + */ + add_glyph_pack(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = c.typstrendererbuilder_add_glyph_pack(this.__wbg_ptr, y(e)); + return M(t); + } + /** + * @param {Uint8Array} _font_buffer + * @returns {Promise} + */ + add_raw_font(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = c.typstrendererbuilder_add_raw_font(this.__wbg_ptr, y(e)); + return M(t); + } + /** + * @param {Array} _fonts + * @returns {Promise} + */ + add_web_fonts(e) { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const t = c.typstrendererbuilder_add_web_fonts(this.__wbg_ptr, y(e)); + return M(t); + } +} +const En = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_typstworker_free(r >>> 0, 1)); +class Pn { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, En.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_typstworker_free(e, 0); + } + /** + * @param {string} _action + * @param {Uint8Array} _data + * @returns {Promise} + */ + manipulate_data(e, t) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const o = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr); + const a = W(e, c.__wbindgen_export_1, c.__wbindgen_export_2), d = O; + c.typstworker_manipulate_data(o, this.__wbg_ptr, a, d, y(t)); + var n = m().getInt32(o + 4 * 0, !0), i = m().getInt32(o + 4 * 1, !0), s = m().getInt32(o + 4 * 2, !0); + if (s) + throw M(i); + return M(n); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @returns {Promise} + */ + get_pages_info() { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + p(this.__wbg_ptr); + const e = c.typstworker_get_pages_info(this.__wbg_ptr); + return M(e); + } + /** + * @param {Uint8Array} _actions + * @param {HTMLCanvasElement[]} _canvas_list + * @param {RenderPageImageOptions[]} _data + * @returns {Promise} + */ + render_canvas(e, t, n) { + try { + if (this.__wbg_ptr == 0) throw new Error("Attempt to use a moved value"); + const a = c.__wbindgen_add_to_stack_pointer(-16); + p(this.__wbg_ptr); + const d = Se(e, c.__wbindgen_export_1), _ = O, f = gt(t, c.__wbindgen_export_1), w = O, P = gt(n, c.__wbindgen_export_1), C = O; + c.typstworker_render_canvas(a, this.__wbg_ptr, d, _, f, w, P, C); + var i = m().getInt32(a + 4 * 0, !0), s = m().getInt32(a + 4 * 1, !0), o = m().getInt32(a + 4 * 2, !0); + if (o) + throw M(s); + return M(i); + } finally { + c.__wbindgen_add_to_stack_pointer(16); + } + } +} +const Ct = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => c.__wbg_workerbridge_free(r >>> 0, 1)); +class Ee { + constructor() { + throw new Error("cannot invoke `new` directly"); + } + static __wrap(e) { + e = e >>> 0; + const t = Object.create(Ee.prototype); + return t.__wbg_ptr = e, Ct.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, Ct.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + c.__wbg_workerbridge_free(e, 0); + } +} +async function In(r, e) { + if (typeof Response == "function" && r instanceof Response) { + if (typeof WebAssembly.instantiateStreaming == "function") + try { + return await WebAssembly.instantiateStreaming(r, e); + } catch (n) { + if (r.headers.get("Content-Type") != "application/wasm") + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", n); + else + throw n; + } + const t = await r.arrayBuffer(); + return await WebAssembly.instantiate(t, e); + } else { + const t = await WebAssembly.instantiate(r, e); + return t instanceof WebAssembly.Instance ? { instance: t, module: r } : t; + } +} +function Xt() { + const r = {}; + return r.wbg = {}, r.wbg.__wbg_appendChild_8204974b7328bf98 = function() { + return x(function(e, t) { + const n = u(e).appendChild(u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_buffer_609cc3eee51ed158 = function() { + return h(function(e) { + const t = u(e).buffer; + return y(t); + }, arguments); + }, r.wbg.__wbg_call_672a4d21634d4a24 = function() { + return x(function(e, t) { + const n = u(e).call(u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_call_7cccdd69e0791ae2 = function() { + return x(function(e, t, n) { + const i = u(e).call(u(t), u(n)); + return y(i); + }, arguments); + }, r.wbg.__wbg_call_833bed5770ea2041 = function() { + return x(function(e, t, n, i) { + const s = u(e).call(u(t), u(n), u(i)); + return y(s); + }, arguments); + }, r.wbg.__wbg_call_b8adc8b1d0a0d8eb = function() { + return x(function(e, t, n, i, s) { + const o = u(e).call(u(t), u(n), u(i), u(s)); + return y(o); + }, arguments); + }, r.wbg.__wbg_clearRect_8e4ba7ea0e06711a = function() { + return h(function(e, t, n, i, s) { + u(e).clearRect(t, n, i, s); + }, arguments); + }, r.wbg.__wbg_clientWidth_ce67a04dc15fce39 = function() { + return h(function(e) { + const t = u(e).clientWidth; + return p(t), t; + }, arguments); + }, r.wbg.__wbg_clip_8e8cfb00a055cd03 = function() { + return h(function(e, t) { + u(e).clip(u(t)); + }, arguments); + }, r.wbg.__wbg_clip_f584e320f8a2b022 = function() { + return h(function(e, t) { + u(e).clip(u(t)); + }, arguments); + }, r.wbg.__wbg_cloneNode_e35b333b87d51340 = function() { + return x(function(e) { + const t = u(e).cloneNode(); + return y(t); + }, arguments); + }, r.wbg.__wbg_content_537e4105afcd9cee = function() { + return h(function(e) { + const t = u(e).content; + return y(t); + }, arguments); + }, r.wbg.__wbg_createElement_8c9931a732ee2fea = function() { + return x(function(e, t, n) { + const i = u(e).createElement(k(t, n)); + return y(i); + }, arguments); + }, r.wbg.__wbg_createImageBitmap_705010fb21a22922 = function() { + return x(function(e, t) { + const n = u(e).createImageBitmap(u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_createImageBitmap_b814e27800576bdb = function() { + return x(function(e, t) { + const n = u(e).createImageBitmap(u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_createObjectURL_6e98d2f9c7bd9764 = function() { + return x(function(e, t) { + const n = URL.createObjectURL(u(t)), i = W(n, c.__wbindgen_export_1, c.__wbindgen_export_2), s = O; + m().setInt32(e + 4 * 1, s, !0), m().setInt32(e + 4 * 0, i, !0); + }, arguments); + }, r.wbg.__wbg_document_d249400bd7bd996d = function() { + return h(function(e) { + const t = u(e).document; + return L(t) ? 0 : y(t); + }, arguments); + }, r.wbg.__wbg_drawImage_0915f348c5d54848 = function() { + return x(function(e, t, n, i) { + u(e).drawImage(u(t), n, i); + }, arguments); + }, r.wbg.__wbg_drawImage_16485aae76d89dbf = function() { + return x(function(e, t, n, i, s, o) { + u(e).drawImage(u(t), n, i, s, o); + }, arguments); + }, r.wbg.__wbg_drawImage_468585e3ecfa189a = function() { + return x(function(e, t, n, i, s, o) { + u(e).drawImage(u(t), n, i, s, o); + }, arguments); + }, r.wbg.__wbg_drawImage_472a4d5b6df3739a = function() { + return x(function(e, t, n, i) { + u(e).drawImage(u(t), n, i); + }, arguments); + }, r.wbg.__wbg_drawImage_473e6602e24e18aa = function() { + return x(function(e, t, n, i) { + u(e).drawImage(u(t), n, i); + }, arguments); + }, r.wbg.__wbg_drawImage_86fd8465c00c7bc6 = function() { + return x(function(e, t, n, i, s, o) { + u(e).drawImage(u(t), n, i, s, o); + }, arguments); + }, r.wbg.__wbg_drawImage_ff273710b96c85cc = function() { + return x(function(e, t, n, i, s, o) { + u(e).drawImage(u(t), n, i, s, o); + }, arguments); + }, r.wbg.__wbg_error_7534b8e9a36f1ab4 = function() { + return h(function(e, t) { + let n, i; + try { + n = e, i = t, console.error(k(e, t)); + } finally { + c.__wbindgen_export_3(n, i, 1); + } + }, arguments); + }, r.wbg.__wbg_fillRect_b1529535ac758d4c = function() { + return h(function(e, t, n, i, s) { + u(e).fillRect(t, n, i, s); + }, arguments); + }, r.wbg.__wbg_fillRect_c38d5d56492a2368 = function() { + return h(function(e, t, n, i, s) { + u(e).fillRect(t, n, i, s); + }, arguments); + }, r.wbg.__wbg_fill_51814702df845abd = function() { + return h(function(e, t, n) { + u(e).fill(u(t), ft[n]); + }, arguments); + }, r.wbg.__wbg_fill_5d26765e6d1d8f6b = function() { + return h(function(e, t) { + u(e).fill(u(t)); + }, arguments); + }, r.wbg.__wbg_fill_64902335a40baa8d = function() { + return h(function(e, t, n) { + u(e).fill(u(t), ft[n]); + }, arguments); + }, r.wbg.__wbg_fill_cbb22e6ac4da5b1b = function() { + return h(function(e, t) { + u(e).fill(u(t)); + }, arguments); + }, r.wbg.__wbg_firstElementChild_21331181ca115bcc = function() { + return h(function(e) { + const t = u(e).firstElementChild; + return L(t) ? 0 : y(t); + }, arguments); + }, r.wbg.__wbg_firstElementChild_d75d385f5abd1414 = function() { + return h(function(e) { + const t = u(e).firstElementChild; + return L(t) ? 0 : y(t); + }, arguments); + }, r.wbg.__wbg_getAttribute_ea5166be2deba45e = function() { + return h(function(e, t, n, i) { + const s = u(t).getAttribute(k(n, i)); + var o = L(s) ? 0 : W(s, c.__wbindgen_export_1, c.__wbindgen_export_2), a = O; + m().setInt32(e + 4 * 1, a, !0), m().setInt32(e + 4 * 0, o, !0); + }, arguments); + }, r.wbg.__wbg_getContext_e9cf379449413580 = function() { + return x(function(e, t, n) { + const i = u(e).getContext(k(t, n)); + return L(i) ? 0 : y(i); + }, arguments); + }, r.wbg.__wbg_getContext_f65a0debd1e8f8e8 = function() { + return x(function(e, t, n) { + const i = u(e).getContext(k(t, n)); + return L(i) ? 0 : y(i); + }, arguments); + }, r.wbg.__wbg_get_67b2ba62fc30de12 = function() { + return x(function(e, t) { + const n = Reflect.get(u(e), u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_globalCompositeOperation_154b0f30008caa5e = function() { + return x(function(e, t) { + const n = u(t).globalCompositeOperation, i = W(n, c.__wbindgen_export_1, c.__wbindgen_export_2), s = O; + m().setInt32(e + 4 * 1, s, !0), m().setInt32(e + 4 * 0, i, !0); + }, arguments); + }, r.wbg.__wbg_globalCompositeOperation_1f405e2ef7c5118b = function() { + return x(function(e, t) { + const n = u(t).globalCompositeOperation, i = W(n, c.__wbindgen_export_1, c.__wbindgen_export_2), s = O; + m().setInt32(e + 4 * 1, s, !0), m().setInt32(e + 4 * 0, i, !0); + }, arguments); + }, r.wbg.__wbg_height_d3f39e12f0f62121 = function() { + return h(function(e) { + const t = u(e).height; + return p(t), t; + }, arguments); + }, r.wbg.__wbg_incrdomdocclient_new = function() { + return h(function(e) { + const t = xe.__wrap(e); + return y(t); + }, arguments); + }, r.wbg.__wbg_instanceof_CanvasRenderingContext2d_df82a4d3437bf1cc = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof CanvasRenderingContext2D; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_Element_0af65443936d5154 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof Element; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_HtmlCanvasElement_2ea67072a7624ac5 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof HTMLCanvasElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_HtmlDivElement_dbc6eb62eb772174 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof HTMLDivElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_HtmlElement_51378c201250b16c = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof HTMLElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_HtmlTemplateElement_7929a67c77198607 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof HTMLTemplateElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_ImageBitmap_d093d508663e313d = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof ImageBitmap; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_OffscreenCanvasRenderingContext2d_a070fdde7ba760a3 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof OffscreenCanvasRenderingContext2D; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_OffscreenCanvas_d55760945f91bf51 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof OffscreenCanvas; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_Promise_935168b8f4b49db3 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof Promise; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_SvgGraphicsElement_8b2cbd8116680c53 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof SVGGraphicsElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_SvgsvgElement_6a0d878e0d0f979c = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof SVGSVGElement; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof Window; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_instanceof_WorkerGlobalScope_dbdbdea7e3b56493 = function() { + return h(function(e) { + let t; + try { + t = u(e) instanceof WorkerGlobalScope; + } catch { + t = !1; + } + const n = t; + return j(n), n; + }, arguments); + }, r.wbg.__wbg_lastElementChild_1269b660ec3e6985 = function() { + return h(function(e) { + const t = u(e).lastElementChild; + return L(t) ? 0 : y(t); + }, arguments); + }, r.wbg.__wbg_length_a446193dc22c12f8 = function() { + return h(function(e) { + const t = u(e).length; + return p(t), t; + }, arguments); + }, r.wbg.__wbg_log_1ae1e9f741096e91 = function() { + return h(function(e, t) { + console.log(u(e), u(t)); + }, arguments); + }, r.wbg.__wbg_log_c222819a41e063d3 = function() { + return h(function(e) { + console.log(u(e)); + }, arguments); + }, r.wbg.__wbg_measureText_f0f078704231c37f = function() { + return x(function(e, t, n) { + const i = u(e).measureText(k(t, n)); + return y(i); + }, arguments); + }, r.wbg.__wbg_new_23a2665fac83c611 = function() { + return h(function(e, t) { + try { + var n = { a: e, b: t }, i = (o, a) => { + const d = n.a; + n.a = 0; + try { + return xn(d, n.b, o, a); + } finally { + n.a = d; + } + }; + const s = new Promise(i); + return y(s); + } finally { + n.a = n.b = 0; + } + }, arguments); + }, r.wbg.__wbg_new_2ef971087cb43792 = function() { + return x(function(e, t) { + const n = new OffscreenCanvas(e >>> 0, t >>> 0); + return y(n); + }, arguments); + }, r.wbg.__wbg_new_405e22f390576ce2 = function() { + return h(function() { + const e = new Object(); + return y(e); + }, arguments); + }, r.wbg.__wbg_new_6377da097a44ce6e = function() { + return x(function() { + const e = new Image(); + return y(e); + }, arguments); + }, r.wbg.__wbg_new_78feb108b6472713 = function() { + return h(function() { + const e = new Array(); + return y(e); + }, arguments); + }, r.wbg.__wbg_new_8a6f238a6ece86ea = function() { + return h(function() { + const e = new Error(); + return y(e); + }, arguments); + }, r.wbg.__wbg_new_a12002a7f91c75be = function() { + return h(function(e) { + const t = new Uint8Array(u(e)); + return y(t); + }, arguments); + }, r.wbg.__wbg_new_c68d7209be747379 = function() { + return h(function(e, t) { + const n = new Error(k(e, t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_newnoargs_105ed471475aaf50 = function() { + return h(function(e, t) { + const n = createCspSafeFunction(k(e, t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function() { + return h(function(e, t, n) { + const i = new Uint8Array(u(e), t >>> 0, n >>> 0); + return y(i); + }, arguments); + }, r.wbg.__wbg_newwithlength_a381634e90c276d4 = function() { + return h(function(e) { + const t = new Uint8Array(e >>> 0); + return y(t); + }, arguments); + }, r.wbg.__wbg_newwithpathstring_e9586ab3affcd4fd = function() { + return x(function(e, t) { + const n = new Path2D(k(e, t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_newwithu8arraysequenceandoptions_068570c487f69127 = function() { + return x(function(e, t) { + const n = new Blob(u(e), u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_nextElementSibling_8472709bec4de113 = function() { + return h(function(e) { + const t = u(e).nextElementSibling; + return L(t) ? 0 : y(t); + }, arguments); + }, r.wbg.__wbg_push_737cfc8c1432c2c6 = function() { + return h(function(e, t) { + const n = u(e).push(u(t)); + return p(n), n; + }, arguments); + }, r.wbg.__wbg_putImageData_4c5aa10f3b3e4924 = function() { + return x(function(e, t, n, i) { + u(e).putImageData(u(t), n, i); + }, arguments); + }, r.wbg.__wbg_putImageData_6d5d5ef6ee83898b = function() { + return x(function(e, t, n, i) { + u(e).putImageData(u(t), n, i); + }, arguments); + }, r.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function() { + return h(function(e) { + queueMicrotask(u(e)); + }, arguments); + }, r.wbg.__wbg_queueMicrotask_d3219def82552485 = function() { + return h(function(e) { + const t = u(e).queueMicrotask; + return y(t); + }, arguments); + }, r.wbg.__wbg_removeProperty_0e85471f4dfc00ae = function() { + return x(function(e, t, n, i) { + const s = u(t).removeProperty(k(n, i)), o = W(s, c.__wbindgen_export_1, c.__wbindgen_export_2), a = O; + m().setInt32(e + 4 * 1, a, !0), m().setInt32(e + 4 * 0, o, !0); + }, arguments); + }, r.wbg.__wbg_remove_e2d2659f3128c045 = function() { + return h(function(e) { + u(e).remove(); + }, arguments); + }, r.wbg.__wbg_renderpageimageoptions_unwrap = function() { + return h(function(e) { + const t = le.__unwrap(M(e)); + return p(t), t; + }, arguments); + }, r.wbg.__wbg_replaceWith_9ce9927e3141d0f6 = function() { + return x(function(e, t) { + u(e).replaceWith(u(t)); + }, arguments); + }, r.wbg.__wbg_resolve_4851785c9c5f573d = function() { + return h(function(e) { + const t = Promise.resolve(u(e)); + return y(t); + }, arguments); + }, r.wbg.__wbg_restore_1ef50af0835a4649 = function() { + return h(function(e) { + u(e).restore(); + }, arguments); + }, r.wbg.__wbg_restore_cc5ae2746f7b5043 = function() { + return h(function(e) { + u(e).restore(); + }, arguments); + }, r.wbg.__wbg_revokeObjectURL_27267efebeb457c7 = function() { + return x(function(e, t) { + URL.revokeObjectURL(k(e, t)); + }, arguments); + }, r.wbg.__wbg_save_5f563096e64939da = function() { + return h(function(e) { + u(e).save(); + }, arguments); + }, r.wbg.__wbg_save_c675a7a4bbd44e4a = function() { + return h(function(e) { + u(e).save(); + }, arguments); + }, r.wbg.__wbg_setAttribute_2704501201f15687 = function() { + return x(function(e, t, n, i, s) { + u(e).setAttribute(k(t, n), k(i, s)); + }, arguments); + }, r.wbg.__wbg_setLineDash_0e3f3e194352a774 = function() { + return x(function(e, t) { + u(e).setLineDash(u(t)); + }, arguments); + }, r.wbg.__wbg_setLineDash_325e094206df53e9 = function() { + return x(function(e, t) { + u(e).setLineDash(u(t)); + }, arguments); + }, r.wbg.__wbg_setProperty_f2cf326652b9a713 = function() { + return x(function(e, t, n, i, s) { + u(e).setProperty(k(t, n), k(i, s)); + }, arguments); + }, r.wbg.__wbg_setTransform_8c4d954cafb34b75 = function() { + return x(function(e, t, n, i, s, o, a) { + u(e).setTransform(t, n, i, s, o, a); + }, arguments); + }, r.wbg.__wbg_setTransform_da2f0baec3f09522 = function() { + return x(function(e, t, n, i, s, o, a) { + u(e).setTransform(t, n, i, s, o, a); + }, arguments); + }, r.wbg.__wbg_set_37837023f3d740e8 = function() { + return h(function(e, t, n) { + u(e)[t >>> 0] = M(n); + }, arguments); + }, r.wbg.__wbg_set_65595bdd868b3009 = function() { + return h(function(e, t, n) { + u(e).set(u(t), n >>> 0); + }, arguments); + }, r.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { + return x(function(e, t, n) { + const i = Reflect.set(u(e), u(t), u(n)); + return j(i), i; + }, arguments); + }, r.wbg.__wbg_setfillStyle_2205fca942c641ba = function() { + return h(function(e, t, n) { + u(e).fillStyle = k(t, n); + }, arguments); + }, r.wbg.__wbg_setfillStyle_cb059a69ce15cc28 = function() { + return h(function(e, t, n) { + u(e).fillStyle = k(t, n); + }, arguments); + }, r.wbg.__wbg_setfont_4c3584ef2f5c9f7e = function() { + return h(function(e, t, n) { + u(e).font = k(t, n); + }, arguments); + }, r.wbg.__wbg_setglobalCompositeOperation_9a7a92bac2fb7ffd = function() { + return x(function(e, t, n) { + u(e).globalCompositeOperation = k(t, n); + }, arguments); + }, r.wbg.__wbg_setglobalCompositeOperation_b000e874f8f4a9d3 = function() { + return x(function(e, t, n) { + u(e).globalCompositeOperation = k(t, n); + }, arguments); + }, r.wbg.__wbg_setheight_da683a33fa99843c = function() { + return h(function(e, t) { + u(e).height = t >>> 0; + }, arguments); + }, r.wbg.__wbg_setinnerHTML_31bde41f835786f7 = function() { + return h(function(e, t, n) { + u(e).innerHTML = k(t, n); + }, arguments); + }, r.wbg.__wbg_setlineCap_3a3987ad3f03b31d = function() { + return h(function(e, t, n) { + u(e).lineCap = k(t, n); + }, arguments); + }, r.wbg.__wbg_setlineCap_52b6d742c95a5630 = function() { + return h(function(e, t, n) { + u(e).lineCap = k(t, n); + }, arguments); + }, r.wbg.__wbg_setlineDashOffset_030d80d07cd52ee4 = function() { + return h(function(e, t) { + u(e).lineDashOffset = t; + }, arguments); + }, r.wbg.__wbg_setlineDashOffset_59f274962f6a0553 = function() { + return h(function(e, t) { + u(e).lineDashOffset = t; + }, arguments); + }, r.wbg.__wbg_setlineJoin_79ca64e7e9efaff7 = function() { + return h(function(e, t, n) { + u(e).lineJoin = k(t, n); + }, arguments); + }, r.wbg.__wbg_setlineJoin_7e005d90ef83d627 = function() { + return h(function(e, t, n) { + u(e).lineJoin = k(t, n); + }, arguments); + }, r.wbg.__wbg_setlineWidth_3c8b7156949a9f4b = function() { + return h(function(e, t) { + u(e).lineWidth = t; + }, arguments); + }, r.wbg.__wbg_setlineWidth_ec730c524f09baa9 = function() { + return h(function(e, t) { + u(e).lineWidth = t; + }, arguments); + }, r.wbg.__wbg_setmiterLimit_26162c359bb28eb2 = function() { + return h(function(e, t) { + u(e).miterLimit = t; + }, arguments); + }, r.wbg.__wbg_setmiterLimit_9ffca64ec692501d = function() { + return h(function(e, t) { + u(e).miterLimit = t; + }, arguments); + }, r.wbg.__wbg_setonerror_e94ca1221abc457f = function() { + return h(function(e, t) { + u(e).onerror = u(t); + }, arguments); + }, r.wbg.__wbg_setonload_264a0d330b7166fb = function() { + return h(function(e, t) { + u(e).onload = u(t); + }, arguments); + }, r.wbg.__wbg_setsrc_c239193cc7ab0470 = function() { + return h(function(e, t, n) { + u(e).src = k(t, n); + }, arguments); + }, r.wbg.__wbg_setstrokeStyle_070920f27992b9a6 = function() { + return h(function(e, t, n) { + u(e).strokeStyle = k(t, n); + }, arguments); + }, r.wbg.__wbg_setstrokeStyle_415833f3f0eb5076 = function() { + return h(function(e, t, n) { + u(e).strokeStyle = k(t, n); + }, arguments); + }, r.wbg.__wbg_settype_39ed370d3edd403c = function() { + return h(function(e, t, n) { + u(e).type = k(t, n); + }, arguments); + }, r.wbg.__wbg_setwidth_c5fed9f5e7f0b406 = function() { + return h(function(e, t) { + u(e).width = t >>> 0; + }, arguments); + }, r.wbg.__wbg_stack_0ed75d68575b0f3c = function() { + return h(function(e, t) { + const n = u(t).stack, i = W(n, c.__wbindgen_export_1, c.__wbindgen_export_2), s = O; + m().setInt32(e + 4 * 1, s, !0), m().setInt32(e + 4 * 0, i, !0); + }, arguments); + }, r.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { + return h(function() { + const e = typeof global > "u" ? null : global; + return L(e) ? 0 : y(e); + }, arguments); + }, r.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { + return h(function() { + const e = typeof globalThis > "u" ? null : globalThis; + return L(e) ? 0 : y(e); + }, arguments); + }, r.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { + return h(function() { + const e = typeof self > "u" ? null : self; + return L(e) ? 0 : y(e); + }, arguments); + }, r.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { + return h(function() { + const e = typeof window > "u" ? null : window; + return L(e) ? 0 : y(e); + }, arguments); + }, r.wbg.__wbg_stringify_f7ed6987935b4a24 = function() { + return x(function(e) { + const t = JSON.stringify(u(e)); + return y(t); + }, arguments); + }, r.wbg.__wbg_stroke_1b0348380fb5a54b = function() { + return h(function(e, t) { + u(e).stroke(u(t)); + }, arguments); + }, r.wbg.__wbg_stroke_e9b15e77122a9be9 = function() { + return h(function(e, t) { + u(e).stroke(u(t)); + }, arguments); + }, r.wbg.__wbg_style_fb30c14e5815805c = function() { + return h(function(e) { + const t = u(e).style; + return y(t); + }, arguments); + }, r.wbg.__wbg_then_44b73946d2fb3e7d = function() { + return h(function(e, t) { + const n = u(e).then(u(t)); + return y(n); + }, arguments); + }, r.wbg.__wbg_then_48b406749878a531 = function() { + return h(function(e, t, n) { + const i = u(e).then(u(t), u(n)); + return y(i); + }, arguments); + }, r.wbg.__wbg_transferToImageBitmap_4b1cc41c0f7e5de5 = function() { + return x(function(e) { + const t = u(e).transferToImageBitmap(); + return y(t); + }, arguments); + }, r.wbg.__wbg_typstrenderer_new = function() { + return h(function(e) { + const t = Re.__wrap(e); + return y(t); + }, arguments); + }, r.wbg.__wbg_warn_4ca3906c248c47c4 = function() { + return h(function(e) { + console.warn(u(e)); + }, arguments); + }, r.wbg.__wbg_width_2fafd30484634e26 = function() { + return h(function(e) { + return u(e).width; + }, arguments); + }, r.wbg.__wbg_width_4f334fc47ef03de1 = function() { + return h(function(e) { + const t = u(e).width; + return p(t), t; + }, arguments); + }, r.wbg.__wbindgen_cb_drop = function(e) { + const t = M(e).original; + if (t.cnt-- == 1) + return t.a = 0, !0; + const n = !1; + return j(n), n; + }, r.wbg.__wbindgen_closure_wrapper1625 = function() { + return h(function(e, t, n) { + const i = lt(e, t, 134, vn); + return y(i); + }, arguments); + }, r.wbg.__wbindgen_closure_wrapper1626 = function() { + return h(function(e, t, n) { + const i = lt(e, t, 134, Sn); + return y(i); + }, arguments); + }, r.wbg.__wbindgen_closure_wrapper2253 = function() { + return h(function(e, t, n) { + const i = pn(e, t, 134, Cn); + return y(i); + }, arguments); + }, r.wbg.__wbindgen_debug_string = function(e, t) { + const n = Je(u(t)), i = W(n, c.__wbindgen_export_1, c.__wbindgen_export_2), s = O; + m().setInt32(e + 4 * 1, s, !0), m().setInt32(e + 4 * 0, i, !0); + }, r.wbg.__wbindgen_is_function = function(e) { + const t = typeof u(e) == "function"; + return j(t), t; + }, r.wbg.__wbindgen_is_undefined = function(e) { + const t = u(e) === void 0; + return j(t), t; + }, r.wbg.__wbindgen_jsval_eq = function(e, t) { + const n = u(e) === u(t); + return j(n), n; + }, r.wbg.__wbindgen_memory = function() { + const e = c.memory; + return y(e); + }, r.wbg.__wbindgen_number_new = function(e) { + return y(e); + }, r.wbg.__wbindgen_object_clone_ref = function(e) { + const t = u(e); + return y(t); + }, r.wbg.__wbindgen_object_drop_ref = function(e) { + M(e); + }, r.wbg.__wbindgen_string_get = function(e, t) { + const n = u(t), i = typeof n == "string" ? n : void 0; + var s = L(i) ? 0 : W(i, c.__wbindgen_export_1, c.__wbindgen_export_2), o = O; + m().setInt32(e + 4 * 1, o, !0), m().setInt32(e + 4 * 0, s, !0); + }, r.wbg.__wbindgen_string_new = function(e, t) { + const n = k(e, t); + return y(n); + }, r.wbg.__wbindgen_throw = function(e, t) { + throw new Error(k(e, t)); + }, r; +} +function Yt(r, e) { + return c = r.exports, Zt.__wbindgen_wasm_module = e, Q = null, se = null, ie = null, c; +} +function Mn(r) { + if (c !== void 0) return c; + typeof r < "u" && (Object.getPrototypeOf(r) === Object.prototype ? { module: r } = r : console.warn("using deprecated parameters for `initSync()`; pass a single object instead")); + const e = Xt(); + r instanceof WebAssembly.Module || (r = new WebAssembly.Module(r)); + const t = new WebAssembly.Instance(r, e); + return Yt(t, r); +} +async function Zt(r) { + if (c !== void 0) return c; + typeof r < "u" && (Object.getPrototypeOf(r) === Object.prototype ? { module_or_path: r } = r : console.warn("using deprecated parameters for the initialization function; pass a single object instead")), typeof r > "u" && (r = er("typst_ts_renderer_bg.wasm", import.meta.url)); + const e = Xt(); + (typeof r == "string" || typeof Request == "function" && r instanceof Request || typeof URL == "function" && r instanceof URL) && (r = fetch(r)); + const { instance: t, module: n } = await In(await r, e); + return Yt(t, n); +} +let er = async function(r, e) { + throw new Error("Cannot import wasm module without importer: " + r + " " + e); +}; +function tr(r) { + er = r; +} +let Ln = async function(r, e) { + const t = (m) => import(m), { readFileSync: n } = await t("fs"), i = new URL(r, e); + return await n(i).buffer; +}; +const An = typeof process < "u" && process.versions != null && process.versions.node != null; +An && tr(Ln); +const Ye = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + CreateSessionOptions: Qt, + IncrDomDocClient: xe, + PageInfo: ue, + PagesInfo: ke, + RenderPageImageOptions: le, + RenderSession: N, + RenderSessionOptions: kn, + TypstRenderer: Re, + TypstRendererBuilder: Rn, + TypstWorker: Pn, + WorkerBridge: Ee, + default: Zt, + initSync: Mn, + renderer_build_info: yn, + setImportWasmModule: tr +}, Symbol.toStringTag, { value: "Module" })); +let g; +const K = new Array(128).fill(void 0); +K.push(void 0, null, !0, !1); +function b(r) { + return K[r]; +} +let _e = K.length; +function v(r) { + _e === K.length && K.push(K.length + 1); + const e = _e; + return _e = K[e], K[e] = r, e; +} +function Z(r, e) { + try { + return r.apply(this, e); + } catch (t) { + g.__wbindgen_export_0(v(t)); + } +} +const rr = typeof TextDecoder < "u" ? new TextDecoder("utf-8", { ignoreBOM: !0, fatal: !0 }) : { decode: () => { + throw Error("TextDecoder not available"); +} }; +typeof TextDecoder < "u" && rr.decode(); +let oe = null; +function re() { + return (oe === null || oe.byteLength === 0) && (oe = new Uint8Array(g.memory.buffer)), oe; +} +function V(r, e) { + return r = r >>> 0, rr.decode(re().subarray(r, r + e)); +} +function Tn(r) { + r < 132 || (K[r] = _e, _e = r); +} +function I(r) { + const e = b(r); + return Tn(r), e; +} +let T = 0; +const be = typeof TextEncoder < "u" ? new TextEncoder("utf-8") : { encode: () => { + throw Error("TextEncoder not available"); +} }, On = typeof be.encodeInto == "function" ? function(r, e) { + return be.encodeInto(r, e); +} : function(r, e) { + const t = be.encode(r); + return e.set(t), { + read: r.length, + written: t.length + }; +}; +function B(r, e, t) { + if (t === void 0) { + const a = be.encode(r), d = e(a.length, 1) >>> 0; + return re().subarray(d, d + a.length).set(a), T = a.length, d; + } + let n = r.length, i = e(n, 1) >>> 0; + const s = re(); + let o = 0; + for (; o < n; o++) { + const a = r.charCodeAt(o); + if (a > 127) break; + s[i + o] = a; + } + if (o !== n) { + o !== 0 && (r = r.slice(o)), i = t(i, n, n = o + r.length * 3, 1) >>> 0; + const a = re().subarray(i + o, i + n), d = On(r, a); + o += d.written, i = t(i, n, o, 1) >>> 0; + } + return T = o, i; +} +let X = null; +function S() { + return (X === null || X.buffer.detached === !0 || X.buffer.detached === void 0 && X.buffer !== g.memory.buffer) && (X = new DataView(g.memory.buffer)), X; +} +function z(r) { + return r == null; +} +const xt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => { + g.__wbindgen_export_4.get(r.dtor)(r.a, r.b); +}); +function Fn(r, e, t, n) { + const i = { a: r, b: e, cnt: 1, dtor: t }, s = (...o) => { + i.cnt++; + const a = i.a; + i.a = 0; + try { + return n(a, i.b, ...o); + } finally { + --i.cnt === 0 ? (g.__wbindgen_export_4.get(i.dtor)(a, i.b), xt.unregister(i)) : i.a = a; + } + }; + return s.original = i, xt.register(s, i, i), s; +} +function Ke(r) { + const e = typeof r; + if (e == "number" || e == "boolean" || r == null) + return `${r}`; + if (e == "string") + return `"${r}"`; + if (e == "symbol") { + const i = r.description; + return i == null ? "Symbol" : `Symbol(${i})`; + } + if (e == "function") { + const i = r.name; + return typeof i == "string" && i.length > 0 ? `Function(${i})` : "Function"; + } + if (Array.isArray(r)) { + const i = r.length; + let s = "["; + i > 0 && (s += Ke(r[0])); + for (let o = 1; o < i; o++) + s += ", " + Ke(r[o]); + return s += "]", s; + } + const t = /\[object ([^\]]+)\]/.exec(toString.call(r)); + let n; + if (t && t.length > 1) + n = t[1]; + else + return toString.call(r); + if (n == "Object") + try { + return "Object(" + JSON.stringify(r) + ")"; + } catch { + return "Object"; + } + return r instanceof Error ? `${r.name}: ${r.message} +${r.stack}` : n; +} +function nr(r, e) { + const t = e(r.length * 1, 1) >>> 0; + return re().set(r, t / 1), T = r.length, t; +} +function Dn(r, e) { + return r = r >>> 0, re().subarray(r / 1, r / 1 + e); +} +function $n(r) { + const e = g.get_font_info(v(r)); + return I(e); +} +function jn(r, e) { + r = r >>> 0; + const t = S(), n = []; + for (let i = r; i < r + 4 * e; i += 4) + n.push(I(t.getUint32(i, !0))); + return n; +} +function ge(r, e) { + const t = e(r.length * 4, 4) >>> 0, n = S(); + for (let i = 0; i < r.length; i++) + n.setUint32(t + 4 * i, v(r[i]), !0); + return T = r.length, t; +} +function Bn(r, e) { + if (!(r instanceof e)) + throw new Error(`expected instance of ${e.name}`); +} +function Wn(r, e, t) { + g.__wbindgen_export_5(r, e, v(t)); +} +function zn(r, e, t, n) { + g.__wbindgen_export_6(r, e, v(t), v(n)); +} +const kt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => g.__wbg_incrserver_free(r >>> 0, 1)); +class de { + static __wrap(e) { + e = e >>> 0; + const t = Object.create(de.prototype); + return t.__wbg_ptr = e, kt.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, kt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + g.__wbg_incrserver_free(e, 0); + } + /** + * @param {boolean} attach + */ + set_attach_debug_info(e) { + g.incrserver_set_attach_debug_info(this.__wbg_ptr, e); + } + /** + * @returns {Uint8Array | undefined} + */ + current() { + try { + const n = g.__wbindgen_add_to_stack_pointer(-16); + g.incrserver_current(n, this.__wbg_ptr); + var e = S().getInt32(n + 4 * 0, !0), t = S().getInt32(n + 4 * 1, !0); + let i; + return e !== 0 && (i = Dn(e, t).slice(), g.__wbindgen_export_1(e, t * 1, 1)), i; + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + reset() { + g.incrserver_reset(this.__wbg_ptr); + } +} +const We = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => g.__wbg_proxycontext_free(r >>> 0, 1)); +class Pe { + static __wrap(e) { + e = e >>> 0; + const t = Object.create(Pe.prototype); + return t.__wbg_ptr = e, We.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, We.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + g.__wbg_proxycontext_free(e, 0); + } + /** + * Creates a new `ProxyContext` instance. + * @param {any} context + */ + constructor(e) { + const t = g.proxycontext_new(v(e)); + return this.__wbg_ptr = t >>> 0, We.register(this, this.__wbg_ptr, this), this; + } + /** + * Returns the JavaScript this. + * @returns {any} + */ + get context() { + const e = g.proxycontext_context(this.__wbg_ptr); + return I(e); + } + /** + * A convenience function to untar a tarball and call a callback for each + * entry. + * @param {Uint8Array} data + * @param {Function} cb + */ + untar(e, t) { + try { + const s = g.__wbindgen_add_to_stack_pointer(-16), o = nr(e, g.__wbindgen_export_2), a = T; + g.proxycontext_untar(s, this.__wbg_ptr, o, a, v(t)); + var n = S().getInt32(s + 4 * 0, !0), i = S().getInt32(s + 4 * 1, !0); + if (i) + throw I(n); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } +} +const Rt = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => g.__wbg_typstcompiler_free(r >>> 0, 1)); +class Ie { + static __wrap(e) { + e = e >>> 0; + const t = Object.create(Ie.prototype); + return t.__wbg_ptr = e, Rt.register(t, t.__wbg_ptr, t), t; + } + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, Rt.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + g.__wbg_typstcompiler_free(e, 0); + } + reset() { + try { + const n = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompiler_reset(n, this.__wbg_ptr); + var e = S().getInt32(n + 4 * 0, !0), t = S().getInt32(n + 4 * 1, !0); + if (t) + throw I(e); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {any} inputs + */ + set_inputs(e) { + try { + const i = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompiler_set_inputs(i, this.__wbg_ptr, v(e)); + var t = S().getInt32(i + 4 * 0, !0), n = S().getInt32(i + 4 * 1, !0); + if (n) + throw I(t); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} path + * @param {string} content + * @returns {boolean} + */ + add_source(e, t) { + const n = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), i = T, s = B(t, g.__wbindgen_export_2, g.__wbindgen_export_3), o = T; + return g.typstcompiler_add_source(this.__wbg_ptr, n, i, s, o) !== 0; + } + /** + * @param {string} path + * @param {Uint8Array} content + * @returns {boolean} + */ + map_shadow(e, t) { + const n = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), i = T, s = nr(t, g.__wbindgen_export_2), o = T; + return g.typstcompiler_map_shadow(this.__wbg_ptr, n, i, s, o) !== 0; + } + /** + * @param {string} path + * @returns {boolean} + */ + unmap_shadow(e) { + const t = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), n = T; + return g.typstcompiler_unmap_shadow(this.__wbg_ptr, t, n) !== 0; + } + reset_shadow() { + g.typstcompiler_reset_shadow(this.__wbg_ptr); + } + /** + * @returns {string[]} + */ + get_loaded_fonts() { + try { + const i = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompiler_get_loaded_fonts(i, this.__wbg_ptr); + var e = S().getInt32(i + 4 * 0, !0), t = S().getInt32(i + 4 * 1, !0), n = jn(e, t).slice(); + return g.__wbindgen_export_1(e, t * 4, 4), n; + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} main_file_path + * @returns {string} + */ + get_ast(e) { + let t, n; + try { + const f = g.__wbindgen_add_to_stack_pointer(-16), w = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), P = T; + g.typstcompiler_get_ast(f, this.__wbg_ptr, w, P); + var i = S().getInt32(f + 4 * 0, !0), s = S().getInt32(f + 4 * 1, !0), o = S().getInt32(f + 4 * 2, !0), a = S().getInt32(f + 4 * 3, !0), d = i, _ = s; + if (a) + throw d = 0, _ = 0, I(o); + return t = d, n = _, V(d, _); + } finally { + g.__wbindgen_add_to_stack_pointer(16), g.__wbindgen_export_1(t, n, 1); + } + } + /** + * @returns {any} + */ + get_semantic_token_legend() { + try { + const i = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompiler_get_semantic_token_legend(i, this.__wbg_ptr); + var e = S().getInt32(i + 4 * 0, !0), t = S().getInt32(i + 4 * 1, !0), n = S().getInt32(i + 4 * 2, !0); + if (n) + throw I(t); + return I(e); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} offset_encoding + * @param {string | null} [file_path] + * @param {string | null} [result_id] + * @returns {object} + */ + get_semantic_tokens(e, t, n) { + try { + const w = g.__wbindgen_add_to_stack_pointer(-16), P = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), C = T; + var i = z(t) ? 0 : B(t, g.__wbindgen_export_2, g.__wbindgen_export_3), s = T, o = z(n) ? 0 : B(n, g.__wbindgen_export_2, g.__wbindgen_export_3), a = T; + g.typstcompiler_get_semantic_tokens(w, this.__wbg_ptr, P, C, i, s, o, a); + var d = S().getInt32(w + 4 * 0, !0), _ = S().getInt32(w + 4 * 1, !0), f = S().getInt32(w + 4 * 2, !0); + if (f) + throw I(_); + return I(d); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} fmt + * @param {number} diagnostics_format + * @returns {any} + */ + get_artifact(e, t) { + try { + const o = g.__wbindgen_add_to_stack_pointer(-16), a = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), d = T; + g.typstcompiler_get_artifact(o, this.__wbg_ptr, a, d, t); + var n = S().getInt32(o + 4 * 0, !0), i = S().getInt32(o + 4 * 1, !0), s = S().getInt32(o + 4 * 2, !0); + if (s) + throw I(i); + return I(n); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} main_file_path + * @param {(Array)[] | null} [inputs] + */ + set_compiler_options(e, t) { + try { + const a = g.__wbindgen_add_to_stack_pointer(-16), d = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), _ = T; + var n = z(t) ? 0 : ge(t, g.__wbindgen_export_2), i = T; + g.typstcompiler_set_compiler_options(a, this.__wbg_ptr, d, _, n, i); + var s = S().getInt32(a + 4 * 0, !0), o = S().getInt32(a + 4 * 1, !0); + if (o) + throw I(s); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} main_file_path + * @param {(Array)[] | null | undefined} inputs + * @param {string} selector + * @param {string | null} [field] + * @returns {string} + */ + query(e, t, n, i) { + let s, o; + try { + const D = g.__wbindgen_add_to_stack_pointer(-16), _r = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), ur = T; + var a = z(t) ? 0 : ge(t, g.__wbindgen_export_2), d = T; + const dr = B(n, g.__wbindgen_export_2, g.__wbindgen_export_3), lr = T; + var _ = z(i) ? 0 : B(i, g.__wbindgen_export_2, g.__wbindgen_export_3), f = T; + g.typstcompiler_query(D, this.__wbg_ptr, _r, ur, a, d, dr, lr, _, f); + var w = S().getInt32(D + 4 * 0, !0), P = S().getInt32(D + 4 * 1, !0), C = S().getInt32(D + 4 * 2, !0), F = S().getInt32(D + 4 * 3, !0), A = w, $ = P; + if (F) + throw A = 0, $ = 0, I(C); + return s = A, o = $, V(A, $); + } finally { + g.__wbindgen_add_to_stack_pointer(16), g.__wbindgen_export_1(s, o, 1); + } + } + /** + * @param {string} main_file_path + * @param {(Array)[] | null | undefined} inputs + * @param {string} fmt + * @param {number} diagnostics_format + * @returns {any} + */ + compile(e, t, n, i) { + try { + const f = g.__wbindgen_add_to_stack_pointer(-16), w = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), P = T; + var s = z(t) ? 0 : ge(t, g.__wbindgen_export_2), o = T; + const C = B(n, g.__wbindgen_export_2, g.__wbindgen_export_3), F = T; + g.typstcompiler_compile(f, this.__wbg_ptr, w, P, s, o, C, F, i); + var a = S().getInt32(f + 4 * 0, !0), d = S().getInt32(f + 4 * 1, !0), _ = S().getInt32(f + 4 * 2, !0); + if (_) + throw I(d); + return I(a); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @returns {IncrServer} + */ + create_incr_server() { + try { + const i = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompiler_create_incr_server(i, this.__wbg_ptr); + var e = S().getInt32(i + 4 * 0, !0), t = S().getInt32(i + 4 * 1, !0), n = S().getInt32(i + 4 * 2, !0); + if (n) + throw I(t); + return de.__wrap(e); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {string} main_file_path + * @param {(Array)[] | null | undefined} inputs + * @param {IncrServer} state + * @param {number} diagnostics_format + * @returns {any} + */ + incr_compile(e, t, n, i) { + try { + const f = g.__wbindgen_add_to_stack_pointer(-16), w = B(e, g.__wbindgen_export_2, g.__wbindgen_export_3), P = T; + var s = z(t) ? 0 : ge(t, g.__wbindgen_export_2), o = T; + Bn(n, de), g.typstcompiler_incr_compile(f, this.__wbg_ptr, w, P, s, o, n.__wbg_ptr, i); + var a = S().getInt32(f + 4 * 0, !0), d = S().getInt32(f + 4 * 1, !0), _ = S().getInt32(f + 4 * 2, !0); + if (_) + throw I(d); + return I(a); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } +} +const Et = typeof FinalizationRegistry > "u" ? { register: () => { +}, unregister: () => { +} } : new FinalizationRegistry((r) => g.__wbg_typstcompilerbuilder_free(r >>> 0, 1)); +class qn { + __destroy_into_raw() { + const e = this.__wbg_ptr; + return this.__wbg_ptr = 0, Et.unregister(this), e; + } + free() { + const e = this.__destroy_into_raw(); + g.__wbg_typstcompilerbuilder_free(e, 0); + } + constructor() { + try { + const i = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompilerbuilder_new(i); + var e = S().getInt32(i + 4 * 0, !0), t = S().getInt32(i + 4 * 1, !0), n = S().getInt32(i + 4 * 2, !0); + if (n) + throw I(t); + return this.__wbg_ptr = e >>> 0, Et.register(this, this.__wbg_ptr, this), this; + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + set_dummy_access_model() { + try { + const n = g.__wbindgen_add_to_stack_pointer(-16); + g.typstcompilerbuilder_set_dummy_access_model(n, this.__wbg_ptr); + var e = S().getInt32(n + 4 * 0, !0), t = S().getInt32(n + 4 * 1, !0); + if (t) + throw I(e); + } finally { + g.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * @param {any} context + * @param {Function} mtime_fn + * @param {Function} is_file_fn + * @param {Function} real_path_fn + * @param {Function} read_all_fn + * @returns {Promise} + */ + set_access_model(e, t, n, i, s) { + const o = g.typstcompilerbuilder_set_access_model(this.__wbg_ptr, v(e), v(t), v(n), v(i), v(s)); + return I(o); + } + /** + * @param {any} context + * @param {Function} real_resolve_fn + * @returns {Promise} + */ + set_package_registry(e, t) { + const n = g.typstcompilerbuilder_set_package_registry(this.__wbg_ptr, v(e), v(t)); + return I(n); + } + /** + * @param {Uint8Array} data + * @returns {Promise} + */ + add_raw_font(e) { + const t = g.typstcompilerbuilder_add_raw_font(this.__wbg_ptr, v(e)); + return I(t); + } + /** + * @param {Array} fonts + * @returns {Promise} + */ + add_web_fonts(e) { + const t = g.typstcompilerbuilder_add_web_fonts(this.__wbg_ptr, v(e)); + return I(t); + } + /** + * @returns {Promise} + */ + build() { + const e = this.__destroy_into_raw(), t = g.typstcompilerbuilder_build(e); + return I(t); + } +} +async function Nn(r, e) { + if (typeof Response == "function" && r instanceof Response) { + if (typeof WebAssembly.instantiateStreaming == "function") + try { + return await WebAssembly.instantiateStreaming(r, e); + } catch (n) { + if (r.headers.get("Content-Type") != "application/wasm") + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", n); + else + throw n; + } + const t = await r.arrayBuffer(); + return await WebAssembly.instantiate(t, e); + } else { + const t = await WebAssembly.instantiate(r, e); + return t instanceof WebAssembly.Instance ? { instance: t, module: r } : t; + } +} +function ir() { + const r = {}; + return r.wbg = {}, r.wbg.__wbg_buffer_609cc3eee51ed158 = function(e) { + const t = b(e).buffer; + return v(t); + }, r.wbg.__wbg_call_672a4d21634d4a24 = function() { + return Z(function(e, t) { + const n = b(e).call(b(t)); + return v(n); + }, arguments); + }, r.wbg.__wbg_call_7cccdd69e0791ae2 = function() { + return Z(function(e, t, n) { + const i = b(e).call(b(t), b(n)); + return v(i); + }, arguments); + }, r.wbg.__wbg_call_b8adc8b1d0a0d8eb = function() { + return Z(function(e, t, n, i, s) { + const o = b(e).call(b(t), b(n), b(i), b(s)); + return v(o); + }, arguments); + }, r.wbg.__wbg_done_769e5ede4b31c67b = function(e) { + return b(e).done; + }, r.wbg.__wbg_entries_3265d4158b33e5dc = function(e) { + const t = Object.entries(b(e)); + return v(t); + }, r.wbg.__wbg_error_13f62fdb4fc06f92 = function(e, t, n) { + console.error(b(e), b(t), b(n)); + }, r.wbg.__wbg_error_7534b8e9a36f1ab4 = function(e, t) { + let n, i; + try { + n = e, i = t, console.error(V(e, t)); + } finally { + g.__wbindgen_export_1(n, i, 1); + } + }, r.wbg.__wbg_getTimezoneOffset_6b5752021c499c47 = function(e) { + return b(e).getTimezoneOffset(); + }, r.wbg.__wbg_get_67b2ba62fc30de12 = function() { + return Z(function(e, t) { + const n = Reflect.get(b(e), b(t)); + return v(n); + }, arguments); + }, r.wbg.__wbg_get_b9b93047fe3cf45b = function(e, t) { + const n = b(e)[t >>> 0]; + return v(n); + }, r.wbg.__wbg_getwithrefkey_1dc361bd10053bfe = function(e, t) { + const n = b(e)[b(t)]; + return v(n); + }, r.wbg.__wbg_info_3daf2e093e091b66 = function(e) { + console.info(b(e)); + }, r.wbg.__wbg_instanceof_ArrayBuffer_e14585432e3737fc = function(e) { + let t; + try { + t = b(e) instanceof ArrayBuffer; + } catch { + t = !1; + } + return t; + }, r.wbg.__wbg_instanceof_Map_f3469ce2244d2430 = function(e) { + let t; + try { + t = b(e) instanceof Map; + } catch { + t = !1; + } + return t; + }, r.wbg.__wbg_instanceof_Object_7f2dcef8f78644a4 = function(e) { + let t; + try { + t = b(e) instanceof Object; + } catch { + t = !1; + } + return t; + }, r.wbg.__wbg_instanceof_Uint8Array_17156bcf118086a9 = function(e) { + let t; + try { + t = b(e) instanceof Uint8Array; + } catch { + t = !1; + } + return t; + }, r.wbg.__wbg_isArray_a1eab7e0d067391b = function(e) { + return Array.isArray(b(e)); + }, r.wbg.__wbg_isSafeInteger_343e2beeeece1bb0 = function(e) { + return Number.isSafeInteger(b(e)); + }, r.wbg.__wbg_iterator_9a24c88df860dc65 = function() { + return v(Symbol.iterator); + }, r.wbg.__wbg_length_a446193dc22c12f8 = function(e) { + return b(e).length; + }, r.wbg.__wbg_length_e2d2a49132c1b256 = function(e) { + return b(e).length; + }, r.wbg.__wbg_log_53ca6abb454c8644 = function(e, t, n) { + console.log(b(e), b(t), b(n)); + }, r.wbg.__wbg_new_23a2665fac83c611 = function(e, t) { + try { + var n = { a: e, b: t }, i = (o, a) => { + const d = n.a; + n.a = 0; + try { + return zn(d, n.b, o, a); + } finally { + n.a = d; + } + }; + const s = new Promise(i); + return v(s); + } finally { + n.a = n.b = 0; + } + }, r.wbg.__wbg_new_31a97dac4f10fab7 = function(e) { + const t = new Date(b(e)); + return v(t); + }, r.wbg.__wbg_new_405e22f390576ce2 = function() { + const e = new Object(); + return v(e); + }, r.wbg.__wbg_new_78feb108b6472713 = function() { + const e = new Array(); + return v(e); + }, r.wbg.__wbg_new_8a6f238a6ece86ea = function() { + const e = new Error(); + return v(e); + }, r.wbg.__wbg_new_a12002a7f91c75be = function(e) { + const t = new Uint8Array(b(e)); + return v(t); + }, r.wbg.__wbg_new_c68d7209be747379 = function(e, t) { + const n = new Error(V(e, t)); + return v(n); + }, r.wbg.__wbg_new_e3b321dcfef89fc7 = function(e) { + const t = new Uint32Array(b(e)); + return v(t); + }, r.wbg.__wbg_newnoargs_105ed471475aaf50 = function(e, t) { + const n = createCspSafeFunction(V(e, t)); + return v(n); + }, r.wbg.__wbg_newwithargs_ab6ffe8cd6c19c04 = function(e, t, n, i) { + const s = createCspSafeFunction(V(e, t), V(n, i)); + return v(s); + }, r.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(e, t, n) { + const i = new Uint8Array(b(e), t >>> 0, n >>> 0); + return v(i); + }, r.wbg.__wbg_newwithbyteoffsetandlength_f1dead44d1fc7212 = function(e, t, n) { + const i = new Uint32Array(b(e), t >>> 0, n >>> 0); + return v(i); + }, r.wbg.__wbg_next_25feadfc0913fea9 = function(e) { + const t = b(e).next; + return v(t); + }, r.wbg.__wbg_next_6574e1a8a62d1055 = function() { + return Z(function(e) { + const t = b(e).next(); + return v(t); + }, arguments); + }, r.wbg.__wbg_now_807e54c39636c349 = function() { + return Date.now(); + }, r.wbg.__wbg_proxycontext_new = function(e) { + const t = Pe.__wrap(e); + return v(t); + }, r.wbg.__wbg_push_737cfc8c1432c2c6 = function(e, t) { + return b(e).push(b(t)); + }, r.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(e) { + queueMicrotask(b(e)); + }, r.wbg.__wbg_queueMicrotask_d3219def82552485 = function(e) { + const t = b(e).queueMicrotask; + return v(t); + }, r.wbg.__wbg_resolve_4851785c9c5f573d = function(e) { + const t = Promise.resolve(b(e)); + return v(t); + }, r.wbg.__wbg_set_37837023f3d740e8 = function(e, t, n) { + b(e)[t >>> 0] = I(n); + }, r.wbg.__wbg_set_3f1d0b984ed272ed = function(e, t, n) { + b(e)[I(t)] = I(n); + }, r.wbg.__wbg_set_65595bdd868b3009 = function(e, t, n) { + b(e).set(b(t), n >>> 0); + }, r.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { + return Z(function(e, t, n) { + return Reflect.set(b(e), b(t), b(n)); + }, arguments); + }, r.wbg.__wbg_stack_0ed75d68575b0f3c = function(e, t) { + const n = b(t).stack, i = B(n, g.__wbindgen_export_2, g.__wbindgen_export_3), s = T; + S().setInt32(e + 4 * 1, s, !0), S().setInt32(e + 4 * 0, i, !0); + }, r.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() { + const e = typeof global > "u" ? null : global; + return z(e) ? 0 : v(e); + }, r.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() { + const e = typeof globalThis > "u" ? null : globalThis; + return z(e) ? 0 : v(e); + }, r.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() { + const e = typeof self > "u" ? null : self; + return z(e) ? 0 : v(e); + }, r.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() { + const e = typeof window > "u" ? null : window; + return z(e) ? 0 : v(e); + }, r.wbg.__wbg_then_44b73946d2fb3e7d = function(e, t) { + const n = b(e).then(b(t)); + return v(n); + }, r.wbg.__wbg_typstcompiler_new = function(e) { + const t = Ie.__wrap(e); + return v(t); + }, r.wbg.__wbg_value_cd1ffa7b1ab794f1 = function(e) { + const t = b(e).value; + return v(t); + }, r.wbg.__wbindgen_as_number = function(e) { + return +b(e); + }, r.wbg.__wbindgen_bigint_from_i64 = function(e) { + return v(e); + }, r.wbg.__wbindgen_bigint_from_u64 = function(e) { + const t = BigInt.asUintN(64, e); + return v(t); + }, r.wbg.__wbindgen_bigint_get_as_i64 = function(e, t) { + const n = b(t), i = typeof n == "bigint" ? n : void 0; + S().setBigInt64(e + 8 * 1, z(i) ? BigInt(0) : i, !0), S().setInt32(e + 4 * 0, !z(i), !0); + }, r.wbg.__wbindgen_boolean_get = function(e) { + const t = b(e); + return typeof t == "boolean" ? t ? 1 : 0 : 2; + }, r.wbg.__wbindgen_cb_drop = function(e) { + const t = I(e).original; + return t.cnt-- == 1 ? (t.a = 0, !0) : !1; + }, r.wbg.__wbindgen_closure_wrapper16165 = function(e, t, n) { + const i = Fn(e, t, 1133, Wn); + return v(i); + }, r.wbg.__wbindgen_debug_string = function(e, t) { + const n = Ke(b(t)), i = B(n, g.__wbindgen_export_2, g.__wbindgen_export_3), s = T; + S().setInt32(e + 4 * 1, s, !0), S().setInt32(e + 4 * 0, i, !0); + }, r.wbg.__wbindgen_error_new = function(e, t) { + const n = new Error(V(e, t)); + return v(n); + }, r.wbg.__wbindgen_in = function(e, t) { + return b(e) in b(t); + }, r.wbg.__wbindgen_is_bigint = function(e) { + return typeof b(e) == "bigint"; + }, r.wbg.__wbindgen_is_function = function(e) { + return typeof b(e) == "function"; + }, r.wbg.__wbindgen_is_object = function(e) { + const t = b(e); + return typeof t == "object" && t !== null; + }, r.wbg.__wbindgen_is_string = function(e) { + return typeof b(e) == "string"; + }, r.wbg.__wbindgen_is_undefined = function(e) { + return b(e) === void 0; + }, r.wbg.__wbindgen_jsval_eq = function(e, t) { + return b(e) === b(t); + }, r.wbg.__wbindgen_jsval_loose_eq = function(e, t) { + return b(e) == b(t); + }, r.wbg.__wbindgen_memory = function() { + const e = g.memory; + return v(e); + }, r.wbg.__wbindgen_number_get = function(e, t) { + const n = b(t), i = typeof n == "number" ? n : void 0; + S().setFloat64(e + 8 * 1, z(i) ? 0 : i, !0), S().setInt32(e + 4 * 0, !z(i), !0); + }, r.wbg.__wbindgen_number_new = function(e) { + return v(e); + }, r.wbg.__wbindgen_object_clone_ref = function(e) { + const t = b(e); + return v(t); + }, r.wbg.__wbindgen_object_drop_ref = function(e) { + I(e); + }, r.wbg.__wbindgen_string_get = function(e, t) { + const n = b(t), i = typeof n == "string" ? n : void 0; + var s = z(i) ? 0 : B(i, g.__wbindgen_export_2, g.__wbindgen_export_3), o = T; + S().setInt32(e + 4 * 1, o, !0), S().setInt32(e + 4 * 0, s, !0); + }, r.wbg.__wbindgen_string_new = function(e, t) { + const n = V(e, t); + return v(n); + }, r.wbg.__wbindgen_throw = function(e, t) { + throw new Error(V(e, t)); + }, r; +} +function sr(r, e) { + return g = r.exports, or.__wbindgen_wasm_module = e, X = null, oe = null, g; +} +function Un(r) { + if (g !== void 0) return g; + typeof r < "u" && (Object.getPrototypeOf(r) === Object.prototype ? { module: r } = r : console.warn("using deprecated parameters for `initSync()`; pass a single object instead")); + const e = ir(); + r instanceof WebAssembly.Module || (r = new WebAssembly.Module(r)); + const t = new WebAssembly.Instance(r, e); + return sr(t, r); +} +async function or(r) { + if (g !== void 0) return g; + typeof r < "u" && (Object.getPrototypeOf(r) === Object.prototype ? { module_or_path: r } = r : console.warn("using deprecated parameters for the initialization function; pass a single object instead")), typeof r > "u" && (r = ar("typst_ts_web_compiler_bg.wasm", import.meta.url)); + const e = ir(); + (typeof r == "string" || typeof Request == "function" && r instanceof Request || typeof URL == "function" && r instanceof URL) && (r = fetch(r)); + const { instance: t, module: n } = await Nn(await r, e); + return sr(t, n); +} +let ar = async function(r, e) { + throw new Error("Cannot import wasm module without importer: " + r + " " + e); +}; +function cr(r) { + ar = r; +} +let Hn = async function(r, e) { + const t = (m) => import(m), { readFileSync: n } = await t("fs"), i = new URL(r, e); + return await n(i).buffer; +}; +const Vn = typeof process < "u" && process.versions != null && process.versions.node != null; +Vn && cr(Hn); +const Me = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + IncrServer: de, + ProxyContext: Pe, + TypstCompiler: Ie, + TypstCompilerBuilder: qn, + default: or, + get_font_info: $n, + initSync: Un, + setImportWasmModule: cr +}, Symbol.toStringTag, { value: "Module" })); +export { + we as $typst, + vr as FetchAccessModel, + Ne as FetchPackageRegistry, + U as TypstSnippet, + zt as createTypstCompiler, + nt as createTypstRenderer, + ni as createTypstSvgRenderer, + ne as preloadRemoteFonts, + Ze as preloadSystemFonts, + ii as rendererBuildInfo +}; diff --git a/app/web_ui.py b/app/web_ui.py index 726c9c8..aa7e2c5 100644 --- a/app/web_ui.py +++ b/app/web_ui.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -"""Read-only HTTPS administration UI for the AD-integrated file server.""" +"""HTTPS administration UI for the AD-integrated file server.""" import base64 import datetime as dt +import fcntl import hashlib import hmac import http.cookies @@ -59,6 +60,12 @@ STATIC_ROOT = os.getenv("WEB_STATIC_DIR", "/app/web") STATE_DB = STATE_DB_PATH BACKUP_STATUS_FILE = os.getenv("BACKUP_STATUS_FILE", "/state/backup-status.json") BACKUP_LOG_FILE = os.getenv("BACKUP_LOG_FILE", "/var/log/backup.log") +BACKUP_LOCK_FILE = "/state/backup.lock" +RECONCILE_STATUS_FILE = os.getenv( + "RECONCILE_STATUS_FILE", "/state/reconcile-status.json" +) +RECONCILE_LOG_FILE = os.getenv("RECONCILE_LOG_FILE", "/var/log/reconcile.log") +RECONCILE_LOCK_FILE = "/state/reconcile.lock" TLS_CERT_FILE = os.getenv("WEB_TLS_CERT_FILE", "/state/tls/web.crt") TLS_KEY_FILE = os.getenv("WEB_TLS_KEY_FILE", "/state/tls/web.key") JWT_COOKIE = "adfs_session" @@ -68,6 +75,7 @@ DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") SID_RE = re.compile(r"S-\d+(?:-\d+)+", re.IGNORECASE) LOGIN_LIMIT: Dict[str, deque] = {} LOGIN_LIMIT_LOCK = threading.Lock() +ACTION_LAUNCH_LOCK = threading.Lock() def log(message: str) -> None: @@ -85,6 +93,81 @@ def env_int(name: str, default: int, minimum: int, maximum: int) -> int: return default +def env_bool(name: str, default: bool) -> bool: + raw = os.getenv(name) + if raw is None or not raw.strip(): + return default + return raw.strip().casefold() in {"1", "true", "yes", "on"} + + +def query_includes_log(params: Dict[str, List[str]]) -> bool: + raw = params.get("log", ["1"])[0].strip().casefold() + return raw not in {"0", "false", "no", "off"} + + +def lock_is_held(path: str) -> bool: + try: + with open(path, "r+", encoding="utf-8") as handle: + try: + fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + return True + fcntl.flock(handle, fcntl.LOCK_UN) + except OSError: + return False + return False + + +class ActionConflict(RuntimeError): + pass + + +def launch_background(command: List[str], extra_env: Dict[str, str]) -> None: + environment = os.environ.copy() + environment.update(extra_env) + try: + process = subprocess.Popen( + command, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + env=environment, + close_fds=True, + start_new_session=True, + ) + except OSError as exc: + raise RuntimeError(f"Aktion konnte nicht gestartet werden: {exc}") from exc + threading.Thread(target=process.wait, name="action-reaper", daemon=True).start() + + +def start_backup_action(username: str) -> Dict[str, object]: + if not os.getenv("BACKUP_DESTINATION", "").strip(): + raise ActionConflict("Es ist kein Sicherungsziel eingerichtet") + if not os.getenv("BACKUP_ARCHIVE_PASSWORD", ""): + raise ActionConflict("BACKUP_ARCHIVE_PASSWORD ist nicht gesetzt") + with ACTION_LAUNCH_LOCK: + if lock_is_held(BACKUP_LOCK_FILE): + raise ActionConflict("Eine Sicherung läuft bereits") + launch_background( + [sys.executable, "/app/backup_to_destination.py"], + {"BACKUP_TRIGGER": "web"}, + ) + log(f"{username} started a manual backup") + return {"accepted": True, "action": "backup"} + + +def start_reconciliation_action(username: str) -> Dict[str, object]: + with ACTION_LAUNCH_LOCK: + if lock_is_held(RECONCILE_LOCK_FILE): + raise ActionConflict("Ein Freigabenabgleich läuft bereits") + launch_background( + [sys.executable, "/app/reconcile_shares.py"], + {"RECONCILE_TRIGGER": "web"}, + ) + log(f"{username} started a manual share reconciliation") + return {"accepted": True, "action": "reconciliation"} + + def read_json(path: str, default): try: with open(path, encoding="utf-8") as handle: @@ -662,11 +745,39 @@ def tail_lines(path: str, count: int) -> List[str]: return [] -def backup_payload() -> Dict[str, object]: +def backup_payload(include_log: bool = True) -> Dict[str, object]: value = read_json(BACKUP_STATUS_FILE, {}) - value["enabled"] = bool(os.getenv("BACKUP_DESTINATION", "").strip()) + value.pop("log", None) + configured = bool(os.getenv("BACKUP_DESTINATION", "").strip()) + automatic = configured and env_bool("BACKUP_AUTO_ENABLED", True) + value["enabled"] = configured + value["manualEnabled"] = configured + value["automaticEnabled"] = automatic value["scheduledHour"] = env_int("BACKUP_START_HOUR", 2, 0, 23) - value["log"] = tail_lines(BACKUP_LOG_FILE, 100) + if "state" not in value: + value["state"] = "waiting" if configured else "disabled" + value["percent"] = 0.0 + if include_log: + value["log"] = tail_lines(BACKUP_LOG_FILE, 100) + return value + + +def reconciliation_payload(include_log: bool = True) -> Dict[str, object]: + value = read_json(RECONCILE_STATUS_FILE, {}) + value.pop("log", None) + if "state" not in value: + value.update( + { + "state": "waiting", + "phase": "waiting", + "percent": 0.0, + "message": "No reconciliation has run yet", + } + ) + value["automaticEnabled"] = True + value["scheduledIntervalMinutes"] = 5 + if include_log: + value["log"] = tail_lines(RECONCILE_LOG_FILE, 150) return value @@ -728,15 +839,46 @@ class App: recent = query_audit({"limit": ["12"]}) return {"usage": usage, "activeGroups": share_count(), "recentEvents": recent["events"], "eventCount": recent["matched"], "backup": backup_payload(), "audit": audit_archive_summary()} - def system(self) -> Dict[str, object]: + def system_summary(self) -> Dict[str, object]: checks = {} - for name, command in {"domainTrust": ["wbinfo", "-t"], "sambaConfig": ["testparm", "-s"]}.items(): + commands = { + "domainTrust": ["wbinfo", "-t"], + "sambaConfig": ["testparm", "-s"], + } + for name, command in commands.items(): try: - result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10, check=False) + result = subprocess.run( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=10, + check=False, + ) checks[name] = result.returncode == 0 except (OSError, subprocess.TimeoutExpired): checks[name] = False - return {"hostname": os.getenv("WEB_HOSTNAME", ""), "checks": checks, "tls": tls_summary(), "audit": audit_archive_summary(), "usage": self.usage.snapshot(), "serverTime": now_utc().isoformat(timespec="seconds")} + return { + "hostname": os.getenv("WEB_HOSTNAME", ""), + "checks": checks, + "tls": tls_summary(), + "serverTime": now_utc().isoformat(timespec="seconds"), + } + + def system(self) -> Dict[str, object]: + value = self.system_summary() + value["audit"] = audit_archive_summary() + value["usage"] = self.usage.snapshot() + return value + + def report(self) -> Dict[str, object]: + """Return every reportable snapshot without reading either log.""" + return { + "generatedAt": now_utc().isoformat(timespec="seconds"), + "groups": self.directory.get(), + "storage": self.usage.snapshot(), + "backup": backup_payload(include_log=False), + "system": self.system_summary(), + } APP: Optional[App] = None @@ -748,13 +890,18 @@ class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args) -> None: log(f"{self.client_address[0]} {fmt % args}") - def security_headers(self) -> None: + def security_headers(self, cache_control: str = "no-store") -> None: self.send_header("Strict-Transport-Security", "max-age=31536000; includeSubDomains") - self.send_header("Content-Security-Policy", "default-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'") + self.send_header( + "Content-Security-Policy", + "default-src 'self'; connect-src 'self'; img-src 'self' data:; " + "style-src 'self'; script-src 'self' 'wasm-unsafe-eval'; " + "base-uri 'none'; frame-ancestors 'none'; form-action 'self'", + ) self.send_header("X-Content-Type-Options", "nosniff") self.send_header("Referrer-Policy", "no-referrer") self.send_header("Permissions-Policy", "camera=(), microphone=(), geolocation=()") - self.send_header("Cache-Control", "no-store") + self.send_header("Cache-Control", cache_control) def send_json(self, value: object, status: int = 200, cookie: Optional[str] = None) -> None: body = json.dumps(value, separators=(",", ":")).encode() @@ -836,6 +983,25 @@ class Handler(BaseHTTPRequestHandler): cookie = f"{JWT_COOKIE}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Strict" self.send_json({"ok": True}, cookie=cookie) return + if parsed.path.startswith("/api/actions/"): + user = self.require_user() + if user is None: + return + try: + if parsed.path == "/api/actions/backup": + result = start_backup_action(str(user["sub"])) + elif parsed.path == "/api/actions/reconciliation": + result = start_reconciliation_action(str(user["sub"])) + else: + self.send_error_json(HTTPStatus.NOT_FOUND, "Nicht gefunden") + return + self.send_json(result, HTTPStatus.ACCEPTED) + except ActionConflict as exc: + self.send_error_json(HTTPStatus.CONFLICT, str(exc)) + except RuntimeError as exc: + log(f"Action {parsed.path} failed: {exc}") + self.send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) + return self.send_error_json(HTTPStatus.NOT_FOUND, "Nicht gefunden") def do_GET(self) -> None: # pylint: disable=invalid-name,too-many-return-statements @@ -861,9 +1027,17 @@ class Handler(BaseHTTPRequestHandler): elif path == "/api/activity": self.send_json(query_audit(params)) elif path == "/api/backup": - self.send_json(backup_payload()) + self.send_json( + backup_payload(include_log=query_includes_log(params)) + ) + elif path == "/api/reconciliation": + self.send_json( + reconciliation_payload(include_log=query_includes_log(params)) + ) elif path == "/api/system": self.send_json(APP.system()) + elif path == "/api/report": + self.send_json(APP.report()) else: self.send_error_json(HTTPStatus.NOT_FOUND, "Nicht gefunden") except (ValueError, OSError, RuntimeError) as exc: @@ -875,7 +1049,12 @@ class Handler(BaseHTTPRequestHandler): def serve_static(self, path: str) -> None: files = { "/assets/app.js": ("app.js", "text/javascript; charset=utf-8"), + "/assets/report.mjs": ("report.mjs", "text/javascript; charset=utf-8"), "/assets/styles.css": ("styles.css", "text/css; charset=utf-8"), + "/assets/vendor/typst/0.6.0-csp1/typst.mjs": ("vendor/typst/typst.mjs", "text/javascript; charset=utf-8"), + "/assets/vendor/typst/0.6.0-csp1/compiler.wasm": ("vendor/typst/compiler.wasm", "application/wasm"), + "/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Regular.otf": ("vendor/typst/LibertinusSerif-Regular.otf", "font/otf"), + "/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Semibold.otf": ("vendor/typst/LibertinusSerif-Semibold.otf", "font/otf"), "/favicon.svg": ("favicon.svg", "image/svg+xml"), } if path in files: @@ -889,7 +1068,12 @@ class Handler(BaseHTTPRequestHandler): self.send_error_json(HTTPStatus.NOT_FOUND, "Statische Datei nicht gefunden") return self.send_response(HTTPStatus.OK) - self.security_headers() + cache_control = ( + "public, max-age=31536000, immutable" + if path.startswith("/assets/vendor/typst/") + else "no-store" + ) + self.security_headers(cache_control) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.end_headers() diff --git a/dev/backup.Dockerfile b/dev/backup.Dockerfile index fd5ed4e..fedf982 100644 --- a/dev/backup.Dockerfile +++ b/dev/backup.Dockerfile @@ -3,7 +3,7 @@ FROM debian:12-slim ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ - && apt-get install -y --no-install-recommends rsync tini \ + && apt-get install -y --no-install-recommends p7zip-full rsync tini \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /backup diff --git a/dev/e2e.py b/dev/e2e.py index 56dcbd9..38701d6 100755 --- a/dev/e2e.py +++ b/dev/e2e.py @@ -159,10 +159,51 @@ def main() -> int: check(b"[hidden]" in styles.body and b"display: none !important" in styles.body, "hidden views can be made visible by author CSS") script = http("/assets/app.js") check(b"getUTCHours()" in script.body and b" UTC`" in script.body, "UI does not format timestamps explicitly in UTC") + check( + b"Sicherung jetzt starten" in script.body + and b"Freigaben jetzt abgleichen" in script.body + and b'href="/reconciliation"' in index.body, + "manual actions or the top-level reconciliation navigation are missing", + ) check(b"localTime" not in script.body and b"eyebrow" not in script.body, "obsolete local-time or decorative UI code remains") + report_script = http("/assets/report.mjs") + check( + report_script.status == 200 + and b"compiler.pdf({mainContent:" in report_script.body + and b"getUTCHours()" in report_script.body, + "client-side Typst report module is missing or does not use UTC", + ) + typst_script = http("/assets/vendor/typst/0.6.0-csp1/typst.mjs") + check( + typst_script.status == 200 and b"TypstSnippet" in typst_script.body, + "vendored Typst browser wrapper is missing", + ) + typst_wasm = http("/assets/vendor/typst/0.6.0-csp1/compiler.wasm") + check( + typst_wasm.status == 200 + and typst_wasm.body.startswith(b"\x00asm") + and typst_wasm.headers.get("Content-Type") == "application/wasm", + "vendored Typst compiler WASM is missing or has the wrong MIME type", + ) + check( + "immutable" in typst_wasm.headers.get("Cache-Control", ""), + "large immutable Typst assets are not browser-cacheable", + ) + typst_font = http("/assets/vendor/typst/0.6.0-csp1/LibertinusSerif-Regular.otf") + check( + typst_font.status == 200 + and typst_font.body.startswith(b"OTTO") + and typst_font.headers.get("Content-Type") == "font/otf", + "vendored Typst report font is missing or has the wrong MIME type", + ) check(b"brand-mark" not in index.body, "decorative brand mark remains") check("max-age=" in index.headers.get("Strict-Transport-Security", ""), "HSTS missing") - check("default-src 'self'" in index.headers.get("Content-Security-Policy", ""), "CSP missing") + csp = index.headers.get("Content-Security-Policy", "") + check("default-src 'self'" in csp, "CSP missing") + check( + "script-src 'self' 'wasm-unsafe-eval'" in csp and "'unsafe-eval'" not in csp, + "CSP does not narrowly permit the local WebAssembly compiler", + ) with socket.create_connection(("localhost", HTTPS_PORT), timeout=10) as raw: with TLS_CONTEXT.wrap_socket(raw, server_hostname=HTTPS_HOST) as secured: certificate = secured.getpeercert() @@ -173,6 +214,14 @@ def main() -> int: announce("JWT boundary, real Kerberos credentials, and Domain Admin authorization") unauthenticated = http("/api/session") check(unauthenticated.status == 401, "protected API accepted an anonymous request") + check( + http("/api/actions/backup", method="POST", value={}).status == 401, + "anonymous backup action was accepted", + ) + check( + http("/api/actions/reconciliation", method="POST", value={}).status == 401, + "anonymous reconciliation action was accepted", + ) non_admin = http( "/api/login", method="POST", @@ -386,6 +435,95 @@ def main() -> int: ) check(marker.returncode == 0, "backup target has no completed snapshot marker") + announce("encrypted non-solid per-group archives at the backup target") + archive_check = engine_run( + "exec", + BACKUP_CONTAINER, + "sh", + "-ec", + """ +archive=$(find /backup/fileserver/snapshots -path '*/data/groups/data/Finance.7z' -type f | sort | tail -n 1) +test -n "$archive" +archive_dir=${archive%/*} +archive_count=$(find "$archive_dir" -maxdepth 1 -type f -name '*.7z' | wc -l | tr -d ' ') +test "$archive_count" -eq 3 +test ! -d "$archive_dir/Finance" +listing=$(printf '%s\n' "$PREVIEW_ARCHIVE_PASSWORD" | 7z l -slt "$archive") +printf '%s\n' "$listing" | grep -q '^Solid = -$' +printf '%s\n' "$listing" | grep -q '^Encrypted = +$' +if printf '%s\n' 'wrong archive password' | 7z l -slt "$archive" >/dev/null 2>&1; then + exit 1 +fi +""", + check_result=False, + ) + check( + archive_check.returncode == 0, + "group archive is missing, solid, unencrypted, or accepted a wrong password: " + + (archive_check.stderr.strip() or archive_check.stdout.strip()), + ) + + announce("authenticated manual backup action and terminal status") + manual_backup_start = eventually( + "manual backup action acceptance", + lambda: http("/api/actions/backup", method="POST", value={}, token=token), + lambda response: response.status == 202, + timeout=30, + ) + check( + manual_backup_start.json().get("action") == "backup", + "manual backup action returned the wrong payload", + ) + manual_backup = eventually( + "manual web backup completion", + lambda: http("/api/backup", token=token), + lambda response: ( + response.status == 200 + and response.json().get("trigger") == "web" + and response.json().get("state") in {"completed", "failed"} + ), + timeout=240, + interval=2, + ).json() + check(manual_backup.get("state") == "completed", f"manual web backup failed: {manual_backup}") + check(float(manual_backup.get("percent", 0)) == 100.0, "manual web backup is not at 100%") + + announce("authenticated reconciliation action, progress status, and live log") + reconciliation_start = eventually( + "manual reconciliation action acceptance", + lambda: http("/api/actions/reconciliation", method="POST", value={}, token=token), + lambda response: response.status == 202, + timeout=30, + ) + check( + reconciliation_start.json().get("action") == "reconciliation", + "manual reconciliation action returned the wrong payload", + ) + reconciliation = eventually( + "manual reconciliation completion", + lambda: http("/api/reconciliation", token=token), + lambda response: ( + response.status == 200 + and response.json().get("trigger") == "web" + and response.json().get("state") in {"completed", "failed"} + ), + timeout=240, + interval=1, + ).json() + check( + reconciliation.get("state") == "completed", + f"manual reconciliation failed: {reconciliation}", + ) + check( + float(reconciliation.get("percent", 0)) == 100.0 + and reconciliation.get("phase") == "completed", + "completed reconciliation status is incomplete", + ) + check( + any("completed" in line.casefold() for line in reconciliation.get("log", [])), + "reconciliation completion is absent from the live log", + ) + announce("overview and system health aggregation") overview = http("/api/overview", token=token) check(overview.status == 200 and overview.json().get("activeGroups") == 3, "overview group count is wrong") @@ -397,6 +535,35 @@ def main() -> int: check(system_payload.get("tls", {}).get("sans"), "system TLS summary is empty") check(system_payload.get("audit", {}).get("days", 0) >= 1, "system activity database summary is incomplete") + announce("log-free PDF report snapshot") + report = http("/api/report", token=token) + check(report.status == 200, f"report endpoint failed: {report.body!r}") + report_payload = report.json() + check( + set(report_payload) == {"generatedAt", "groups", "storage", "backup", "system"}, + f"report snapshot has unexpected sections: {sorted(report_payload)}", + ) + check("log" not in report_payload["backup"], "backup log leaked into report snapshot") + check("audit" not in report_payload["system"], "activity metadata leaked into report snapshot") + check("usage" not in report_payload["system"], "duplicate usage leaked into report snapshot") + check( + report_payload["groups"].get("groups") == groups_payload.get("groups"), + "report membership hierarchy differs from the group API", + ) + report_totals = report_payload["storage"].get("totals", {}) + check( + all(int(report_totals.get(field, 0)) > 0 for field in ( + "dataBytes", + "privateBytes", + "fslogixBytes", + )), + "report storage snapshot is incomplete", + ) + check( + report_payload["backup"].get("state") == backup_payload.get("state"), + "report backup status differs from the backup API", + ) + logout = http("/api/logout", method="POST", value={}, token=token) check(logout.status == 200 and "Max-Age=0" in logout.headers.get("Set-Cookie", ""), "logout did not clear session cookie") diff --git a/scripts/dev b/scripts/dev index 7fb16cd..3f3bf6b 100755 --- a/scripts/dev +++ b/scripts/dev @@ -41,6 +41,8 @@ dev_admin_user=${DEV_ADMIN_USER:-previewadmin} dev_admin_password=${DEV_ADMIN_PASSWORD:-PreviewAdmin123!} dev_backup_user=${DEV_BACKUP_USER:-preview} dev_backup_password=${DEV_BACKUP_PASSWORD:-PreviewBackup123!} +dev_archive_password=${DEV_ARCHIVE_PASSWORD:-PreviewArchive123!} +dev_backup_auto_enabled=${DEV_BACKUP_AUTO_ENABLED:-true} dev_ca_password=${DEV_CA_PASSWORD:-PreviewCa123!} dev_ca_provisioner=${DEV_CA_PROVISIONER:-preview} dev_https_port=${DEV_HTTPS_PORT:-8443} @@ -250,6 +252,7 @@ printf 'starting disposable rsync backup target\n' --ip "$backup_ip" \ -e "BACKUP_USERNAME=${dev_backup_user}" \ -e "BACKUP_PASSWORD=${dev_backup_password}" \ + -e "PREVIEW_ARCHIVE_PASSWORD=${dev_archive_password}" \ -v "$backup_volume:/backup" \ "$backup_image" >/dev/null @@ -326,6 +329,8 @@ printf 'starting actual file server and HTTPS web UI\n' -e "WEB_DIRECTORY_CACHE_SECONDS=30" \ -e "AUDIT_POLL_SECONDS=0.25" \ -e "BACKUP_DESTINATION=rsync://${dev_backup_user}:${dev_backup_password}@backup.${dev_dns_domain}/backups/fileserver" \ + -e "BACKUP_ARCHIVE_PASSWORD=${dev_archive_password}" \ + -e "BACKUP_AUTO_ENABLED=${dev_backup_auto_enabled}" \ -e "BACKUP_PROGRESS=never" \ -e "BACKUP_PROGRESS_INTERVAL_SECONDS=1" \ -e "BACKUP_RETENTION_DAILY=3" \ @@ -389,7 +394,7 @@ run_backup_loop() { local elapsed while "$engine" inspect "$files_container" >/dev/null 2>&1; do "$engine" exec "$files_container" /bin/bash -lc \ - 'source /app/runtime.env && exec /usr/bin/python3 /app/backup_to_destination.py' || true + 'source /app/runtime.env && BACKUP_TRIGGER=automatic exec /usr/bin/python3 /app/backup_to_destination.py' || true elapsed=0 while (( elapsed < dev_backup_interval )); do "$engine" inspect "$files_container" >/dev/null 2>&1 || return 0 diff --git a/setup b/setup index cbaf5b2..7d1d985 100755 --- a/setup +++ b/setup @@ -124,6 +124,9 @@ write_env_file() { local domain_admins_sid="" local fslogix_group_sid="" local backup_destination="" + local backup_archive_password="" + local backup_auto_enabled="true" + local backup_auto_input="" local backup_start_hour="2" local backup_retention_daily="3" local backup_retention_weekly="2" @@ -205,6 +208,14 @@ write_env_file() { fi read -r -p "BACKUP_DESTINATION (optional URL, press Enter to disable): " backup_destination + if [[ -n "$backup_destination" ]]; then + prompt_value backup_archive_password "BACKUP_ARCHIVE_PASSWORD (encrypts group archives)" true + read -r -p "Enable automatic daily backups? [Y/n]: " backup_auto_input + case "${backup_auto_input,,}" in + n|no) backup_auto_enabled="false" ;; + *) backup_auto_enabled="true" ;; + esac + fi read -r -p "BACKUP_START_HOUR [2]: " backup_start_hour backup_start_hour="${backup_start_hour:-2}" read -r -p "BACKUP_RETENTION_DAILY [3]: " backup_retention_daily @@ -294,6 +305,8 @@ AD_DNS_IP=${ad_dns_ip} AD_DNS_NAME=${ad_dns_name} AD_DNS_IP_AUTO=${ad_dns_ip_auto} BACKUP_DESTINATION=${backup_destination} +BACKUP_ARCHIVE_PASSWORD=${backup_archive_password} +BACKUP_AUTO_ENABLED=${backup_auto_enabled} BACKUP_START_HOUR=${backup_start_hour} BACKUP_RETENTION_DAILY=${backup_retention_daily} BACKUP_RETENTION_WEEKLY=${backup_retention_weekly} @@ -373,6 +386,8 @@ AD_DNS_IP=${ad_dns_ip} AD_DNS_NAME=${ad_dns_name} AD_DNS_IP_AUTO=${ad_dns_ip_auto} BACKUP_DESTINATION=${backup_destination} +BACKUP_ARCHIVE_PASSWORD=${backup_archive_password} +BACKUP_AUTO_ENABLED=${backup_auto_enabled} BACKUP_START_HOUR=${backup_start_hour} BACKUP_RETENTION_DAILY=${backup_retention_daily} BACKUP_RETENTION_WEEKLY=${backup_retention_weekly} @@ -400,6 +415,9 @@ ACME_HTTP_PORT=${acme_http_port} # BACKUP_DESTINATION=smb://DOMAIN%5Cuser:pass@backup.example.com/Backups/samba # BACKUP_DESTINATION=davfs://user:pass@webdav.example.com/remote.php/dav/files/backup # BACKUP_DESTINATION=sftp://user:pass@sftp.example.com/exports/samba +# BACKUP_ARCHIVE_PASSWORD=use-a-long-random-secret +# BACKUP_AUTO_ENABLED=true +# BACKUP_ARCHIVE_TEMP_DIR=/tmp # BACKUP_START_HOUR=2 # BACKUP_RETENTION_DAILY=3 # BACKUP_RETENTION_WEEKLY=2 diff --git a/tests/report_pdf_smoke.mjs b/tests/report_pdf_smoke.mjs new file mode 100644 index 0000000..1a07e0c --- /dev/null +++ b/tests/report_pdf_smoke.mjs @@ -0,0 +1,99 @@ +import fs from "node:fs"; + +globalThis.window = globalThis; + +const report = await import(new URL("../app/web/report.mjs", import.meta.url)); +const typst = await import(new URL("../app/web/vendor/typst/typst.mjs", import.meta.url)); +globalThis.Function = () => { + throw new Error("JavaScript dynamic code generation is disabled by CSP"); +}; +const vendor = new URL("../app/web/vendor/typst/", import.meta.url); +const binary = name => new Uint8Array(fs.readFileSync(new URL(name, vendor))); + +typst.$typst.setCompilerInitOptions({ + getModule: () => ({module_or_path: binary("compiler.wasm")}), +}); +typst.$typst.use( + typst.TypstSnippet.disableDefaultFontAssets(), + typst.TypstSnippet.preloadFontData(binary("LibertinusSerif-Regular.otf")), + typst.TypstSnippet.preloadFontData(binary("LibertinusSerif-Semibold.otf")) +); + +const data = { + generatedAt: "2026-08-01T20:27:05+00:00", + storage: { + scannedAt: "2026-08-01T20:20:00+00:00", + scanSeconds: 1.25, + totals: {dataBytes: 1048576, privateBytes: 2048, fslogixBytes: 4096}, + groups: [{name: "Forschung & Entwicklung", bytes: 1048576}], + users: [{name: "ludwig.lehnert", privateBytes: 2048, fslogixBytes: 4096, totalBytes: 6144}], + }, + groups: { + fetchedAt: "2026-08-01T20:21:00+00:00", + truncated: false, + groups: [{ + folder: "Forschung [2026] #1", + sam: "FS_Forschung", + active: true, + userCount: 1, + groupCount: 2, + members: [{ + type: "group", + name: "Domänen-Benutzer", + sam: "Domain Users", + members: [{ + type: "group", + name: "Forschung und Entwicklung – interne Projektmitglieder", + sam: "Forschung_Entwicklung_Intern", + members: [{type: "user", name: "Ludwig \"Test\" Lehnert", sam: "ludwig.lehnert", members: []}], + }], + }], + }], + }, + backup: { + enabled: true, + scheduledHour: 2, + state: "completed", + percent: 100, + transferredBytes: 1054720, + totalBytes: 1054720, + startedAt: "2026-08-01T02:00:00+00:00", + finishedAt: "2026-08-01T02:01:00+00:00", + snapshot: "2026-08-01T020000Z", + message: "Backup completed; 3 snapshot(s) retained", + activeFiles: [], + log: ["DARF NICHT IN DIE PDF"], + }, + system: { + hostname: "files.example.test", + serverTime: "2026-08-01T20:27:05+00:00", + checks: {domainTrust: true, sambaConfig: true}, + tls: { + subject: {commonName: "files.example.test"}, + issuer: {commonName: "Interne CA"}, + notAfter: "2027-08-01T00:00:00+00:00", + sans: [["DNS", "files.example.test"]], + }, + audit: {secret: "DARF EBENFALLS NICHT IN DIE PDF"}, + }, +}; + +const source = report.buildReportSource(data); +if (source.includes("DARF NICHT") || source.includes("DARF EBENFALLS NICHT")) { + throw new Error("log data leaked into Typst source"); +} +if ( + source.includes("[- ]") || + !source.includes("columns: (depth * 9pt + 8pt, 44pt, 1fr)") || + !source.includes("above: 1.8pt, below: 1.8pt") +) { + throw new Error("group hierarchy is not rendered as an indented row grid"); +} +const pdf = await typst.$typst.pdf({mainContent: source}); +if (!pdf || new TextDecoder().decode(pdf.slice(0, 5)) !== "%PDF-") { + throw new Error("Typst did not produce a PDF"); +} +if (process.env.REPORT_SMOKE_OUTPUT) { + fs.writeFileSync(process.env.REPORT_SMOKE_OUTPUT, pdf); +} +console.log("client Typst PDF smoke test passed (" + pdf.length + " bytes)"); diff --git a/tests/test_backup_to_destination.py b/tests/test_backup_to_destination.py index 72a44c2..b7f0819 100644 --- a/tests/test_backup_to_destination.py +++ b/tests/test_backup_to_destination.py @@ -2,6 +2,7 @@ import io import os import shutil import sqlite3 +import subprocess import tempfile import unittest from unittest import mock @@ -188,6 +189,131 @@ class ProgressParsingTests(unittest.TestCase): self.assertEqual(total, 8) +class GroupArchiveTests(unittest.TestCase): + def test_archive_password_is_required_and_rejects_control_characters(self): + with mock.patch.dict(os.environ, {}, clear=True): + with self.assertRaisesRegex(RuntimeError, "BACKUP_ARCHIVE_PASSWORD"): + backup.archive_password() + + with mock.patch.dict( + os.environ, {"BACKUP_ARCHIVE_PASSWORD": "secret\nsecond-line"}, clear=True + ): + with self.assertRaisesRegex(RuntimeError, "unsupported characters"): + backup.archive_password() + + def test_groups_are_staged_as_encrypted_non_solid_archives(self): + with tempfile.TemporaryDirectory() as tmpdir: + source_root = os.path.join(tmpdir, "groups") + os.makedirs(os.path.join(source_root, "data", "Finance")) + os.makedirs(os.path.join(source_root, "archive", "Former")) + os.makedirs(os.path.join(source_root, "metadata")) + with open( + os.path.join(source_root, "data", "Finance", "report.txt"), + "w", + encoding="utf-8", + ) as handle: + handle.write("finance") + with open( + os.path.join(source_root, "archive", "Former", "old.txt"), + "w", + encoding="utf-8", + ) as handle: + handle.write("former") + with open( + os.path.join(source_root, "data", "README.txt"), + "w", + encoding="utf-8", + ) as handle: + handle.write("preserve me") + with open( + os.path.join(source_root, "metadata", "index.txt"), + "w", + encoding="utf-8", + ) as handle: + handle.write("metadata") + + commands = [] + + def fake_run(command, **kwargs): + commands.append((command, kwargs)) + archive_path = command[-3] + with open(archive_path, "wb") as handle: + handle.write(b"dummy-7z") + return backup.subprocess.CompletedProcess(command, 0, "", "") + + staged_temp = None + try: + with mock.patch.object(backup, "run_command", side_effect=fake_run): + staged_temp, staged_root, count = backup.prepare_group_archives( + source_root, "archive secret", tmpdir + ) + + self.assertEqual(count, 2) + self.assertTrue( + os.path.isfile(os.path.join(staged_root, "data", "Finance.7z")) + ) + self.assertTrue( + os.path.isfile(os.path.join(staged_root, "archive", "Former.7z")) + ) + self.assertTrue( + os.path.isfile(os.path.join(staged_root, "data", "README.txt")) + ) + self.assertTrue( + os.path.isfile(os.path.join(staged_root, "metadata", "index.txt")) + ) + self.assertFalse( + os.path.exists(os.path.join(staged_root, "data", "Finance")) + ) + + self.assertEqual(len(commands), 2) + for command, kwargs in commands: + self.assertEqual(command[:3], ["7z", "a", "-t7z"]) + self.assertIn("-m0=lzma2", command) + self.assertIn("-mx=5", command) + self.assertIn("-mmt=on", command) + self.assertIn("-ms=off", command) + self.assertIn("-mhe=on", command) + self.assertIn("-p", command) + self.assertNotIn("archive secret", command) + self.assertEqual(kwargs["input_text"], "archive secret\n") + self.assertFalse(kwargs["check"]) + self.assertTrue(os.path.isdir(kwargs["cwd"])) + finally: + if staged_temp is not None: + shutil.rmtree(staged_temp, ignore_errors=True) + + @unittest.skipUnless(shutil.which("7z"), "7z is needed for the archive smoke test") + def test_real_archive_is_encrypted_and_non_solid(self): + with tempfile.TemporaryDirectory() as tmpdir: + source = os.path.join(tmpdir, "Finance") + archive_path = os.path.join(tmpdir, "Finance.7z") + os.mkdir(source) + with open(os.path.join(source, "report.txt"), "w", encoding="utf-8") as handle: + handle.write("classified") + + backup.create_group_archive(source, archive_path, "correct secret") + + correct = subprocess.run( + [shutil.which("7z"), "l", "-slt", archive_path], + input="correct secret\n", + capture_output=True, + text=True, + check=False, + ) + wrong = subprocess.run( + [shutil.which("7z"), "l", "-slt", archive_path], + input="wrong secret\n", + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(correct.returncode, 0, correct.stdout + correct.stderr) + self.assertIn("Solid = -", correct.stdout) + self.assertIn("Path = Finance/report.txt", correct.stdout) + self.assertNotEqual(wrong.returncode, 0) + + class StateSnapshotTests(unittest.TestCase): def test_online_snapshot_includes_wal_commits_and_other_state(self): with tempfile.TemporaryDirectory() as tmpdir: diff --git a/tests/test_reconcile_shares.py b/tests/test_reconcile_shares.py index fc29dc9..cdf3e75 100644 --- a/tests/test_reconcile_shares.py +++ b/tests/test_reconcile_shares.py @@ -1,4 +1,5 @@ import base64 +import json import os import sqlite3 import tempfile @@ -30,6 +31,30 @@ def principal(dn, sam, classes, members=(), member_of=(), sid=""): } +class ReconcileStatusTests(unittest.TestCase): + def test_status_tracks_web_trigger_progress_and_completion_in_utc(self): + with tempfile.TemporaryDirectory() as tmpdir: + status_path = os.path.join(tmpdir, "reconcile-status.json") + status = rs.ReconcileStatus(status_path) + + status.begin("web") + status.progress(57.5, "data-permissions", "Syncing permissions", "FS_Finance") + status.complete("Reconciliation completed") + + with open(status_path, encoding="utf-8") as handle: + payload = json.load(handle) + + self.assertEqual(payload["state"], "completed") + self.assertEqual(payload["trigger"], "web") + self.assertEqual(payload["phase"], "completed") + self.assertEqual(payload["percent"], 100.0) + self.assertEqual(payload["message"], "Reconciliation completed") + self.assertIsNone(payload["currentItem"]) + self.assertRegex(payload["startedAt"], r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$") + self.assertRegex(payload["finishedAt"], r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$") + self.assertFalse(os.path.exists(f"{status_path}.tmp")) + + class GroupFolderNameTests(unittest.TestCase): def test_sanitizer_preserves_leading_dot(self): self.assertEqual(rs.sanitize_group_folder_name(".Finance"), ".Finance") diff --git a/tests/test_web_ui.py b/tests/test_web_ui.py index 8d92596..6f5cd3b 100644 --- a/tests/test_web_ui.py +++ b/tests/test_web_ui.py @@ -1,6 +1,8 @@ import datetime as dt import os +import shutil import sqlite3 +import subprocess import tempfile import unittest from unittest import mock @@ -34,6 +36,167 @@ class TokenManagerTests(unittest.TestCase): web_ui.TokenManager("short", 600) +class ReportPayloadTests(unittest.TestCase): + @mock.patch("app.web_ui.backup_payload") + def test_report_snapshot_excludes_all_log_data(self, backup_payload): + backup_payload.return_value = {"state": "completed"} + app = mock.Mock() + app.directory.get.return_value = {"groups": [], "fetchedAt": None} + app.usage.snapshot.return_value = {"groups": [], "users": [], "totals": {}} + app.system_summary.return_value = { + "hostname": "files.example.test", + "checks": {}, + "tls": {}, + "serverTime": "2026-08-01T12:00:00+00:00", + } + + payload = web_ui.App.report(app) + + backup_payload.assert_called_once_with(include_log=False) + self.assertNotIn("log", payload["backup"]) + self.assertNotIn("audit", payload["system"]) + self.assertNotIn("usage", payload["system"]) + self.assertEqual(payload["system"]["hostname"], "files.example.test") + + @mock.patch("app.web_ui.tail_lines") + @mock.patch("app.web_ui.read_json") + def test_log_free_backup_snapshot_does_not_read_log_file(self, read_json, tail_lines): + read_json.return_value = { + "state": "completed", + "log": ["GEHEIME SICHERUNGSAUSGABE"], + } + + payload = web_ui.backup_payload(include_log=False) + + self.assertNotIn("log", payload) + tail_lines.assert_not_called() + + +class AdministrationActionTests(unittest.TestCase): + def test_log_query_flag_defaults_on_and_accepts_explicit_off_values(self): + self.assertTrue(web_ui.query_includes_log({})) + self.assertTrue(web_ui.query_includes_log({"log": ["1"]})) + for value in ("0", "false", "NO", "off"): + self.assertFalse(web_ui.query_includes_log({"log": [value]})) + + @mock.patch("app.web_ui.tail_lines", return_value=["backup log"]) + @mock.patch("app.web_ui.read_json", return_value={}) + def test_backup_payload_exposes_manual_mode_when_automatic_runs_are_off( + self, _read_json, _tail_lines + ): + with mock.patch.dict( + os.environ, + { + "BACKUP_DESTINATION": "rsync://backup.example.test/target", + "BACKUP_AUTO_ENABLED": "false", + "BACKUP_START_HOUR": "4", + }, + clear=True, + ): + payload = web_ui.backup_payload() + + self.assertTrue(payload["enabled"]) + self.assertTrue(payload["manualEnabled"]) + self.assertFalse(payload["automaticEnabled"]) + self.assertEqual(payload["scheduledHour"], 4) + self.assertEqual(payload["state"], "waiting") + self.assertEqual(payload["log"], ["backup log"]) + + @mock.patch("app.web_ui.tail_lines", return_value=["reconcile log"]) + @mock.patch("app.web_ui.read_json", return_value={}) + def test_reconciliation_payload_has_progress_schedule_and_log( + self, _read_json, _tail_lines + ): + payload = web_ui.reconciliation_payload() + + self.assertEqual(payload["state"], "waiting") + self.assertEqual(payload["phase"], "waiting") + self.assertEqual(payload["percent"], 0.0) + self.assertTrue(payload["automaticEnabled"]) + self.assertEqual(payload["scheduledIntervalMinutes"], 5) + self.assertEqual(payload["log"], ["reconcile log"]) + + @mock.patch("app.web_ui.tail_lines") + @mock.patch("app.web_ui.read_json", return_value={"state": "completed"}) + def test_log_free_reconciliation_snapshot_does_not_read_log_file( + self, _read_json, tail_lines + ): + payload = web_ui.reconciliation_payload(include_log=False) + + self.assertNotIn("log", payload) + tail_lines.assert_not_called() + + @mock.patch("app.web_ui.log") + @mock.patch("app.web_ui.launch_background") + @mock.patch("app.web_ui.lock_is_held", return_value=False) + def test_manual_backup_launches_with_web_trigger( + self, _lock_is_held, launch_background, _log + ): + with mock.patch.dict( + os.environ, + { + "BACKUP_DESTINATION": "rsync://backup.example.test/target", + "BACKUP_ARCHIVE_PASSWORD": "archive secret", + }, + clear=True, + ): + result = web_ui.start_backup_action("EXAMPLE\\alice") + + self.assertEqual(result, {"accepted": True, "action": "backup"}) + launch_background.assert_called_once_with( + [web_ui.sys.executable, "/app/backup_to_destination.py"], + {"BACKUP_TRIGGER": "web"}, + ) + + @mock.patch("app.web_ui.launch_background") + @mock.patch("app.web_ui.lock_is_held", return_value=False) + def test_manual_backup_requires_archive_password( + self, _lock_is_held, launch_background + ): + with mock.patch.dict( + os.environ, + {"BACKUP_DESTINATION": "rsync://backup.example.test/target"}, + clear=True, + ): + with self.assertRaisesRegex( + web_ui.ActionConflict, "BACKUP_ARCHIVE_PASSWORD" + ): + web_ui.start_backup_action("EXAMPLE\\alice") + + launch_background.assert_not_called() + + @mock.patch("app.web_ui.log") + @mock.patch("app.web_ui.launch_background") + @mock.patch("app.web_ui.lock_is_held", return_value=False) + def test_manual_reconciliation_launches_with_web_trigger( + self, _lock_is_held, launch_background, _log + ): + result = web_ui.start_reconciliation_action("EXAMPLE\\alice") + + self.assertEqual(result, {"accepted": True, "action": "reconciliation"}) + launch_background.assert_called_once_with( + [web_ui.sys.executable, "/app/reconcile_shares.py"], + {"RECONCILE_TRIGGER": "web"}, + ) + + +class ReportCompilerTests(unittest.TestCase): + @unittest.skipUnless(shutil.which("node"), "Node.js is needed for the browser Typst smoke test") + def test_vendored_browser_typst_compiles_report_to_pdf(self): + root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + result = subprocess.run( + [shutil.which("node"), os.path.join(root, "tests", "report_pdf_smoke.mjs")], + cwd=root, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=30, + ) + self.assertEqual(result.returncode, 0, result.stdout) + self.assertIn("Typst PDF smoke test passed", result.stdout) + + class DomainAuthenticationTests(unittest.TestCase): @mock.patch.dict(os.environ, {"WORKGROUP": "EXAMPLE"}) def test_bare_username_defaults_to_configured_netbios_domain(self): @@ -462,6 +625,8 @@ class WebPresentationTests(unittest.TestCase): self.assertNotIn("nav-group", html) self.assertIn('>Datenbelegung', html) self.assertIn('>Benutzerbelegung', html) + self.assertIn('href="/reconciliation" data-route="reconciliation">Freigabenabgleich', html) + self.assertIn('href="/report" data-route="report">PDF-Bericht', html) self.assertNotIn("brand-mark", html) self.assertNotIn("eyebrow", html + script) self.assertIn("getUTCHours()", script) @@ -482,6 +647,37 @@ class WebPresentationTests(unittest.TestCase): self.assertIn(".shares-split .list", css) self.assertIn(".shares-split .tree", css) + def test_admin_actions_are_german_and_backup_automatic_setting_is_env_only(self): + script = self.asset("app.js") + + self.assertIn("Sicherung jetzt starten", script) + self.assertIn("Freigaben jetzt abgleichen", script) + self.assertIn('api("/api/actions/backup"', script) + self.assertIn('api("/api/actions/reconciliation"', script) + self.assertIn('includeLog ? "/api/reconciliation"', script) + self.assertIn('"/api/backup?log=0"', script) + self.assertIn('"/api/reconciliation?log=0"', script) + self.assertEqual(script.count("if (active || previousActive)"), 2) + self.assertNotIn("BACKUP_AUTO_ENABLED", script) + + def test_pdf_report_is_client_side_and_uses_only_log_free_endpoint(self): + script = self.asset("app.js") + report = self.asset("report.mjs") + typst = self.asset(os.path.join("vendor", "typst", "typst.mjs")) + + self.assertIn('api("/api/report")', script) + self.assertIn('import("/assets/report.mjs")', script) + self.assertNotIn('api("/api/activity', report) + self.assertNotIn('api("/api/backup', report) + self.assertIn('compiler.pdf({mainContent:', report) + self.assertIn('/assets/vendor/typst/0.6.0-csp1/compiler.wasm', report) + self.assertIn("getUTCHours()", report) + self.assertIn("above: 1.8pt, below: 1.8pt", report) + self.assertIn('["Automatik", backup.automaticEnabled ? "Aktiv" : "Ausgeschaltet"]', report) + self.assertIn("Aktivitätsprotokoll und Sicherungsprotokoll sind nicht Bestandteil", report) + self.assertNotIn("new Function", typst) + self.assertIn("createCspSafeFunction", typst) + def test_samba_audits_only_supported_file_operations(self): path = os.path.join(os.path.dirname(__file__), "..", "etc", "samba", "smb.conf") with open(path, encoding="utf-8") as handle: