Skip to content

Source API Scope ​

The Rule API is a source-level extension surface for contributors building SnapTeX. It is not a package that a .tex project imports, and it is not a runtime plugin API loaded from user files.

Where extension code lives ​

Declare, import, and register custom behavior in src/rules.ts. The active registry is exported from that file as SNAP_TEX_RULES:

ts
export const SNAP_TEX_RULES = defineRuleRegistry({
    metadataExtractors: [/* document metadata readers */],
    renderRules: [/* legacy source rules */],
    astRenderRules: [/* AST node rules */],
    astMathRules: [/* selected commands nested inside AST math */],
    blockDependencyRules: [/* external invalidation */],
    splitterConfig: DEFAULT_SPLITTER_CONFIG,
    splitterRules: [/* block boundaries */]
});

The registry is a lifecycle snapshot shared by LatexDocument and SmartRenderer. Changing a declaration requires rebuilding SnapTeX and fully reloading the preview.

How to read an API page ​

Each page describes one of three kinds of callable value:

KindHow it becomes availableExample
Imported functionImport it from the definition module into src/rules.tsreplaceLatexCommandCalls, escapeHtml
Rule callbackSnapTeX invokes a function registered directly or stored on a rule objectAstRenderRule, PreprocessRule.apply
Context methodSnapTeX creates a context and passes it into a callbackrenderer.protectHtml, context.renderMath

When an example says apply: (text, renderer) => ..., SnapTeX supplies both parameters. When it says defineAstRenderRule((input, context) => ...), the AST walker supplies those parameters.

A nested callback has a different owner. In this example:

ts
renderInlineLatexHtml(source, tex => context.renderMath(tex, false))

your rule supplies source and the callback function; renderInlineLatexHtml later supplies tex each time it finds $...$; SnapTeX supplied context to the enclosing AST rule. Reading from the outer call inward makes ownership explicit.

Examples keep every positional interface parameter visible. An intentionally unused parameter is prefixed with _, as in apply: (text, _renderer) => text. Object inputs are different: a callback such as defineBlockDependencyRule(({ text, deps }) => ...) still receives the complete BlockDependencyInput object and merely extracts the members it uses.

Declaration is not registration ​

These two operations are intentionally separate:

ts
const RULE = defineAstRenderRule((input, context) => { /* return a result or undefined */ });

export const SNAP_TEX_RULES = defineRuleRegistry({
    // RULE starts running only after it appears here.
    astRenderRules: [RULE, ...DEFAULT_AST_RENDER_RULES],
    astMathRules: DEFAULT_AST_MATH_RULES,
    // ...the other required fields
});

defineAstRenderRule and defineBlockDependencyRule return their callbacks. They provide contextual typing; they do not mutate the global registry. defineRuleRegistry creates the lifecycle snapshot consumed by the document and renderer.

Definition and import locations ​

These are repository-internal module paths, relative to src/rules.ts:

API areaDefinition moduleTypical import
Registry functions and defaultssrc/rules.tsAlready local
Shared rule typessrc/types.ts./types
Balanced source readers and escapingsrc/utils.ts./utils
Legacy/shared rendering helperssrc/rule-helpers.ts./rule-helpers
AST contracts and argument readerssrc/ast/rules/index.ts./ast/rules
AST node readerssrc/ast/visit-utils.ts./ast/visit-utils
Metadata readerssrc/metadata.ts./metadata
End-to-end update servicesrc/preview-update-service.tsUsed from tests/hosts rather than registered

Use a type import for TypeScript-only contracts and a normal import for functions used at runtime. Existing imports in src/rules.ts should be extended instead of creating a second alias for the same helper.

Output contracts ​

Legacy and AST rules produce output differently:

PathRule outputSafety requirement
LegacyTransformed source that later passes through MarkdownEscape source-controlled text and hide generated HTML with renderer.protectHtml
ASTFinal HTML for the claimed nodeEscape source-controlled text and use context methods for math, references, citations, and images

Do not return source-controlled text inside HTML without escaping it. Do not call legacy protectHtml from AST rules because AST output bypasses Markdown.

Minimal verification ​

Use PreviewUpdateService to test the same parse, scan, dependency, and rendering path used by hosts:

ts
/// <reference types="mocha" />
import * as assert from 'assert';
import { BrowserUri } from '../../apps/standalone/src/browser-file-provider';
import { MemoryFileProvider } from './test-helpers';
import { PreviewUpdateService } from '../preview-update-service';
import { SNAP_TEX_RULES } from '../rules';

const uri = new BrowserUri('/main.tex');
const source = '\\begin{document}\nExpected output.\n\\end{document}';
const service = new PreviewUpdateService(
    new MemoryFileProvider(),
    SNAP_TEX_RULES
);

test('renders expected output', async () => {
    const payload = await service.render(uri, source, {
        backendMode: 'legacy',
        deferFullHtml: false
    });

    const html = payload.htmls?.join('') ?? '';
    assert.match(html, /Expected output/);
});

The relative imports above assume a test file directly under src/test/; adjust them to your file location. MemoryFileProvider is the repository test helper from src/test/test-helpers.ts, using BrowserUri so tests need no VS Code process. Production hosts supply their own IFileProvider.

Select only the backend containing the rule you changed. Assert final HTML or payload behavior rather than the existence of a declaration.

Next references ​

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