Cross links ([[Note#Heading|Label]]) and embeds (![[Image.png|100x145]])
produce CrossLink(dest, label) and CrossEmbedded(dest, label, dimensions),
respectively. dest is a cross destination with
raw path and optional anchor; the label is null when no separator was authored
and an empty string for [[Note|]]. These are leaves with exhaustive visit and
walk callbacks. Resolving files, rendering and transclusion belong to consumers.
Markdown Core is a cross-platform Markdown parser that exposes the same immutable abstract syntax tree (AST) in C, Swift, Kotlin, and ECMAScript. The C engine and every binding live in this repository, so a release gives each platform the same parser behavior, node model, source locations, and extension defaults.
The project provides parsing and AST traversal, not Markdown rendering or AST mutation. It inherits from the cmark and cmark-gfm projects and from the independently developed fork at DongyuZhao/cmark-gfm. Parts of the inherited implementation were rewritten before and after this repository was created. Markdown Core is an independent project and does not plan to merge its changes back upstream. See UPSTREAM.md for the exact baseline, divergence history, and license relationship.
All platform APIs have one synchronous parse entry point: Document.parse in
Swift, Kotlin, and ECMAScript, and markdown_core_document_parse in C.
A parse produces only the AST. Every node carries a scope: the pair of
(line, column) boundaries reported by the cmark-family parser. It is the
source-faithful editor cursor range of that node's own occurrence, not a byte
range or an expanded set of locations that supplied resolved or inherited
semantic values.
The parser does not retain or publish the input, a normalized source copy, a line index, tokens, trivia, or recovery nodes. Consumers that need the Markdown text keep their own input. The Swift, Kotlin, and ECMAScript bindings copy the AST into platform values and retain no native parser handle; the C API exposes an owned document with borrowed node views.
There are no parse options. Every parse recognizes the one Markdown Core
dialect: footnotes, tables, strikethrough, autolinks, task lists, formulas
(including dollar and LaTeX delimiters), and directives, always on, on the
CommonMark base; quotation marks, hyphens, and periods are stored as written.
TreeDumper and dump() produce a canonical debug representation for logs,
tests, and debugging; dump text is not a persistence or interchange format.
Task prefixes accept exactly one authored Unicode scalar, such as - [?],
- [✓], or - [🚀], followed by a space, tab, vertical tab, or form feed.
The item preserves that scalar in marker; completion is derived. Recognition
is limited to the item's opening line and removes the prefix before deciding
its first block. Empty or multi-scalar markers and a missing separator remain
literal text.
Block identifiers use text #id# to populate
Paragraph.anchor, or - [✓] task #id# to populate ListItem.anchor.
A standalone identifier line can attach to an eligible preceding list,
callout, or table under the module's boundary rules. The marker disappears
from visible content, while scopes retain the authored source positions.
Identifiers are declarations; target resolution belongs to consumers.
Within a pipe-table cell, write a cross-link label separator as \|, as in
[[Note\|Label]] or ![[asset\|100x145]]. It remains inside that cell.
Inline $x$ and display $$ forms use Formula and FormulaBlock under the
formula grammar. Ordinary fenced code remains CodeBlock
with its info, language label, and literal body. Code, formula, comment, HTML
token, and cross-reference payloads retain their ownership boundaries; text
between paired inline HTML tags remains eligible for Markdown parsing.
==highlight== produces Mark with parsed inline content, including nested
emphasis, links, and other inline nodes. Matching consumes two equals signs
at a time; unmatched signs remain text. Typed visitors and walking visitors
include the Mark case, and its scope covers both delimiters and the body.
++inserted++ produces Insertion with parsed inline content. Repeated pairs
nest (++++text++++), and an odd leftover plus stays outside the matching
pairs (+++text+++). Insertion participates in exhaustive and walking visitors;
its scope includes the delimiters. Escapes and opaque bodies retain literal plus signs.
^[inline note] produces a one-item Cite and a document-owned Footnote
whose content holds the parsed inline body directly. Referenced definitions and
inline notes share Document.footnotes in source order. Generated inline-N
ids avoid every authored id; nested calls remain id edges and can be visited
without following semantic cycles.
%%comment%% produces Comment, the kind an HTML comment already produces,
inline or as a block when both %% fences stand on lines of their own under
the same container prefixes. The body is opaque and stored as written, nothing
is stripped, and a consumer that does not want comments drops the nodes.
Pipe, simple, multiline and grid tables share one model. caption is an
optional TableCaption with inline content; Table:, table: and : accept
a preceding or following caption. Between tables, an uncaptained preceding
table claims it first. Walkers visit the caption before head, content, and
foot. Multiline and grid cells use ordinary block content and authored relative
widths; pipe and simple cells use inline content and null widths. Grid cells
carry rowspan and colspan once in their starting row. Source-defined rows
with no starting cells retain cells=[]; an authored empty cell retains
content=[]. Consumers derive occupied coordinates and layout from these rows
and spans.
The root Swift package supports iOS 26 and macOS 26 or later and exports the
MarkdownCore product and module:
.package(url: "https://github.com/nouprax/markdown-core", from: "3.0.0")import MarkdownCore
let document = try Document.parse("# Hello")
print(document.dump())The Swift AST is an immutable, Sendable value tree. The module also provides
exhaustive typed visitors and stack-safe, read-only depth-first walking through
MarkupWalkingVisitor and Markup.walk(with:).
Use the root Maven coordinate from a Kotlin Multiplatform source set:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.nouprax:kotlin-markdown-core:3.0.0")
}
}
}import com.nouprax.markdown.core.Document
val document = Document.parse("# Hello")
println(document.dump())The published targets are Android (API 21 or later), JVM 17, macOS arm64, and
Linux x64. Android's four-ABI JNI payload is an internal dependency; consumers
do not need a separate C or Prefab package. On JDK 26 or later, JVM applications
should launch with --enable-native-access=ALL-UNNAMED to avoid a restricted
native-access warning from the package-private JNI loader.
Kotlin/Native directly cinterops the read-only C facade. JVM and Android use a separate one-call JNI payload. Neither adapter is shared with the ES/Wasm linear-memory ABI; the targets share the AST contract, not a binding bridge.
Install the ESM package with your package manager:
pnpm add @nouprax/es-markdown-coreimport { Document, TreeDumper } from "@nouprax/es-markdown-core";
const document = Document.parse("# Hello");
console.log(document.content[0].kind, document.content[0].scope);
console.log(TreeDumper.dump(document));The package supports Node.js 20 or later and browser environments that can load its WebAssembly asset. Module import completes WebAssembly initialization, so parsing is synchronous after the import resolves. The generated TypeScript surface is recursively readonly; JavaScript objects are not runtime-frozen. Native pointers, WebAssembly memory, and initialization internals are not exported.
An installed CMake package exports one complete library target containing the parser and all supported extensions:
find_package(markdown-core CONFIG REQUIRED)
target_link_libraries(my-app PRIVATE markdown-core::markdown-core)Include the read-only facade as #include <markdown_core.h>. Parse with
markdown_core_document_parse, which takes the source bytes and nothing else,
and release every successful parse with markdown_core_document_free. Nodes and string views borrow from their owning
document and must not outlive it. Error objects and allocated dump buffers use
their corresponding markdown_core_error_free and markdown_core_dump_free
functions.
The library has no process-level initialization or writable parser globals.
Independent parse instances may run concurrently, including their first calls;
read-only document access is also safe. Callers must ensure that a document is
freed only after all access to it has finished. The complete C contract is in
markdown_core.h.
Every > container is a Callout. An opening [!type] line stores the type
as written in variant; optional + and - set collapsed to false and true.
The parsed inline title is visited before content and is never a content
child. A plain quote has null metadata, and a missing title stays null. Custom
types are preserved; default titles, aliases and styling belong to consumers.
Every Markup value also exposes anchor and attributes. Attributes contain
ordered classes and ordered records (name, value), with duplicates
preserved. Directives populate these fields through the shared Pandoc braced
attribute grammar; an absent or empty container produces empty attributes.
Document.metadata holds the first complete --- envelope at the start of a
document. Metadata directly exposes ten optional fields:
name, title, subtitle, time, date, authors, keywords, abstract,
state, and comment. Unknown names, unnamed text, comments, invalid values,
and later duplicates are ignored; valid neighboring fields survive. authors
and keywords accept a single string, a bracketed array, or a block list.
abstract and comment accept single-line text and indented multiline text
with : |. Metadata stays outside Markup children and visitor callbacks.
Numbers retain exact decimal strings. Missing fields are null; an authored null
is a present scalar value. No field order or individual field scope is stored.
Media.dimensions: Dimensions? reads complete W, WxH, alt|W and
alt|WxH suffixes on direct and resolved images. Values range from 1 to
2147483647 without leading zeros; malformed suffixes remain parsed alt content.
Numeric-only labels have empty alt content. Embedded cross links use the same
size grammar in CrossEmbedded.dimensions, retaining the raw label prefix (empty
for size-only labels). Ordinary cross-link labels and invalid suffixes stay raw.
Dimensions is a node-independent value with required width and optional
height; it has no scope or visitor callbacks.
Bracketed spans ([text]{.class}) produce Span(content) with the shared
anchor and attributes. Superscript (^text^) and subscript (~text~) retain
parsed inline content; their bodies must be non-empty and contain no raw
whitespace. An escaped ASCII space within a completed body becomes NBSP.
Strikethrough uses ~~text~~. All three kinds support typed visitors and
walking visitors, and their scopes include their authored delimiters.
packages/markdown-core: C parser, public facade, CLI, extensions, and C tests.packages/swift-markdown-core: Swift binding, tests, and consumer fixture.packages/kotlin-markdown-core: Kotlin binding, platform runtimes, tests, and consumer fixtures.packages/es-markdown-core: ECMAScript/TypeScript package and WebAssembly runtime.specs/canonical-ast: shared, platform-independent AST conformance fixtures.docs/specs/dialect.md: the Markdown Core dialect index: the feature set, always on with no switches, the locked executable oracle of each feature, the cross-feature recognition order, opacity, failure, and limit rules.docs/specs/dialect/: one normative module per feature (base language, formulas, directives, attributes, anchors, links and images, cross links, footnotes, citations, comments, marks, strikethrough, superscript and subscript, inserted text, bracketed spans, task lists, lists, definition lists, callouts, block identifiers, properties, tables), plusconflicts.md, the register of source-versus-source collisions, settled decisions, and deliberate exclusions.samples: sample consumers and integration examples.scripts: repository build, formatting, lint, audit, and consumer-check entry points.
Set up or validate the pinned contributor toolchain with
docs/development-environment.md. The
non-interactive entry points are scripts/init-environment.sh --check and
scripts/init-environment.sh --install.
Install the pinned JavaScript development dependencies before using the root
pnpm tasks:
pnpm install --frozen-lockfileBuild an individual package with its native toolchain:
# C library and CLI
pnpm build:c
# Swift package
pnpm build:swift
# Kotlin/JVM artifact and its native payload
scripts/gradle.sh :packages:kotlin-markdown-core:jvmJar
# ECMAScript package and WebAssembly module
pnpm --dir packages/es-markdown-core buildThe C build can also be driven directly:
cmake --preset default
cmake --build --preset default --parallel
cmake --install build/cmake --prefix /path/to/prefixIts CLI is written to
build/cmake/packages/markdown-core/core/markdown-core; it takes files or
standard input and prints the canonical AST dump, with no language switch.
The oracle gates and position audits run that same CLI: there is one language
and one parser, and nothing in the test tree parses a part of it. The main
CMake options
are MARKDOWN_CORE_SHARED, MARKDOWN_CORE_STATIC, MARKDOWN_CORE_TESTS, and
MARKDOWN_CORE_WARNINGS_AS_ERRORS. MARKDOWN_CORE_BENCHMARKS is off by
default and exists only for an explicit local measurement build.
Correctness and public-contract conformance are the test task families. Run the targets for the platforms available on the current host:
# C host
pnpm test:c-host
pnpm conformance:c-host
# Swift on macOS
pnpm test:swift-macos
pnpm conformance:swift-macos
# Kotlin/JVM
pnpm test:kotlin-jvm
pnpm conformance:kotlin-jvm
# ECMAScript
pnpm test:es-node
pnpm test:es-browser
pnpm conformance:es-nodeKotlin also has explicit Android host, Android emulator, macOS arm64, and Linux
x64 targets following the same test:<platform> and
conformance:<platform> naming. Swift has separate iOS Simulator targets.
There is intentionally no cross-host aggregate: required CI runs every
supported platform target on an appropriate host, simulator, browser, or
device.
Performance measurement is an explicit C-host experiment and never a CI gate.
When a controlled environment is available, run pnpm benchmark:c-host. A
separate PR benchmark reports one fixed parser workload and binary size against
the exact PR base. The read-only PR workflow measures and uploads only the
untrusted head result. A privileged default-branch workflow reuses a trusted
exact-SHA baseline when one exists, or checks out, builds, and publishes that
base itself before validating both JSON inputs and updating the comment. It
never executes code from the PR head. Hosted-runner timing and RSS remain
informational and never determine pass/fail. The binding packages intentionally
expose no short wall-clock/RSS loops masquerading as cross-runtime diagnostics.
Run repository-wide formatting, lint, contract, topology, and public-surface checks with:
pnpm verifyThe C presets also provide AddressSanitizer, UndefinedBehaviorSanitizer, and ThreadSanitizer builds. For example:
cmake --preset asan
cmake --build --preset asan --parallel
ctest --preset correctness-asanReplace asan with ubsan or tsan and use the matching correctness preset.
Packaging and isolated consumer checks are available through
pnpm audit:packages and pnpm check:kotlin-consumers; the Swift consumer is
part of pnpm test:swift-macos, and the installed C consumer is exercised by
the C test suite.
Pinned compiler, SDK, runtime, and IDE versions are documented in docs/toolchains.md. Release maintainers must follow docs/releasing.md, including the no-secret release dry run, protected tag/environment approval, Maven signing, npm OIDC, artifact attestation, and post-publication verification. Release notes start from CHANGELOG.md.
Markdown Core preserves all applicable upstream copyright and license notices. See LICENSE, COPYING, and UPSTREAM.md.
Attributes attach to inline code (``x{.code}), ATX and Setext headings,
fenced code, direct links/media, resolved references and angle autolinks.
Nameless fenced containers (`::: {.class}` or `::: class`) produce
`DirectiveBlock` with `name=null`; named and nameless containers share nesting
and closing rules. Definition lists (`Term` followed by `: body` or `~ body`)
produce `DefinitionList.definitions`. Each `Definition` owns its inline `term`,
ordered `content` body collections, and a `compact` boolean determined by the
blank line before its first body. Walking visits the term before the bodies.
Reference definitions can supply an anchor, classes and records. An occurrence's nonempty anchor wins; its classes and records follow inherited declarations, including duplicates. Image dimension suffixes and dimension attribute records remain independent. All returned values use the binding's native collections and remain usable after parsing finishes.
Parsed headings receive automatic anchors: # Hello World declares
hello-world, with -1, -2, and later suffixes for collisions. Explicit
anchors anywhere in the document are reserved first. [Hello World],
[Hello World][], and [go][Hello World] resolve to #hello-world, including
before the heading; an explicit reference definition takes priority. Labels
use authored heading text, so # *Title* is referenced by [*Title*].
Heading attributes stay on the heading, and generated targets add no scope.
The always-on dialect includes inline code, heading, fenced code and link attributes; automatic anchors and implicit heading references; bracketed spans; superscript and subscript; bibliography citations; named and nameless fenced containers; fancy ordered lists and example lists; definition lists; table captions; and simple, multiline and grid tables. These 18 feature groups use one immutable AST and the same behavior on every binding. The dialect contract specifies syntax, precedence and intentional differences from the pinned Pandoc reader. Citation numbering, example-list resolution, table coordinate expansion and rendering remain consumer responsibilities.