Skip to content

Architecture ​

SnapTeX separates project/editor integration, host-neutral document processing, and browser preview display. Read the diagram from top to bottom: hosts provide source and settings, the core returns serializable updates, and the preview runtime owns visible DOM.

text
apps/vscode ───────────────────┐
                              ├─> PreviewUpdateService
apps/web -> apps/standalone ───┘      │
                                      ├─> LatexDocument
                                      ├─> scanner / diff
                                      └─> SmartRenderer
                                              │
                                              v
                                      serializable RenderPayload
                                              │
                                              v
                                  shared browser preview runtime
                                  DOM / virtual shells / PDF / TikZ

Data crosses these boundaries through interfaces and serializable payloads. The core never reaches into an editor UI, and the preview runtime never reads project files directly.

Request flow ​

  1. A host implements IFileProvider, selects a root URI, and calls PreviewUpdateService with source and settings.
  2. The service updates LatexDocument and SmartRenderer using one RuleRegistry snapshot.
  3. The renderer returns a full or patch RenderPayload.
  4. The host sends that payload through the shared preview message protocol.
  5. The preview runtime mounts or patches DOM and sends sync, tooltip, resource, and lazy-block requests back to the host.

This flow is the same in VS Code and Web. Their differences are file access, editor events, settings UI, and transport adapters.

Entry points ​

RuntimeStart reading atDelegates to
VS Code extension activationapps/vscode/src/extension.tsCommands and PreviewPanel
VS Code preview hostapps/vscode/src/panel.tsPreviewUpdateService and webview messages
Web applicationapps/web/src/main.tsStandalone app plus browser project adapters
Standalone editor/preview servicesapps/standalone/src/app.tsCore service and project operations
Optional remote-project serverapps/web/server.mjsAuthenticated project API and static assets
Host-neutral preview lifecyclesrc/preview-update-service.tsLatexDocument and SmartRenderer
Browser preview runtimesrc/webview/main.tsDOM patching, virtualization, resources, and sync

Start at the entry point for the observed behavior, then follow calls toward the owner in the state table below.

State ownership ​

StateLong-lived ownerCrosses a boundary as
Project files and writable operationsHost file provider/project backendURI, text, binary resource, or operation result
Root source, included files, metadata, block spans, compact source-map segmentsLatexDocumentDocument state read by renderer/service
Block render snapshots, dependencies, citation stateSmartRendererRenderPayload and lazy block results
Active editor file, selection, dirty stateVS Code or standalone hostSync/update requests
Mounted HTML and measured heightsPreview runtimeSync/resource messages and diagnostics

No layer should keep a second authoritative copy of state owned elsewhere. Cached hashes, HTML, and shell heights are derived state with explicit invalidation or lifetime rules.

Core: src/ ​

The core has no direct VS Code UI ownership. Its main responsibilities are:

  • parse preamble metadata and project dependencies;
  • split the source into block spans;
  • scan counters, labels, and citations;
  • diff block hashes and dependency fingerprints;
  • render requested blocks;
  • define host/preview message contracts;
  • generate the shared preview HTML template.

LatexDocument owns source text, spans, hashes, metadata, diagnostics, compact source-map segments, and AST artifacts. SmartRenderer owns render rules, protected HTML, cached block snapshots, citation numbering, and patch/full render payloads.

The source map is an internal compact representation rather than an extension API. Consecutive flattened lines from the same source file are stored as one segment. Public document methods translate between original and flattened lines, so callers should use those methods instead of depending on segment storage.

PreviewUpdateService is the host-facing coordinator. Hosts should call it instead of separately driving document parsing and rendering, and tests should use it when verifying complete update behavior.

Standalone host: apps/standalone/ ​

The standalone layer connects the core to a browser editor without committing to a particular storage backend. It owns:

  • CodeMirror setup and LaTeX assistance;
  • preview host orchestration;
  • browser-compatible URI and file-provider primitives;
  • generic project operations and project-tree helpers;
  • ZIP export.

Web and a future Android wrapper can reuse this layer.

Web host: apps/web/ ​

The Web app owns browser UI and browser/server storage adapters:

  • split panes, Explorer, menus, settings, welcome page, and dialogs;
  • local directory handles;
  • IndexedDB workspace contents and shared per-project root, active-file, and history state;
  • remote project API client;
  • static/PWA build;
  • authenticated Node project server and deployment scripts.

These concerns do not belong in the core because they depend on browser APIs, DOM controls, HTTP, or Linux deployment.

VS Code host: apps/vscode/ ​

The extension adapter owns activation, commands, settings, editor events, workspace file access, webview creation, and VS Code reveal behavior. It passes the shared core URI-like values and message payloads rather than exposing VS Code APIs to the renderer.

Shared preview runtime: src/webview/ and media/ ​

The preview runtime runs in both VS Code webviews and the standalone Web app. A host bridge abstracts postMessage. The runtime owns patches, virtualization, scroll/sync UI, tooltips, PDF canvases, and TikZ scheduling.

