All notable changes to @awacloud/md will be documented in this file.

The format is based on Keep a Changelog, and this package adheres to Semantic Versioning.

[Unreleased]

[1.0.0] - 2026-10-07

Added

  • Core CommonMark + GFM reader/writer for the browser, with zero npm runtime dependency beyond @awacloud/fw (workspace) :

    • Typed AST (document, heading, paragraph, list, item, block_quote, code_block, thematic_break, html_block, table, table_row, table_cell, emph, strong, link, image, code, text, softbreak, linebreak, html_inline, strikethrough, …)
      • Walker iterator (entering / leaving events).
    • Block parser (CommonMark §3-§5) — an orchestrator over the mdBlock* sub-module descriptors (block-starts, block-types, cursor, html-patterns, link-ref, list-data, GFM table, task-list).
    • Inline parser (CommonMark §6) — an orchestrator over the mdInline* sub-module descriptors (escapes, code-span, delimiter-stack for emphasis §6.4, link, autolink + autolink-ext for the GFM extended autolinks, line-break, sourcepos, regex, helpers), with a parser-builder that binds the per-instance options.
    • Renderers : renderHtml / render (HTML), renderMarkdown (roundtrip-safe for CommonMark + GFM), renderXml (modelled on cmark --to xml).
    • Source positions (1-indexed [[line, col], [line, col]]) on every block node, and on inline nodes with the sourcepos option.
    • AST manipulation API : replaceNode, wrapNode, flattenNode, findFirst, findAll, cloneNode.
  • Typed errors — MdError / ParseError / RenderError / ContractError. Every throw uses a stable kebab-case code and optional structured context. Error codes documented in docs/api/errors.md :

    • md/parse-not-string — ContractError (input non-string)
    • md/use-bad-extension — ContractError (md.use with bad shape)
    • md/render-invalid-root — RenderError (renderers)
    • md/replace-node-no-parent, md/wrap-node-no-parent, md/flatten-node-no-parent — ContractError (ast/manipulation)
    • md/limit-exceeded — ContractError (parse-time guards)
    • md/parse-error — ContractError (internal block-parser invariant)
  • Pathological-input guards — createMd({ maxDepth, maxNodes, maxUrlLength }) (defaults 1000 / 100000 / 8192). Exceeding any throws ContractError('md/limit-exceeded', …) with structured context. Pass Infinity to opt out.

  • Sanitization consumed via @awacloud/fw/dom/rendering/sanitize.js. createMd({ sanitize: true }) pipes the rendered HTML through the fw sanitizer with strict defaults. Override with sanitizeOpts (allowedTags, allowedAttributes, urlSchemes, dropDangerousContent).

  • Extras (opt-in, 10 modules) loadable via .use(ext) :

    • extra/frontmatter — YAML / TOML / JSON frontmatter extraction.
    • extra/math — LaTeX inline $…$ + block ```math.
    • extra/footnotes — Pandoc [^label] + [^label]: definition.
    • extra/wikilinks — [[Page]], [[Page|Label]], [[Page#anchor]].
    • extra/admonitions — GitHub [!NOTE] + MkDocs !!! note.
    • extra/highlight — ==text== → <mark>.
    • extra/subsuper — H~2~O subscript + x^2^ superscript.
    • extra/toc — [[TOC]] placeholder replaced by a table of contents.
    • extra/emoji — :smile: shortcodes (default table + custom).
    • extra/mermaid — render-time rewrite of ```mermaid fences into <div class="mermaid">.
  • Bundles :

    • @awacloud/md/md-full — the mdFullBundle factory descriptor: it declares md + the 10 extras as dependencies and installs every extra in a deterministic order (idempotent). The core has no wrapper descriptor: @awacloud/md/md is the md facade descriptor itself.
    • Pre-built, two-surface dist/ for each assembly root (md, md-full) : dist/build/<root>.{js,min.js,meta.json} (fw-mode, the fw dependencies declared) and dist/standalone/<root>.{js,min.js,meta.json} (framework-free, every factory inlined), plus the dist/build/index.js barrel. Generated by tools/generate-bundles.mjs (bun run gen:bundles, a driver over @awacloud/tool-prebuild-generator) and exposed through the ./build/* and ./standalone/* sub-paths.
  • _shared/mdShared factory for helper deduplication across extras — returns the canonical stateless helpers escapeHtml, unescapeHtml, escapeForRegex and a createLocalWalker(BLOCK, INLINE) factory building a parameterised WalkerLocal class (exported, not called by any module yet). Consumed by extra/{admonitions,footnotes,math,subsuper}.js (escapeHtml), extra/mermaid.js (unescapeHtml) and extra/frontmatter.js (escapeForRegex). Worker-safe renderer factories (render/html.js, render/xml.js) deliberately keep their own inline WalkerLocal / escapeHtml copies instead, since a worker-safe factory body must close only over its own scope.

  • Extension hook .use({ name, install(md) }) aligned with the markdown-it / remark ecosystem. Idempotent on name.

  • Documentation under docs/ : a top-level index, an API reference (one page per source module, plus the extras and bundles) and the user guides (getting started, read/write, extending, coverage, html-document).

  • Architecture — CommonMark + GFM in core (no opt-in needed for the standard surface; only extensions beyond the spec are extras); two-phase parser (CommonMark §3 — block then inline); canonical delimiter stack (§6.4) for emphasis; renderers always work from the AST, never re-parse the text; every factory is worker-safe and self-contained; browser-only (string, Uint8Array, TextEncoder, TextDecoder); zero external dependency beyond @awacloud/fw (workspace) — htmlEntities (HTML5 table), url.encodeSafe (CommonMark URL percent-encoder), and sanitizeHtml (XSS hardening) are consumed from @awacloud/fw rather than re-implemented locally.

  • Package surface — package.json exports :

    .                src/main.js                 the four descriptor arrays + every descriptor
    ./modules        src/main.js                 same file, explicit sub-path
    ./md             src/md.js                   the md facade descriptor
    ./md-full        src/bundles/md-full.js      core + the extras pre-wired
    ./bootstrap.js   src/bootstrap.js            bootstrapMd(opts)
    ./extra/*.js     src/extra/*.js              one extra, .js-suffixed form
    ./extra/*        src/extra/*.js              one extra, extension-less form
    ./bundles/*      src/bundles/*.js            one bundle by file name
    ./build/*        dist/build/*                pre-built fw-mode bundles
    ./standalone/*   dist/standalone/*           pre-built framework-free bundles
    

    awa.maturity: "L4".

  • Tests — the suite (bun test packages/front/office/md/ from the repository root) includes the official CommonMark 0.31.2 spec examples (tests/commonmark-suite.test.js), a hand-curated set of GFM cases (tests/gfm-suite.test.js) and the spec-version lockfile (tests/spec-version.test.js, asserting the CommonMark spec.txt header is 0.31.2 / 652 examples — bumps require manual review). Also covered : roundtrip integration (tests/roundtrip.integration.test.js), fuzz / malformed input + deep-nesting (tests/fuzz.test.js), sanitize integration (tests/sanitize.test.js).

  • mdHtmlTheme (src/document/theme.js) — the embedded stylesheet of the single-HTML document builder: light / dark / auto themes over one shared rules body, shell layout, .md-body reading styles, responsive + print rules. Documented at docs/api/document/theme.md.

  • mdHtmlDocument (src/document/html-document.js, renderFragment/build) — Markdown (one or several documents) → ONE self-contained, sanitised HTML document: all-level heading anchors, a sidebar and hash router in multi-document mode, a per-document table of contents, cross-document links, print CSS and a licence notice block. Every fragment passes @awacloud/fw sanitizeHtml last and unconditionally — the single security boundary, never the renderer's own safe/sanitize options. Documented at docs/api/document/html-document.md and the guide.

  • bootstrapMd(opts) (src/bootstrap.js, ./bootstrap.js export) — a runtime-bootstrap primitive: registers all four manifest arrays (fw_require, modules, extras, bundle) on an @awacloud/fw ModuleRuntime (fresh, or host-supplied) and returns lazy accessors (runtime, resolve, md, mdFull, htmlDocument) in one call. Documented at docs/api/bootstrap.md.

  • Two new error codes — md/document-bad-input and md/document-bad-option (both ContractError, thrown by mdHtmlTheme.css / mdHtmlDocument.renderFragment / .build). Documented at docs/api/errors.md.

  • mdNode.trustedHtmlInline(literal) and mdNode.trustedHtmlBlock(literal) — the public way for an extension to build an html_inline / html_block node that renderHtml keeps under the safe default. The parser never produces such a node, so Markdown text cannot forge one. The caller escapes any untrusted part of the literal. Documented at docs/api/ast/node.md.

  • renderMarkdown serialises subscript, superscript and highlight nodes (~x~, ^x^, ==x==). Every other extension node (math_*, footnote_*, admonition, extension-built html_*) is still emitted as the empty string; the round-trip guarantee covers CommonMark + GFM only. Documented at docs/api/render/markdown.md.

  • strikethrough nodes carry delimiterCount (1 for ~x~, 2 for ~~x~~), kept by cloneNode.

  • mdHtmlDocument: FragmentOptions.reservedIds (ids a heading may not take) and a markdown flag on the links.resolve target (true for a .md target). Documented at docs/api/document/html-document.md.

Changed

  • Breaking — renderHtml / render are safe by default. safe is now on unless safe: false is passed (per call, or as the instance default via createMd({ safe: false }); a per-call option wins): raw HTML written in the Markdown source is stripped and javascript: / vbscript: / file: / data: URLs are neutralized (allowDataImage unchanged). The HTML the built-in extras build (admonitions, footnotes, highlight, math, sub/superscript) is kept, and so is the HTML a third-party extension builds with mdNode.trustedHtmlInline / mdNode.trustedHtmlBlock; any other html_inline / html_block node an extension builds is stripped unless safe: false. To restore the previous output — the spec-exact CommonMark raw passthrough — pass { safe: false }. sanitize still defaults to false. mdHtmlDocument output is unchanged (it renders with an explicit safe: false before its own sanitiser pass).

  • mdSubsuper is a post-parse AST pass (the shape mdHighlight uses) instead of a rewrite of the source text. A single-tilde strikethrough without whitespace (~x~) becomes a subscript node, ^x^ in text a superscript node, and both are lowered to trusted <sub> / <sup> at render time, so the extra now renders under the safe default without safe: false. Code spans, fenced code, autolinks and bare URLs keep their bytes. Documented at docs/api/extra/subsuper.md.

  • mdHtmlDocument consults links.resolve for every relative link target, not only .md ones; the target carries markdown: false for the others. A resolver that assumed .md targets only must now check target.markdown (or the path) and return null for the rest.

  • Documentation pass — the README follows the published-package skeleton (every exports key has its sub-path row, every Quick Start snippet is executed), the guides and the API pages were checked against the source in both directions (option and member names, dependency lists, return values), every documented code example registers the @awacloud/fw modules first (fw_require) and runs, and the coverage claims state their limits: the CommonMark examples all pass, the GFM extensions are covered by a curated case set, and the default HTML output is not passed through the fw sanitizer (sanitize is opt-in; safe is on by default). Package-local working files (the pre-publication checklist and the internal audit records) no longer ship, and neither do the internal references that pointed at them.

  • Dist — dist regenerated with the licence banner: every committed dist/**/*.js / .min.js opens with the package's /*! … */ legal block (content from the repository's licence matrix), each *.meta.json bytes entry is measured on the final bytes, and the builtAt timestamp is gone — bun run gen:bundles is now byte-deterministic.

  • Package contents — the npm tarball now ships NOTICE (dual licence + third-party attributions) and the commonmark.js third-party notice text (third-party/NOTICE-commonmark); the _test-runtime.js test scaffold and the package-local working files no longer ship.

  • Package scope renamed to @awacloud/md (from the legacy awa org scope), together with every scoped reference this package carries — prose, JSDoc, generator literals and the committed dist/. The generator's emitted identity literals (tools/generate-bundles.mjs package: / builtBy:) were flipped and dist/ regenerated in the same commit, so the committed bundles and their *.meta.json sidecars now declare "package": "@awacloud/md" and "builtBy": "@awacloud/tool-prebuild-generator …".

  • extra/emoji — shortcode lookup moved from a plain object to a Map. DEFAULT_EMOJI_TABLE keeps its public shape (a plain Record<string, string>, still mergeable via { ...DEFAULT_EMOJI_TABLE, custom: '🎯' }); only the internal lookup changed. Observable behaviour change: a shortcode whose name collides with an Object.prototype member (:constructor:, :toString:, :valueOf:, :hasOwnProperty:) is now correctly a miss and is left verbatim in the document, where the old table[key] indexing resolved it to the inherited value and stringified it into the output. expandEmojiInAst(root, table) accepts a Map as well as a plain object and returns root. Covered by src/extra/emoji.test.js (mdEmoji — Map-backed lookup).

  • Strict factory-only architecture completed across src/. Every module (ast/*, block/*, inline/*, extra/*, render/*, bundles/*, refs/linkRefs.js, common.js, errors.js, md.js, md-walker.js) exports one self-contained, worker-safe { name, dependencies, factory } descriptor — no helper, constant or singleton lives outside a factory body (the top-level imports only feed the generated deps: field of the descriptors; no factory captures them). All silent-materialization fallbacks (arg || mdXxx.factory()) were removed — factories require every dependency to be injected by the ModuleRuntime or passed explicitly, and cross-module identity (instanceof) is preserved via the runtime's shared resolution/caching. As part of the same migration, 19 sub-modules (8 block/*, 10 inline/*, ast/walker.js) were promoted to first-class factory descriptors, each declaring its complete dependency graph.

  • Modular error handling. src/errors.js no longer exports top-level error classes or an mdErrorsShared singleton — MdError / ParseError / RenderError / ContractError are declared exclusively inside the mdErrors factory body and materialised once through the runtime. src/main.js does not re-export the classes: obtain them with runtime.resolve('mdErrors') (or mdErrors.factory() for an isolated set), and every module that throws receives that same resolved instance, so instanceof agrees package-wide.

  • Performance — the TextEncoder used by common.js / encodeUrl is allocated once per factory instance instead of per non-ASCII URL character; inline/parser.js's lineStarts array is no longer built when sourcepos is disabled, avoiding an O(n) scan + allocation per parse. render/html.js keeps its own mirrored escapeHtml / encodeUrl helpers on purpose (a worker-safe factory closes only over its own scope).

  • Compatibility — the legacy createMd({ allowlist }) option now emits a console.warn (previously silently ignored) ; migration path is sanitizeOpts.allowedTags / sanitizeOpts.allowedAttributes. replaceNode, wrapNode, cloneNode raise a native TypeError on missing arguments — documented as the JS-convention for argument pre-conditions in docs/api/errors.md.

  • renderMarkdown(text) parses with the instance's md.parse, so the installed extensions apply to a string argument exactly as they do for renderHtml(text) (same AST). With extras installed, the node types renderMarkdown does not serialise (math, footnotes, admonitions) are dropped from the output of a string input, as they already were for an AST input. A core instance behaves as before.

  • Documentation: the TOC example no longer links a non-existent image; parser and theme pages reference modules, not internal milestones.

Removed

  • preprocessSubSup, the source-level rewrite mdSubsuper used to export. Use expandSubSupInAst and lowerSubSupToHtml, or install the extra with .use(mdSubsuper).

Fixed

  • The md-full bundle keeps inline math and footnote references intact. mdSubsuper is now installed after mdFootnotes and mdMath, so $x^{2}y^{3}$ stays one math span and a[^1]b[^2] keeps both footnote references (the ^…^ scan used to split them first).

  • renderMarkdown escapes a table-cell pipe exactly once: a|b is written a\|b (it was a\\\|b), also inside a code span, so the output re-parses to the same cell.

  • renderMarkdown keeps a single-tilde strikethrough as ~x~ when the parser read one tilde; ~~x~~ and hand-built nodes still emit ~~.

  • renderMarkdown escapes a block marker that starts a paragraph line. A paragraph whose text starts a line with 1., 1), -, +, *, #..###### or > (followed by whitespace or the end of the line) used to re-parse as a list, a heading or a quote. The first line and every line after a soft or hard break are now escaped (1\. Step One, \- item, \# x), so the output re-parses as the same paragraph. Lines that start with anything else render byte-identically. A line made only of - or = (a setext underline or thematic break) and a marker indented by one to three spaces are escaped too.

  • renderMarkdown link and image destinations that contain whitespace, < or a control character are written as <…> instead of being emitted bare, so the output re-parses to the same destination.

  • mdToc heading text is escaped in the generated list, so link syntax in a heading no longer becomes a live link in the table of contents, and a soft or hard break inside a heading joins with a space: the slug of a setext heading Foo / bar is now foo-bar (it was foobar).

  • mdEmoji expands shortcodes that contain an underscore (:heart_eyes:, :fox_face:, …), which the inline parser splits into several text nodes and which were never matched before.

  • mdSubsuper no longer rewrites code spans or URLs and no longer eats a character before the superscript: E=mc^2^ keeps its c, and `a^b^c` and https://ex.org/~a/~b/ stay as written.

  • mdHtmlDocument heading ids never collide with the article ids or the navigation toggle id: a colliding heading is renamed slug-N (and an in-document link to it follows), and every id already emitted in a multi-document page is reserved.

  • The createMd({ allowlist }) warning names the real option, sanitizeOpts.allowedAttributes (it said allowedAttrs).

  • Documentation links resolve from the npm tarball. Links that pointed outside the package, or at files the tarball does not ship, now point at the public repository at this release's tag, so they resolve from the tarball; references to sources that are not published are plain-text citations, and a See also entry naming a page that exists nowhere is removed.

  • src/refs/linkRefs.js rewritten strict factory-only : removed the top-level normalizeLabel export, moved its helpers into the refsLinkRefs factory body.

  • md.use(...extras) installs every extension in order (it loops one extension at a time and skips an already-installed name).

Security

  • Mermaid fence bodies stay escaped — mdMermaid rewrote ```mermaid blocks AFTER render (and after the sanitize pass) and unescaped the body, so <img src=x onerror=…> or </div><script>… inside a mermaid fence came out as live markup under every option set, including { safe: true, sanitize: true }. The body now stays HTML-escaped inside <div class="mermaid">; Mermaid's runtime entity-decodes the element content, so diagrams render unchanged.
  • safe data: scheme handling — renderHtml() (safe by default) blocks all data: schemes. Opt-in via the allowDataImage:true option for data:image/(png|jpeg|gif|webp| svg+xml|bmp|ico|avif|apng). The option belongs to the renderer's safe mode; the fw sanitizer has no such opt-in and always removes data: URLs.
  • Defense-in-depth — enabling sanitize:true always strips data:image regardless of allowDataImage (documented behavior — fw sanitize is the outermost, strictest layer).
  • GFM disallowed raw HTML filter enabled by default (disallowedRawHtml: true). It acts on the raw passthrough, i.e. under safe: false, where the output is the CommonMark-compliant HTML (raw HTML and javascript: links pass through unless sanitize: true is set).