/**
* --------------------------------------------------------------------
* 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;