Pure-JavaScript Markdown parser and serializer for the browser, built on CommonMark 0.31 and the GitHub Flavored Markdown extensions (tables, task lists, strikethrough, extended autolinks, the disallowed raw HTML filter). It parses to a CommonMark-compatible AST, renders HTML, Markdown (CommonMark + GFM roundtrip) and XML, offers opt-in extras (frontmatter, math, footnotes, wikilinks, admonitions, highlight, sub/superscript, table of contents, emoji, mermaid) and can assemble Markdown documents into one self-contained HTML page. The only dependency is @awacloud/fw; no Node API is used.
Coverage claim, with its limit: every example of the vendored CommonMark 0.31.2 specification passes, while the GFM extensions are covered by a hand-curated set of cases, not by the full GFM example list; the extras are covered by their own tests only. See the coverage guide.
Installation
npm install @awacloud/md
In the browser, resolve the packages through an import map (adjust the paths to where your server exposes them):
<script type="importmap">
{ "imports": {
"@awacloud/fw": "/node_modules/@awacloud/fw/src/main.js",
"@awacloud/fw/": "/node_modules/@awacloud/fw/src/",
"@awacloud/md": "/node_modules/@awacloud/md/src/main.js",
"@awacloud/md/": "/node_modules/@awacloud/md/src/"
}}
</script>
Quick Start
@awacloud/md is factory-only: its entry point exports @awacloud/fw module descriptors in four arrays (fw_require, modules, extras, bundle), never a resolved md instance. Register the arrays you need on a ModuleRuntime, then resolve by name. fw_require holds the @awacloud/fw modules the package consumes and goes first.
Parse and render
import fw from '@awacloud/fw';
import { fw_require, modules } from '@awacloud/md';
fw.runtime.registerAll(fw_require); // the @awacloud/fw modules md consumes
fw.runtime.registerAll(modules); // the md modules, dependencies first
const md = fw.runtime.resolve('md');
md.renderHtml('# Hello\n\n*world*'); // '<h1>Hello</h1>\n<p><em>world</em></p>\n'
const ast = md.parse('# Title\n\nbody'); // a CommonMark-compatible AST
md.render(ast); // HTML from an AST ('renderHtml' takes text)
md.renderMarkdown(ast); // '# Title\n\nbody\n' (accepts an AST or text)
Isolated instance (worker-safe)
The resolved md carries .createMd(opts), which spawns an independent instance with its own options and its own .use() list:
const m = md.createMd({ sourcepos: true });
m.parse('# Title\n\nbody').firstChild.sourcepos; // [[1, 1], [1, 7]]
Bootstrap helper
bootstrapMd registers all four manifest arrays on a ModuleRuntime (a fresh one, or a host-supplied one) and returns lazy accessors (md, mdFull, htmlDocument, resolve):
import { bootstrapMd } from '@awacloud/md/bootstrap.js';
const { md: booted } = bootstrapMd();
booted.renderHtml('# Hi\n'); // '<h1>Hi</h1>\n'
With the full bundle (all extras)
import { extras, bundle } from '@awacloud/md';
fw.runtime.registerAll(extras); // the opt-in extras
fw.runtime.registerAll(bundle); // the pre-wired composition
const mdFull = fw.runtime.resolve('mdFullBundle');
mdFull.renderHtml('---\ntitle: x\n---\n\n:rocket: $e=mc^2$ ==go==');
mdFullBundle installs the extras on the md instance the runtime already holds, so after this call fw.runtime.resolve('md') is the same extended instance. Take a core-only md.createMd() first when you need one.
One self-contained HTML page
mdHtmlDocument (in modules) depends on four extras (mdToc, mdFrontmatter, mdFootnotes, mdAdmonitions), so extras must be registered too:
const { build } = fw.runtime.resolve('mdHtmlDocument');
const { html } = build({ documents: [{ path: 'a.md', source: '# A\n\nhello\n' }] });
html.startsWith('<!DOCTYPE html>'); // true: one file, no external reference
The .use(...) hook
Each md instance exposes .use(ext) to plug in an extra. The contract is { name, install(md) }: install receives the md API and patches parse / render / renderHtml according to its needs. Idempotent: registering an extra with the same name is a no-op.
const md2 = fw.runtime.resolve('md').createMd();
const mdFrontmatter = fw.runtime.resolve('mdFrontmatter');
const mdEmoji = fw.runtime.resolve('mdEmoji');
const m2 = md2.use(mdFrontmatter).use(mdEmoji);
m2.parse('---\ntitle: x\n---\n\n:rocket: launch').data.frontmatter; // { lang: 'yaml', content: 'title: x' }
m2.renderHtml('---\ntitle: x\n---\n\n:rocket: launch'); // '<p>🚀 launch</p>\n'
The shape differs from ooxml's .use() hook (which relies on hydrate* / dehydrate* hooks on the AST nodes) and matches the markdown-it / remark ecosystem, where extensions modify the tokenizer or the rendering. See docs/guide/extending.md.
Pre-built bundles (dist/)
Each of the two assembly roots (md, md-full) ships as a two-surface build, generated by tools/generate-bundles.mjs (repository only, not part of the published package; a driver over @awacloud/tool-prebuild-generator) and regenerated with:
bun run gen:bundles
| Surface | Path | dependencies |
|---|---|---|
fw-mode: dist/build/<root>.{js,min.js,meta.json} (@awacloud/md/build/<root>.js) |
fw-mode | the @awacloud/fw modules the roots consume (secPolicy, sanitize, htmlEntities, url), injected by a ModuleRuntime |
framework-free: dist/standalone/<root>.{js,min.js,meta.json} (@awacloud/md/standalone/<root>.js) |
framework-free | none: every fw and md-local factory is inlined |
dist/build/index.js (@awacloud/md/build/index.js) is an fw-mode barrel re-exporting the whole @awacloud/md namespace (the four registration arrays and every named descriptor) for bulk registration.
import { mdBundled } from '@awacloud/md/standalone/md.js';
const standalone = mdBundled.factory(); // no registration, no @awacloud/fw
standalone.renderHtml('# Hi\n'); // '<h1>Hi</h1>\n'
Source structure
src/
├── main.js entry point: the four descriptor arrays + named re-exports
├── bootstrap.js bootstrapMd(opts), the runtime-bootstrap primitive
├── md.js the md facade descriptor (mdMod)
├── md-walker.js visitor walker (enter/exit hooks)
├── common.js char codes + HTML/url helpers
├── errors.js MdError / ParseError / RenderError / ContractError
├── ast/ Node, Walker, manipulation, types
├── block/ block-phase CommonMark parser and its sub-modules
├── inline/ inline-phase CommonMark + GFM parser and its sub-modules
├── refs/ link reference map
├── render/ html, markdown (CommonMark + GFM roundtrip), xml
├── extra/ the opt-in extras (frontmatter, math, …)
├── document/ mdHtmlTheme + mdHtmlDocument: md(s) → one self-contained HTML page
└── bundles/ md-full (core with the extras pre-wired)
Documentation
docs/README.md: full indexdocs/guide/getting-started.md: first stepsdocs/guide/read-write.md: parse + render HTML/MD/XML, CommonMark + GFM roundtripdocs/guide/extending.md: writing an extra{ name, install(md) }docs/guide/coverage.md: CommonMark + GFM coverage and its limitsdocs/guide/html-document.md: md(s) → one self-contained HTML page (single/multi, theme, custom extras, theresolvehook, in a browser)docs/api/: API reference by module (core, extras, bundles)
Tests
Unit tests are co-located (src/**/*.test.js) and integration tests live in tests/; both stay in the repository (the published package ships neither). From the monorepo root:
bun test packages/front/office/md/
The suite includes the CommonMark spec examples (tests/commonmark-suite.test.js), the curated GFM cases (tests/gfm-suite.test.js) and a lockfile pinning the CommonMark spec version.
Design choices
- CommonMark-compatible AST:
Nodewith unprefixed public slots, XML format modelled oncmark --to xml. - Markdown roundtrip:
renderMarkdown(ast)re-emits CommonMark + GFM that re-parses to a structurally identical AST. Of the extras' node types onlysubscript,superscriptandhighlightare serialised;math_*,footnote_*,admonitionand thehtml_*nodes an extension builds are emitted as the empty string (seedocs/api/render/markdown.md). - Output hardening is the default:
renderHtml/renderrun withsafeon, so raw HTML written in the Markdown source is stripped andjavascript:/vbscript:/file:/data:URLs are neutralized (data:image/*re-allowed withallowDataImage: true). The HTML the built-in extras generate themselves (admonitions, footnotes, highlight, math, Mermaid containers) is kept, and so is the HTML a third-party extension builds withmdNode.trustedHtmlInline/mdNode.trustedHtmlBlock; any otherhtml_inline/html_blocknode an extension creates is stripped. Passsafe: falsefor spec-exact CommonMark output (raw HTML passes through, only the GFM disallowed tags such as<script>are escaped), andsanitize: trueto pipe the output through@awacloud/fw/dom/rendering/sanitize.js. Seedocs/api/md.md. - Single self-contained HTML document builder:
mdHtmlDocumentturns one or several Markdown sources into ONE.htmlfile with no external reference (embedded theme, sidebar, per-document TOC, cross-links, print CSS, licence notice). It renders with an explicitsafe: falseand passes every fragment through the fw sanitiser last and unconditionally; that pass is the single security boundary, never the renderer's ownsafe/sanitizeoptions, which would run before the heading anchors and link rewrites and strip author raw HTML the sanitiser's allowlist keeps. - Zero external npm dependency:
htmlEntities(HTML5 table) andurl.encodeSafe(CommonMark percent-encode) come from@awacloud/fw. - Worker-safe: each factory is self-sufficient;
createMd()produces isolated state. - Browser-oriented: no Node API (
fs,Buffer,node:*) is used.
See also
@awacloud/fw:ModuleRuntime+ shared helpers- CommonMark 0.31: implemented spec
- GitHub Flavored Markdown: GFM extensions
Exposed sub-paths
| Sub-path | Target | Usage |
|---|---|---|
@awacloud/md |
src/main.js |
Index: the four descriptor arrays (fw_require, modules, extras, bundle) and every module descriptor by binding name |
@awacloud/md/modules |
src/main.js |
Same file as the root, for consumers that import the manifest through an explicit sub-path |
@awacloud/md/md |
src/md.js |
The md facade descriptor (mdMod), not a ready instance |
@awacloud/md/md-full |
src/bundles/md-full.js |
The mdFullBundle descriptor: the core with every extra pre-wired |
@awacloud/md/bootstrap.js |
src/bootstrap.js |
bootstrapMd(opts): registers all four manifest arrays on a ModuleRuntime and returns lazy accessors (md, mdFull, htmlDocument) in one call |
@awacloud/md/extra/*.js |
src/extra/*.js |
One extra by file name with the .js suffix, e.g. @awacloud/md/extra/frontmatter.js: the form to import from browser code (see below) |
@awacloud/md/extra/* |
src/extra/*.js |
The same extras without the suffix (frontmatter, math, footnotes, wikilinks, admonitions, highlight, subsuper, toc, emoji, mermaid); resolved by Bun/Node through the exports map |
@awacloud/md/bundles/* |
src/bundles/*.js |
One bundle by file name, e.g. @awacloud/md/bundles/md-full |
@awacloud/md/build/* |
dist/build/* |
Pre-built fw-mode bundles (dependencies declared) and the index.js barrel |
@awacloud/md/standalone/* |
dist/standalone/* |
Pre-built framework-free bundles (every fw factory inlined) |
See docs/api/bundles/README.md.
Import extras with the .js suffix (@awacloud/md/extra/frontmatter.js). Both key forms resolve under Bun/Node through the exports map, but a browser import map that maps @awacloud/md/ to a path prefix (…/md/src/) has no exports-map awareness: the extension-less form requests …/src/extra/frontmatter and 404s, and one failed import unlinks the whole module graph. The .js-suffixed form resolves identically in both regimes.
Maturity
L4, the highest level of the monorepo's maturity scale (awa.maturity in package.json): code, tests and documentation are publication-ready. Release history: CHANGELOG.md.
Licence
AGPL-3.0-only. Copyright (c) 2026 AwaCloud SAS. A commercial licence is also available; see NOTICE. The commonmark.js attribution (BSD-2-Clause) travels in third-party/NOTICE-commonmark.
Project
- Website: https://awaforge.eu
- Source:
packages/front/office/md - Issues: https://github.com/awacloud/awa/issues
- Security policy:
SECURITY.md
Relations
Depends on
Used by
Install
npm install @awacloud/md@1.0.0Source
https://github.com/awacloud/awa
Directory: packages/front/office/md