/** * -------------------------------------------------------------------- * docmd : the zero-config documentation engine. * * @package @docmd/parser * @website https://docmd.io * @repository https://github.com/docmd-io/docmd * @license MIT * @copyright Copyright (c) 2025-present docmd.io * * [docmd-source] - Please do not remove this header. * -------------------------------------------------------------------- */ /** * Container normaliser * ==================== * * Single-pass linear scan that rewrites `:::` container markdown so that * the existing depth-tracking block rules in `features/common-containers.ts` * always see balanced open/close pairs. * * The classic bugs this addresses: * * F1 — depth tracker is indentation-blind. * `::: grids` + N×` ::: grid` + N×`:::` (one per card) * leaves depth > 0 and the block rule fails to match, so the * whole grids block is dumped as raw `

::: grids
...

`. * F2 — `::: tag` is self-closing but the next orphan `:::` still * decrements depth of the wrong container. * F3 — `::: callout ... ::: card ... :::` silently re-roots. * F4 — bare `:::` lines leak into the page as `

:::

` paragraphs. * F5 — 5+ levels of nesting survive when opens and closes are balanced, * but unbalanced user input collapses inner levels. * * The algorithm is the same one documented in * `battle-test-reports/robust-parser-shim/index.js` (146 lines, * dependency-free). This file is the in-tree port — no plugins, no * configuration, always-on. * * Output is deterministic: the function is a pure function of its input. * Two worker threads given the same source produce byte-identical output. * * ─── DETERMINISM AUDIT ───────────────────────────────────────────────── * Phase 2 (worker-shared-state fix). This module deliberately does NOT * use any of the following non-deterministic primitives. Adding any of * them is a regression and must be flagged in code review. * * ✗ `Date.now()` / `new Date()` — wall-clock time * ✗ `Math.random()` / `crypto.randomUUID` — entropy source * ✗ module-level `let` / `var` — mutable shared state * ✗ `console.log` from inside `normaliseContainers` (use the * `onWarning` callback instead — `console.log` does not affect * output but `DOCMD_ROBUST_DEBUG=1` enables it for ad-hoc tracing) * ✗ reading from `process.env` — env may differ per worker * (use `options` instead) * * The only module-level binding is `SELF_CLOSING_CONTAINER_NAMES`, a * frozen `ReadonlySet` that is constructed once at module load * and never mutated. Safe to share across workers. * * The empirical guarantee lives in three places: * 1. `packages/parser/test/container-normaliser.test.js` — replay * determinism, 100-way concurrency, and cross-worker * `node:worker_threads` determinism. * 2. `packages/core/src/engine/worker-parser.ts` boot-time self-test * (`verifyDeterminismAtBoot`). * 3. The manual end-to-end check at the bottom of this file's docstring. * ────────────────────────────────────────────────────────────────────── */ /** * Container names that produce a single line (no body, no close). * These are matched by name in the open line and the line is passed * through unchanged; any stray `:::` that follows them is a user mistake * (F2) and is removed. */ export declare const SELF_CLOSING_CONTAINER_NAMES: ReadonlySet; /** * Severity levels for normaliser warnings. Mirrors the three messages the * shim emits so downstream consumers can route by severity if they want. */ export type NormaliserWarningSeverity = 'warning' | 'info' | 'error'; export interface NormaliserWarning { /** 1-indexed line number in the original source. */ line: number; severity: NormaliserWarningSeverity; /** Path of the source file (or `` when synthetic). */ path: string; message: string; } export interface NormaliserResult { /** Rewritten source with implicit closes added and stray closes removed. */ source: string; warnings: NormaliserWarning[]; } export interface NormaliserOptions { /** Path used in warning messages. Defaults to ``. */ sourcePath?: string; /** When true, print debug lines to stdout. Defaults to false. */ debug?: boolean; /** Optional sink for warnings — useful for tests and structured logging. */ onWarning?: (warning: NormaliserWarning) => void; } interface ClassifiedLine { kind: 'open' | 'close' | 'other'; name?: string; } /** * Count the leading spaces of a line. Tabs are not interpreted — markdown * container indentation is conventionally spaces. */ export declare function indentOf(line: string): number; /** * Classify a single source line as `open`, `close`, or `other`. * * open — `::: ...` where `` starts with a letter. * Self-closing names (`button`, `tag`, `embed`) are still * classified as `open` — the algorithm distinguishes them via * the SELF_CLOSING_CONTAINER_NAMES set, not here. * close — bare `:::` with optional surrounding whitespace. * other — anything else, passed through verbatim. */ export declare function classifyLine(line: string): ClassifiedLine; /** * Rewrite a markdown source so that every `:::` block has a matching close. * * The function never throws; instead it returns the rewritten source plus * an array of warnings. Callers may surface warnings through `console.warn`, * a structured logger, or both via `options.onWarning`. * * The algorithm is allocation-conscious (single array of output lines, single * stack of open frames) but readability is prioritised over micro-optimisation. */ export declare function normaliseContainers(source: string, options?: NormaliserOptions | string): NormaliserResult; export default normaliseContainers;