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, …)Walkeriterator (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, GFMtable,task-list). - Inline parser (CommonMark §6) — an orchestrator over the
mdInline*sub-module descriptors (escapes,code-span,delimiter-stackfor emphasis §6.4,link,autolink+autolink-extfor the GFM extended autolinks,line-break,sourcepos,regex,helpers), with aparser-builderthat binds the per-instance options. - Renderers :
renderHtml/render(HTML),renderMarkdown(roundtrip-safe for CommonMark + GFM),renderXml(modelled oncmark --to xml). - Source positions (1-indexed
[[line, col], [line, col]]) on every block node, and on inline nodes with thesourceposoption. - AST manipulation API :
replaceNode,wrapNode,flattenNode,findFirst,findAll,cloneNode.
- Typed AST (
-
Typed errors —
MdError/ParseError/RenderError/ContractError. Everythrowuses a stable kebab-casecodeand optional structuredcontext. Error codes documented indocs/api/errors.md:md/parse-not-string—ContractError(input non-string)md/use-bad-extension—ContractError(md.usewith 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 })(defaults1000/100000/8192). Exceeding any throwsContractError('md/limit-exceeded', …)with structuredcontext. PassInfinityto 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 withsanitizeOpts(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~Osubscript +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```mermaidfences into<div class="mermaid">.
-
Bundles :
@awacloud/md/md-full— themdFullBundlefactory descriptor: it declaresmd+ the 10 extras as dependencies and installs every extra in a deterministic order (idempotent). The core has no wrapper descriptor:@awacloud/md/mdis themdfacade 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) anddist/standalone/<root>.{js,min.js,meta.json}(framework-free, every factory inlined), plus thedist/build/index.jsbarrel. Generated bytools/generate-bundles.mjs(bun run gen:bundles, a driver over@awacloud/tool-prebuild-generator) and exposed through the./build/*and./standalone/*sub-paths.
-
_shared/mdSharedfactory for helper deduplication across extras — returns the canonical stateless helpersescapeHtml,unescapeHtml,escapeForRegexand acreateLocalWalker(BLOCK, INLINE)factory building a parameterisedWalkerLocalclass (exported, not called by any module yet). Consumed byextra/{admonitions,footnotes,math,subsuper}.js(escapeHtml),extra/mermaid.js(unescapeHtml) andextra/frontmatter.js(escapeForRegex). Worker-safe renderer factories (render/html.js,render/xml.js) deliberately keep their own inlineWalkerLocal/escapeHtmlcopies 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 onname. -
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), andsanitizeHtml(XSS hardening) are consumed from@awacloud/fwrather than re-implemented locally. -
Package surface —
package.jsonexports:. 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 bundlesawa.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 is0.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-bodyreading styles, responsive + print rules. Documented atdocs/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/fwsanitizeHtmllast and unconditionally — the single security boundary, never the renderer's ownsafe/sanitizeoptions. Documented atdocs/api/document/html-document.mdand the guide. -
bootstrapMd(opts)(src/bootstrap.js,./bootstrap.jsexport) — a runtime-bootstrap primitive: registers all four manifest arrays (fw_require,modules,extras,bundle) on an@awacloud/fwModuleRuntime(fresh, or host-supplied) and returns lazy accessors (runtime,resolve,md,mdFull,htmlDocument) in one call. Documented atdocs/api/bootstrap.md. -
Two new error codes —
md/document-bad-inputandmd/document-bad-option(bothContractError, thrown bymdHtmlTheme.css/mdHtmlDocument.renderFragment/.build). Documented atdocs/api/errors.md. -
mdNode.trustedHtmlInline(literal)andmdNode.trustedHtmlBlock(literal)— the public way for an extension to build anhtml_inline/html_blocknode thatrenderHtmlkeeps 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 atdocs/api/ast/node.md. -
renderMarkdownserialisessubscript,superscriptandhighlightnodes (~x~,^x^,==x==). Every other extension node (math_*,footnote_*,admonition, extension-builthtml_*) is still emitted as the empty string; the round-trip guarantee covers CommonMark + GFM only. Documented atdocs/api/render/markdown.md. -
strikethroughnodes carrydelimiterCount(1 for~x~, 2 for~~x~~), kept bycloneNode. -
mdHtmlDocument:FragmentOptions.reservedIds(ids a heading may not take) and amarkdownflag on thelinks.resolvetarget (truefor a.mdtarget). Documented atdocs/api/document/html-document.md.
Changed
-
Breaking —
renderHtml/renderare safe by default.safeis now on unlesssafe: falseis passed (per call, or as the instance default viacreateMd({ safe: false }); a per-call option wins): raw HTML written in the Markdown source is stripped andjavascript:/vbscript:/file:/data:URLs are neutralized (allowDataImageunchanged). 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 withmdNode.trustedHtmlInline/mdNode.trustedHtmlBlock; any otherhtml_inline/html_blocknode an extension builds is stripped unlesssafe: false. To restore the previous output — the spec-exact CommonMark raw passthrough — pass{ safe: false }.sanitizestill defaults tofalse.mdHtmlDocumentoutput is unchanged (it renders with an explicitsafe: falsebefore its own sanitiser pass). -
mdSubsuperis a post-parse AST pass (the shapemdHighlightuses) instead of a rewrite of the source text. A single-tilde strikethrough without whitespace (~x~) becomes asubscriptnode,^x^in text asuperscriptnode, and both are lowered to trusted<sub>/<sup>at render time, so the extra now renders under the safe default withoutsafe: false. Code spans, fenced code, autolinks and bare URLs keep their bytes. Documented atdocs/api/extra/subsuper.md. -
mdHtmlDocumentconsultslinks.resolvefor every relative link target, not only.mdones; the target carriesmarkdown: falsefor the others. A resolver that assumed.mdtargets only must now checktarget.markdown(or the path) and returnnullfor the rest. -
Documentation pass — the README follows the published-package skeleton (every
exportskey 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/fwmodules 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 (sanitizeis opt-in;safeis 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.jsopens with the package's/*! … */legal block (content from the repository's licence matrix), each*.meta.jsonbytesentry is measured on the final bytes, and thebuiltAttimestamp is gone —bun run gen:bundlesis 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.jstest scaffold and the package-local working files no longer ship. -
Package scope renamed to
@awacloud/md(from the legacyawaorg scope), together with every scoped reference this package carries — prose, JSDoc, generator literals and the committeddist/. The generator's emitted identity literals (tools/generate-bundles.mjspackage:/builtBy:) were flipped anddist/regenerated in the same commit, so the committed bundles and their*.meta.jsonsidecars now declare"package": "@awacloud/md"and"builtBy": "@awacloud/tool-prebuild-generator …". -
extra/emoji— shortcode lookup moved from a plain object to aMap.DEFAULT_EMOJI_TABLEkeeps its public shape (a plainRecord<string, string>, still mergeable via{ ...DEFAULT_EMOJI_TABLE, custom: '🎯' }); only the internal lookup changed. Observable behaviour change: a shortcode whose name collides with anObject.prototypemember (:constructor:,:toString:,:valueOf:,:hasOwnProperty:) is now correctly a miss and is left verbatim in the document, where the oldtable[key]indexing resolved it to the inherited value and stringified it into the output.expandEmojiInAst(root, table)accepts aMapas well as a plain object and returnsroot. Covered bysrc/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-levelimports only feed the generateddeps: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 theModuleRuntimeor 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 (8block/*, 10inline/*,ast/walker.js) were promoted to first-class factory descriptors, each declaring its complete dependency graph. -
Modular error handling.
src/errors.jsno longer exports top-level error classes or anmdErrorsSharedsingleton —MdError/ParseError/RenderError/ContractErrorare declared exclusively inside themdErrorsfactory body and materialised once through the runtime.src/main.jsdoes not re-export the classes: obtain them withruntime.resolve('mdErrors')(ormdErrors.factory()for an isolated set), and every module that throws receives that same resolved instance, soinstanceofagrees package-wide. -
Performance — the
TextEncoderused bycommon.js/encodeUrlis allocated once per factory instance instead of per non-ASCII URL character;inline/parser.js'slineStartsarray is no longer built whensourceposis disabled, avoiding an O(n) scan + allocation per parse.render/html.jskeeps its own mirroredescapeHtml/encodeUrlhelpers on purpose (a worker-safe factory closes only over its own scope). -
Compatibility — the legacy
createMd({ allowlist })option now emits aconsole.warn(previously silently ignored) ; migration path issanitizeOpts.allowedTags/sanitizeOpts.allowedAttributes.replaceNode,wrapNode,cloneNoderaise a nativeTypeErroron missing arguments — documented as the JS-convention for argument pre-conditions indocs/api/errors.md. -
renderMarkdown(text)parses with the instance'smd.parse, so the installed extensions apply to a string argument exactly as they do forrenderHtml(text)(same AST). With extras installed, the node typesrenderMarkdowndoes 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 rewritemdSubsuperused to export. UseexpandSubSupInAstandlowerSubSupToHtml, or install the extra with.use(mdSubsuper).
Fixed
-
The
md-fullbundle keeps inline math and footnote references intact.mdSubsuperis now installed aftermdFootnotesandmdMath, so$x^{2}y^{3}$stays one math span anda[^1]b[^2]keeps both footnote references (the^…^scan used to split them first). -
renderMarkdownescapes a table-cell pipe exactly once:a|bis writtena\|b(it wasa\\\|b), also inside a code span, so the output re-parses to the same cell. -
renderMarkdownkeeps a single-tilde strikethrough as~x~when the parser read one tilde;~~x~~and hand-built nodes still emit~~. -
renderMarkdownescapes a block marker that starts a paragraph line. A paragraph whose text starts a line with1.,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. -
renderMarkdownlink 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. -
mdTocheading 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 headingFoo/baris nowfoo-bar(it wasfoobar). -
mdEmojiexpands shortcodes that contain an underscore (:heart_eyes:,:fox_face:, …), which the inline parser splits into several text nodes and which were never matched before. -
mdSubsuperno longer rewrites code spans or URLs and no longer eats a character before the superscript:E=mc^2^keeps itsc, and`a^b^c`andhttps://ex.org/~a/~b/stay as written. -
mdHtmlDocumentheading ids never collide with the article ids or the navigation toggle id: a colliding heading is renamedslug-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 saidallowedAttrs). -
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.jsrewritten strict factory-only : removed the top-levelnormalizeLabelexport, moved its helpers into therefsLinkRefsfactory body. -
md.use(...extras)installs every extension in order (it loops one extension at a time and skips an already-installedname).
Security
- Mermaid fence bodies stay escaped —
mdMermaidrewrote```mermaidblocks AFTERrender(and after thesanitizepass) 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. safedata: scheme handling —renderHtml()(safe by default) blocks alldata:schemes. Opt-in via theallowDataImage:trueoption fordata:image/(png|jpeg|gif|webp| svg+xml|bmp|ico|avif|apng). The option belongs to the renderer'ssafemode; the fw sanitizer has no such opt-in and always removesdata:URLs.- Defense-in-depth — enabling
sanitize:truealways stripsdata:imageregardless ofallowDataImage(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. undersafe: false, where the output is the CommonMark-compliant HTML (raw HTML andjavascript:links pass through unlesssanitize: trueis set).