Stable boundaries ​

The main contracts are:

  • IFileProvider for project reads;
  • RuleRegistry for rendering extensions;
  • RenderPayload for full and patch updates;
  • HostToPreviewMessage / PreviewToHostMessage for runtime communication;
  • BrowserProject for Web storage backends.

BrowserProject owns storage capabilities rather than UI. Writable adapters provide file operations; adapters backed by an independently editable source may additionally provide watchFiles. StandaloneHost consumes those changes using its saved text as the three-way merge base, while the remote Web adapter maps HTTP manifest revisions and ETags onto that host-neutral contract.

watchFiles(onTextChange, onError, onResourceChange) keeps text updates and lazy binary-resource replacement separate. Both change callbacks may return a promise; adapters wait for it before acknowledging the update and retain failed updates for retry. Retries read current source content, not cached failed snapshots. Acknowledgment means text was applied/merged or a resource descriptor was replaced, not that DOM rendering succeeded. Preview resource failures are isolated to the affected image/PDF; unexpected render failures are diagnosed and reset the renderer for its next request, rather than requeueing delivered text.

The resource callback supplies a new BrowserProjectFile. For requested resources, the host reads all bytes before replacing the last usable URL and sends ResourceChanged to update matching images/PDF canvases and invalidate their HTML/height caches. Shell caches retain only resource paths when offscreen HTML is released. Untouched resources remain lazy. Transient reads reject the callback so the existing watcher retries current source data; missing files or denied access invalidate the resource without retries. Decoding/rendering errors after a completed read do not requeue file delivery. An open PDF also refreshes when its PDF or SyncTeX sidecar changes. compilePdf returns the generated PDF and any sidecar through the same file-provider boundary; it does not provide a query API.

PDF synchronization lives in apps/standalone/src/pdf-sync.ts (loading, errors, Worker lifetime and messages), pdf-sync-worker.ts (query dispatch), and pdf-sync-index.ts (official parser and project-safe path mapping). The Web UI holds one session, without separate readiness/error state. Queries belong to that session rather than repeating the PDF path. The PDF viewer converts DOM positions to PDF coordinates; the shared host retains direction ownership, hidden-pane targets, and editor scrolling. A PDF session has one index and releases it when closed or replaced. TeX preview continues using its existing block maps and AST hints.

Manual saving, auto save, and external text updates share one host queue. Auto save uses a trailing edit delay (autoSaveDelaySeconds, default 1); every edit resets one timer, and a queued save is skipped while a newer edit's timer is pending. It does not poll files or automatically reschedule after a completed save. Manual saves and project/file switches flush through the same queue. Each adapter implements writeText(text, expectedText), where expectedText is the last saved source text, not the editor's current contents. A stale write throws ProjectWriteConflictError with the actual source text for merging. The host applies external edits as editor transactions rather than replacing the document; if a queued notification outlives a save, it reads the backing source again without replacing the unsaved preview cache.

New hosts should implement these boundaries instead of importing another host's UI layer.

Dependency direction ​

Solid arrows are production import directions. The dashed Web-to-server edge is an optional runtime API call; the static Web build does not import the Node server.

  • src/ must not import VS Code or Web UI modules.
  • apps/standalone/ may use browser/editor primitives but must not assume one Web storage backend.
  • apps/web/ may depend on standalone services and browser/server adapters.
  • apps/vscode/ adapts VS Code directly and does not route through the Web UI.
  • src/webview/ and media/ are shared by both hosts and communicate only through the preview bridge/protocol.

When a change would reverse one of these arrows, move the reusable behavior toward the shared boundary and keep platform calls in the adapter.

Static dependency audit ​

The current production import graph is acyclic. AST modules depend on the foundation layer, while only the lifecycle composition modules import AST services; AST code does not import LatexDocument, SmartRenderer, or a host. Likewise, the shared preview runtime imports protocol contracts but never the document or renderer objects that created a payload.

The highest fan-in modules are deliberate foundations:

ModuleWhy many modules depend on itReview rule
src/utils.tsShared text, URI, HTML, scheduling, and balanced LaTeX readersReuse an existing helper before adding another parser or host utility
src/types.tsSerializable document, rendering, sync, and registry contractsKeep host objects and DOM nodes out of these types
src/ast/rules/index.tsAST rule contract and shared argument/render helpersKeep concrete built-in rules in their rule modules
src/ast/visit-utils.tsStructural AST predicates and traversalAdd only parser-shape operations that several AST consumers share

Large fan-out composition files such as src/renderer.ts, src/document.ts, and src/ast/rules/defaults.ts are expected to know many lower-level services. That is not a reason to move their orchestration into those dependencies.

Next ​

  • Follow the data through Rendering Pipeline.
  • For source-level LaTeX support, choose an entry point in the Extension Model.
  • For a host-specific problem, return to the entry-point table and trace only that host's adapter.

A fast structural preview, not a replacement for a full TeX compiler.