From da4685b4d50e093cd84f8f4a07ba75d7531e2ef5 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Mon, 27 Jul 2026 23:53:36 -0500 Subject: [PATCH 01/12] [Fix] C entropy + iterative dump; ES iterative decode, delta rebuild, docs (#40 #44 #45 #48 #61 #63) Co-Authored-By: Claude Fable 5 --- README.md | 10 +- packages/es-markdown-core/README.md | 2 +- packages/es-markdown-core/package.json | 2 +- .../es-markdown-core/scripts/benchmark.mjs | 35 ++++ packages/es-markdown-core/scripts/build.mjs | 2 + packages/es-markdown-core/src/bridge.c | 8 + .../es-markdown-core/src/runtime/c-session.ts | 11 ++ .../es-markdown-core/src/runtime/native.ts | 16 +- .../src/session/markup-session.ts | 105 +++++++++- .../es-markdown-core/src/session/relink.ts | 88 +++++++++ .../es-markdown-core/src/wire/node-decoder.ts | 152 ++++++++++----- packages/es-markdown-core/tests/packaging.mjs | 29 +++ .../es-markdown-core/tests/session.test.mjs | 69 +++++++ .../es-markdown-core/tests/worker-lineage.mjs | 13 ++ packages/markdown-core/extensions/ast.c | 180 ++++++++++++------ packages/markdown-core/extensions/session.c | 58 +++++- packages/markdown-core/tests/CMakeLists.txt | 4 + packages/markdown-core/tests/api/main.c | 35 ++++ .../tests/runners/pathological_runner.c | 31 +-- 19 files changed, 719 insertions(+), 131 deletions(-) create mode 100644 packages/es-markdown-core/src/session/relink.ts create mode 100644 packages/es-markdown-core/tests/worker-lineage.mjs diff --git a/README.md b/README.md index 1f0590a..ab6d6d2 100644 --- a/README.md +++ b/README.md @@ -92,16 +92,16 @@ pnpm add @nouprax/es-markdown-core ``` ```js -import { Document, TreeDumper, Walker } from "@nouprax/es-markdown-core"; +import { Document, MarkupDumper, MarkupWalker } from "@nouprax/es-markdown-core"; const document = Document.parse("# Hello", { directives: false }); -new Walker().walk(document, (event, node) => { - console.log(event, node.kind, node.scope); +new MarkupWalker().walk(document, (event, node, scope) => { + console.log(event, node.kind, scope.start.line); }); -console.log(TreeDumper.dump(document)); +console.log(MarkupDumper.dump(document)); ``` -The package supports Node.js 20 or later and browser environments that can load +The package supports Node.js 24 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. diff --git a/packages/es-markdown-core/README.md b/packages/es-markdown-core/README.md index 714a722..b7855ce 100644 --- a/packages/es-markdown-core/README.md +++ b/packages/es-markdown-core/README.md @@ -9,7 +9,7 @@ same C parser compiled to WebAssembly. pnpm add @nouprax/es-markdown-core ``` -The package is ESM-only and supports Node.js 20 or later and browsers that can +The package is ESM-only and supports Node.js 24 or later and browsers that can load its WebAssembly asset. Importing the module completes WebAssembly initialization, so `Document.parse` is synchronous. diff --git a/packages/es-markdown-core/package.json b/packages/es-markdown-core/package.json index 18b211f..e4dd5bd 100644 --- a/packages/es-markdown-core/package.json +++ b/packages/es-markdown-core/package.json @@ -17,7 +17,7 @@ "./markdown-core.wasm": "./dist/markdown-core.wasm" }, "engines": { - "node": ">=20" + "node": ">=24" }, "publishConfig": { "access": "public" diff --git a/packages/es-markdown-core/scripts/benchmark.mjs b/packages/es-markdown-core/scripts/benchmark.mjs index 8d7d53e..83ceca3 100644 --- a/packages/es-markdown-core/scripts/benchmark.mjs +++ b/packages/es-markdown-core/scripts/benchmark.mjs @@ -48,7 +48,42 @@ function benchmarkSession(workload, unit, units) { ); } +function benchmarkFanOut(workload, width) { + // One-byte edits alternating in the first paragraph of a document with + // `width` root children: the commit must stay proportional to the delta + // (relink, no per-sibling native traffic), not to the root's width. + function replay(session) { + for (let index = 0; index < 20; index += 1) { + session.replace(0, 1, index % 2 ? "a" : "b"); + session.commit(); + } + } + + const session = new MarkupSession(); + try { + session.append("a\n\n".repeat(width)); + session.commit(); + replay(session); + const timings = []; + for (let index = 0; index < 5; index += 1) { + const start = performance.now(); + replay(session); + timings.push(performance.now() - start); + } + timings.sort((left, right) => left - right); + const medianNanoseconds = Math.round((timings[2] / 20) * 1e6); + console.log( + `benchmark runtime=es boundary=wasm_session_stream_and_delta_decode workload=${workload} ` + + `bytes=${width * 3} commits=1 warmup=1 repeats=5 median_ns=${medianNanoseconds} ` + + `peak_rss_kib=${process.resourceUsage().maxRSS} rss_kib=${Math.round(process.memoryUsage().rss / 1024)}` + ); + } finally { + session.close(); + } +} + const unit = "## Section\n\nParagraph with **strong**, [link](https://example.com), and 🚀.\n\n"; benchmark("large_document", unit.repeat(2_000)); benchmark("deep_nesting", "> ".repeat(128) + "leaf\n"); benchmarkSession("streamed_document", unit, 500); +benchmarkFanOut("fan_out_narrow_edit", 10_000); diff --git a/packages/es-markdown-core/scripts/build.mjs b/packages/es-markdown-core/scripts/build.mjs index 4e7acba..2db37a5 100644 --- a/packages/es-markdown-core/scripts/build.mjs +++ b/packages/es-markdown-core/scripts/build.mjs @@ -64,6 +64,8 @@ const exported = [ "es_document_root", "es_node_id", "es_node_revision", + "es_session_node_by_id", + "es_node_parent", "es_error_code", "es_error_free", "es_node_kind", diff --git a/packages/es-markdown-core/src/bridge.c b/packages/es-markdown-core/src/bridge.c index b7ec9a1..160bed6 100644 --- a/packages/es-markdown-core/src/bridge.c +++ b/packages/es-markdown-core/src/bridge.c @@ -133,6 +133,14 @@ uint64_t es_node_id(const markdown_core_node *node) { return markdown_core_node_ uint64_t es_node_revision(const markdown_core_node *node) { return markdown_core_node_get_revision(node); } +const markdown_core_node *es_session_node_by_id(const markdown_core_session *session, uint64_t id) { + return markdown_core_session_node_by_id(session, id); +} + +const markdown_core_node *es_node_parent(const markdown_core_node *node) { + return markdown_core_node_get_parent(node); +} + int32_t es_error_code(const markdown_core_error *error) { return (int32_t)markdown_core_error_get_code(error); } diff --git a/packages/es-markdown-core/src/runtime/c-session.ts b/packages/es-markdown-core/src/runtime/c-session.ts index da67afb..765320d 100644 --- a/packages/es-markdown-core/src/runtime/c-session.ts +++ b/packages/es-markdown-core/src/runtime/c-session.ts @@ -138,6 +138,17 @@ export class CSession { return root; } + /** The committed tree's node for `rawValue`; 0 when no such node + * exists at the committed revision. */ + nodeById(rawValue: number): number { + return native.es_session_node_by_id(this.requirePointer(), BigInt(rawValue)); + } + + /** The canonical parent of a committed-tree node; 0 for the root. */ + nodeParent(pointer: number): number { + return native.es_node_parent(pointer); + } + rootIdentity(): { readonly rawValue: number; readonly revision: number } { const root = this.rootPointer(); return { diff --git a/packages/es-markdown-core/src/runtime/native.ts b/packages/es-markdown-core/src/runtime/native.ts index 7ebc8c5..4e76c27 100644 --- a/packages/es-markdown-core/src/runtime/native.ts +++ b/packages/es-markdown-core/src/runtime/native.ts @@ -26,6 +26,8 @@ export interface NativeExports extends WebAssembly.Exports { es_document_root(document: number): number; es_node_id(node: number): bigint; es_node_revision(node: number): bigint; + es_session_node_by_id(session: number, id: bigint): number; + es_node_parent(node: number): number; es_error_code(error: number): number; es_error_free(error: number): void; es_node_kind(node: number): number; @@ -69,7 +71,9 @@ async function loadWasm(): Promise { // reported value a full second past the previous call: the entropy mix // survives libc only at seconds granularity, and a freed-and-reallocated // session at the same address within the same wall-clock second would - // otherwise mint the same lineage. + // otherwise mint the same lineage. The clock is only the fallback layer + // of the seed; random_get below carries the cross-runtime uniqueness + // contract. let lastNanoseconds = 0n; const wasi = { clock_time_get: (_clockId: number, _precision: bigint, timePtr: number): number => { @@ -80,6 +84,16 @@ async function loadWasm(): Promise { new DataView(memory.buffer).setBigUint64(timePtr, lastNanoseconds, true); return 0; }, + // Backs the engine's getentropy: per-session lineages must stay + // collision-resistant across isolated runtimes (workers, processes), + // where every deterministic input — allocator state, coarse clocks — + // repeats exactly. + random_get: (bufferPointer: number, bufferLength: number): number => { + const memory = memoryHolder.memory; + if (!memory) return 28; // WASI EINVAL; unreachable in practice + crypto.getRandomValues(new Uint8Array(memory.buffer, bufferPointer, bufferLength)); + return 0; + }, fd_close: (): number => 0, fd_seek: (): number => 0, fd_write: (): number => 0, diff --git a/packages/es-markdown-core/src/session/markup-session.ts b/packages/es-markdown-core/src/session/markup-session.ts index 4813e38..cb1d9a4 100644 --- a/packages/es-markdown-core/src/session/markup-session.ts +++ b/packages/es-markdown-core/src/session/markup-session.ts @@ -5,8 +5,11 @@ import type { MarkupID } from "../model/markup-id.js"; import { ParseError } from "../parse-error.js"; import type { ParseOptions } from "../parse-options.js"; import { CSession } from "../runtime/c-session.js"; +import type { RawDelta } from "../runtime/c-session.js"; import type { Commit, Delta } from "./commit.js"; import type { FootnoteInfo } from "./footnote-info.js"; +import { relink } from "./relink.js"; +import type { ChildSwap } from "./relink.js"; import { ScopeResolver } from "./scope-resolver.js"; import { adopt } from "./snapshot.js"; @@ -177,13 +180,19 @@ export class MarkupSession { { kind: "document", id: value.id, revision: value.revision, content: value.content }, resolver ); - } else { + } else if (raw.beforeRevision === 0) { + // First commit: every node is fresh, so one full decode pass + // skips the by-id lookups and depth ordering of the delta + // path. This is also what keeps the one-shot `Document.parse` + // sugar on the v1 performance budget. document = this.native.decoder.decodeDocument(this.native.rootPointer(), { ids: (rawValue) => this.identity(rawValue), adopt: (value) => adopt(value, resolver), mirror: this.mirror, touched }); + } else { + document = this.rebuild(raw, resolver); } for (const rawValue of raw.removed) { this.mirror.delete(rawValue); @@ -201,6 +210,100 @@ export class MarkupSession { } } + /** + * Applies one delta to the mirror: materialize `added` and `changed` + * from the native tree, relink `bubbled` from their previous values, + * children before parents. Untouched siblings are never enumerated + * through the native boundary, keeping the commit proportional to the + * delta (plus the pure-JavaScript child-array copies of relinked + * parents), not to document width. + */ + private rebuild(raw: RawDelta, resolver: ScopeResolver): Document { + const decoder = this.native.decoder; + const rebuilt = new Set([...raw.added, ...raw.changed]); + const depths = new Map(); + const depthOf = (rawValue: number, pointer: number): number => { + // Memoized: the delta carries every ancestor of a change, so the + // combined walks stay O(delta), not O(depth) per entry. + const chain: number[] = []; + let currentRaw = rawValue; + let currentPointer = pointer; + let depth = -1; + while (true) { + const cached = depths.get(currentRaw); + if (cached !== undefined) { + depth = cached; + break; + } + chain.push(currentRaw); + const parent = this.native.nodeParent(currentPointer); + if (!parent) break; + currentPointer = parent; + currentRaw = decoder.rawId(parent); + } + for (let index = chain.length - 1; index >= 0; index -= 1) { + depth += 1; + depths.set(chain[index]!, depth); + } + return depth; + }; + const entries = [...raw.added, ...raw.changed, ...raw.bubbled].map((rawValue) => { + const pointer = this.native.nodeById(rawValue); + if (!pointer) throw new Error("the delta names a node the session cannot resolve"); + return { rawValue, pointer, depth: depthOf(rawValue, pointer) }; + }); + // Children before parents: a rebuilt or relinked parent assembles + // its child values from the already-updated mirror. + entries.sort((a, b) => b.depth - a.depth); + const swaps = new Map(); + let adopted: Document | null = null; + for (const entry of entries) { + const previous = this.mirror.get(entry.rawValue); + const revision = decoder.revisionOf(entry.pointer); + let value: Markup; + if (rebuilt.has(entry.rawValue)) { + const children = decoder.childPointers(entry.pointer).map((childPointer) => { + const child = this.mirror.get(decoder.rawId(childPointer)); + if (child === undefined || child.revision !== decoder.revisionOf(childPointer)) { + throw new Error("the delta omitted a node the session mirror does not carry"); + } + return child; + }); + value = + entry.rawValue === this.rootRawValue + ? adopt({ kind: "document", id: this.identity(entry.rawValue), revision, content: children }, resolver) + : decoder.decodeValue(entry.pointer, this.identity(entry.rawValue), revision, children); + } else { + if (previous === undefined) { + throw new Error("the delta bubbled a node the session mirror does not carry"); + } + const relinked = relink(previous, revision, swaps.get(entry.rawValue) ?? []); + value = + entry.rawValue === this.rootRawValue && relinked.kind === "document" + ? adopt( + { kind: "document", id: relinked.id, revision: relinked.revision, content: relinked.content }, + resolver + ) + : relinked; + } + this.mirror.set(entry.rawValue, value); + if (entry.rawValue === this.rootRawValue && value.kind === "document") adopted = value; + if (previous !== undefined) { + const parentPointer = this.native.nodeParent(entry.pointer); + if (parentPointer) { + const parentRaw = decoder.rawId(parentPointer); + if (!rebuilt.has(parentRaw)) { + const list = swaps.get(parentRaw); + if (list) list.push({ previous, next: value }); + else swaps.set(parentRaw, [{ previous, next: value }]); + } + } + } + } + if (adopted === null) throw new Error("the delta did not include the document root"); + return adopted; + } + /** The committed snapshot's current value for `id`; null when no node * with that identity exists at the committed revision. */ node(id: MarkupID): Markup | null { diff --git a/packages/es-markdown-core/src/session/relink.ts b/packages/es-markdown-core/src/session/relink.ts new file mode 100644 index 0000000..86d6a5f --- /dev/null +++ b/packages/es-markdown-core/src/session/relink.ts @@ -0,0 +1,88 @@ +import type { Markup } from "../model/markup.js"; + +/** One child value swap inside a relinked parent: `previous` is the child's + * value in the parent's previous snapshot, `next` its value now. */ +export interface ChildSwap { + readonly previous: Markup; + readonly next: Markup; +} + +function swapped(children: readonly T[], swaps: readonly ChildSwap[]): readonly T[] { + let result: T[] | null = null; + for (const swap of swaps) { + const index = children.indexOf(swap.previous as T); + if (index >= 0) { + result ??= children.slice(); + result[index] = swap.next as T; + } + } + return result ?? children; +} + +/** + * Rebuilds a `bubbled` node from its previous snapshot value: the delta + * contract guarantees its fields and direct child id list are unchanged, so + * the new value is the previous one at the new revision with the swapped + * child values in place. No native traversal happens here — this is the + * "relink bubbled" step of the documented O(delta) mirror update. + */ +export function relink(previous: Markup, revision: number, swaps: readonly ChildSwap[]): Markup { + switch (previous.kind) { + case "document": + case "blockQuote": + case "paragraph": + case "heading": + case "listItem": + case "footnoteDefinition": + case "emphasis": + case "strong": + case "strikethrough": + case "link": + case "image": + case "tableCell": + return { ...previous, revision, content: swapped(previous.content, swaps) }; + case "list": + return { ...previous, revision, items: swapped(previous.items, swaps) }; + case "table": { + const header = swaps.find((swap) => swap.previous === previous.header)?.next; + if (header !== undefined && header.kind !== "tableRow") { + throw new Error("table relink replaced the header with a non-row node"); + } + return { + ...previous, + revision, + header: header ?? previous.header, + rows: swapped(previous.rows, swaps) + }; + } + case "tableRow": + return { ...previous, revision, cells: swapped(previous.cells, swaps) }; + case "directiveBlock": + return { + ...previous, + revision, + label: previous.label === null ? null : swapped(previous.label, swaps), + content: swapped(previous.content, swaps) + }; + case "directive": + return { + ...previous, + revision, + label: previous.label === null ? null : swapped(previous.label, swaps) + }; + case "thematicBreak": + case "codeBlock": + case "htmlBlock": + case "formulaBlock": + case "text": + case "softBreak": + case "lineBreak": + case "code": + case "html": + case "formula": + case "footnoteReference": + // Leaves have no descendants to bubble from; tolerated for + // robustness against a wider-than-necessary delta. + return { ...previous, revision }; + } +} diff --git a/packages/es-markdown-core/src/wire/node-decoder.ts b/packages/es-markdown-core/src/wire/node-decoder.ts index 36eda22..956d132 100644 --- a/packages/es-markdown-core/src/wire/node-decoder.ts +++ b/packages/es-markdown-core/src/wire/node-decoder.ts @@ -50,6 +50,17 @@ const stringField = { const scratchSize = 4 * BigUint64Array.BYTES_PER_ELEMENT; +/** One explicit decode-stack entry; `values` collects the direct child + * values until the node itself can be assembled. */ +interface DecodeFrame { + readonly node: number; + readonly id: MarkupID; + readonly revision: number; + readonly pointers: readonly number[]; + readonly values: Markup[]; + index: number; +} + export class NodeDecoder { private scratch: number; private context: DecodeContext | null = null; @@ -83,7 +94,7 @@ export class NodeDecoder { kind: "document", id: context.ids(this.rawId(root)), revision: this.revisionOf(root), - content: this.content(root) + content: this.decodeChildren(root) }); context.mirror?.set(document.id.rawValue, document); return document; @@ -125,57 +136,89 @@ export class NodeDecoder { return converted; } - private rawId(node: number): number { + rawId(node: number): number { return this.toSafeNumber(this.native.es_node_id(node), "node id"); } - private revisionOf(node: number): number { + revisionOf(node: number): number { return this.toSafeNumber(this.native.es_node_revision(node), "node revision"); } - private copyMarkup(node: number): Markup { + /** One decode frame: a node whose value is assembled once every direct + * child value in `values` is ready. */ + private static frame(node: number, id: MarkupID, revision: number, pointers: number[]): DecodeFrame { + return { node, id, revision, pointers, values: [], index: 0 }; + } + + /** + * Decodes the direct children of `parent` post-order over an explicit + * frame stack: nesting depth is input-controlled, so the decoder must + * not grow the JavaScript call stack with it. Nodes outside the + * context's `touched` set reuse their mirror value, subtree and all. + */ + private decodeChildren(parent: number): Markup[] { const context = this.context!; - const rawId = this.rawId(node); - const revision = this.revisionOf(node); - if (context.touched !== null && !context.touched.has(rawId)) { - const existing = context.mirror?.get(rawId); - if (existing === undefined || existing.revision !== revision) { - throw new Error("the delta omitted a node the session mirror does not carry"); + const stack: DecodeFrame[] = [ + NodeDecoder.frame(parent, context.ids(this.rawId(parent)), this.revisionOf(parent), this.childPointers(parent)) + ]; + while (true) { + const top = stack[stack.length - 1]!; + if (top.index < top.pointers.length) { + const pointer = top.pointers[top.index]!; + top.index += 1; + const rawId = this.rawId(pointer); + const revision = this.revisionOf(pointer); + if (context.touched !== null && !context.touched.has(rawId)) { + const existing = context.mirror?.get(rawId); + if (existing === undefined || existing.revision !== revision) { + throw new Error("the delta omitted a node the session mirror does not carry"); + } + top.values.push(existing); + continue; + } + stack.push(NodeDecoder.frame(pointer, context.ids(rawId), revision, this.childPointers(pointer))); + continue; } - return existing; + stack.pop(); + if (stack.length === 0) return top.values; + const value = this.decodeValue(top.node, top.id, top.revision, top.values); + context.mirror?.set(top.id.rawValue, value); + stack[stack.length - 1]!.values.push(value); } - const value = this.copyMarkupValue(node, context.ids(rawId), revision); - context.mirror?.set(rawId, value); - return value; } - private copyMarkupValue(node: number, id: MarkupID, revision: number): Markup { + /** + * Assembles one node's value from its native fields and its ready direct + * child values. Context-free: session rebuilds call this per delta entry + * with mirror-sourced children. + */ + decodeValue(node: number, id: MarkupID, revision: number, children: readonly Markup[]): Markup { const kind = this.kind(node); switch (kind) { case "document": throw new Error("native parser returned a nested document node"); case "blockQuote": - return { kind, id, revision, content: this.content(node) }; + return { kind, id, revision, content: children }; case "paragraph": - return { kind, id, revision, content: this.content(node) }; + return { kind, id, revision, content: children }; case "heading": { const level = this.native.es_node_heading_level(node); if (!Number.isInteger(level) || level < 1 || level > 6) { throw new Error(`native parser returned an invalid heading level ${level}`); } - return { kind, id, revision, level, content: this.content(node) }; + return { kind, id, revision, level, content: children }; } case "thematicBreak": return { kind, id, revision }; case "list": - return this.copyList(node, id, revision); + return this.copyList(node, id, revision, children); case "listItem": return { kind, id, revision, checked: this.nullableBoolean(this.native.es_node_checked(node), "list item checked state"), - content: this.content(node) + content: children }; case "codeBlock": return { @@ -197,9 +240,9 @@ export class NodeDecoder { return { kind, id, revision, mode, literal: this.requiredString(node, stringField.formulaLiteral) }; } case "table": - return this.copyTable(node, id, revision); + return this.copyTable(node, id, revision, children); case "directiveBlock": { - const fields = this.directiveFields(node); + const fields = this.directiveFields(node, children); if (fields.mode !== "standalone") { throw new Error("native parser returned an embedded directive block"); } @@ -211,7 +254,7 @@ export class NodeDecoder { id, revision, label: this.requiredString(node, stringField.footnoteLabel), - content: this.content(node) + content: children }; case "text": return { kind, id, revision, literal: this.requiredString(node, stringField.literal) }; @@ -238,11 +281,11 @@ export class NodeDecoder { literal: this.requiredString(node, stringField.formulaLiteral) }; case "emphasis": - return { kind, id, revision, content: this.content(node) }; + return { kind, id, revision, content: children }; case "strong": - return { kind, id, revision, content: this.content(node) }; + return { kind, id, revision, content: children }; case "strikethrough": - return { kind, id, revision, content: this.content(node) }; + return { kind, id, revision, content: children }; case "link": return { kind, @@ -250,7 +293,7 @@ export class NodeDecoder { revision, destination: this.readString(node, stringField.linkDestination), title: this.readString(node, stringField.linkTitle), - content: this.content(node) + content: children }; case "image": return { @@ -259,10 +302,10 @@ export class NodeDecoder { revision, source: this.readString(node, stringField.imageSource), title: this.readString(node, stringField.imageTitle), - content: this.content(node) + content: children }; case "directive": { - const fields = this.directiveFields(node); + const fields = this.directiveFields(node, children); if (fields.mode !== "embedded") throw new Error("native parser returned a standalone directive"); if (fields.content.length !== 0) throw new Error("inline directive contains block content"); return { @@ -278,21 +321,25 @@ export class NodeDecoder { case "footnoteReference": return { kind, id, revision, label: this.requiredString(node, stringField.footnoteLabel) }; case "tableRow": - return this.copyTableRow(node, id, revision); + return this.copyTableRow(node, id, revision, children); case "tableCell": - return this.copyTableCell(node, id, revision); + return this.copyTableCell(id, revision, children); } return unreachable(kind); } - private copyList(node: number, id: MarkupID, revision: number): Extract { + private copyList( + node: number, + id: MarkupID, + revision: number, + children: readonly Markup[] + ): Extract { const flavor = this.listFlavor(this.native.es_node_list_flavor(node)); const start = this.readStart(node); if (flavor === "bullet" && start !== null) { throw new Error("native parser returned a start value for a bullet list"); } - const items = this.childPointers(node).map((child) => { - const item = this.copyMarkup(child); + const items = children.map((item) => { if (item.kind !== "listItem") throw new Error("list contains a non-item node"); return item; }); @@ -307,13 +354,17 @@ export class NodeDecoder { }; } - private copyTable(node: number, id: MarkupID, revision: number): Extract { + private copyTable( + node: number, + id: MarkupID, + revision: number, + children: readonly Markup[] + ): Extract { const columnCount = this.count(this.native.es_node_table_column_count(node), "table column count"); const alignments = Array.from({ length: columnCount }, (_, index) => this.tableAlignment(this.native.es_node_table_alignment(node, index)) ); - const rows = this.childPointers(node).map((child) => { - const row = this.copyMarkup(child); + const rows = children.map((row) => { if (row.kind !== "tableRow") throw new Error("table contains a non-row node"); return row; }); @@ -329,52 +380,49 @@ export class NodeDecoder { }; } - private copyTableRow(node: number, id: MarkupID, revision: number): TableRow { + private copyTableRow(node: number, id: MarkupID, revision: number, children: readonly Markup[]): TableRow { return { kind: "tableRow", id, revision, isHeader: this.boolean(this.native.es_node_table_row_header(node), "table header state"), - cells: this.childPointers(node).map((child) => { - const cell = this.copyMarkup(child); + cells: children.map((cell) => { if (cell.kind !== "tableCell") throw new Error("table row contains a non-cell node"); return cell; }) }; } - private copyTableCell(node: number, id: MarkupID, revision: number): TableCell { - return { kind: "tableCell", id, revision, content: this.content(node) }; + private copyTableCell(id: MarkupID, revision: number, children: readonly Markup[]): TableCell { + return { kind: "tableCell", id, revision, content: children }; } - private directiveFields(node: number): { + private directiveFields( + node: number, + children: readonly Markup[] + ): { readonly mode: PlacementMode; readonly name: string; readonly attributes: string | null; readonly label: readonly Markup[] | null; readonly content: readonly Markup[]; } { - const childPointers = this.childPointers(node); const labelCount = this.native.es_node_directive_label_count(node); - if (!Number.isInteger(labelCount) || labelCount < -1 || labelCount > childPointers.length) { + if (!Number.isInteger(labelCount) || labelCount < -1 || labelCount > children.length) { throw new Error(`native parser returned an invalid directive label count ${labelCount}`); } - const label = labelCount < 0 ? null : childPointers.slice(0, labelCount).map((child) => this.copyMarkup(child)); + const label = labelCount < 0 ? null : children.slice(0, labelCount); const contentOffset = labelCount < 0 ? 0 : labelCount; return { mode: this.placement(this.native.es_node_directive_mode(node)), name: this.requiredString(node, stringField.directiveName), attributes: this.readString(node, stringField.directiveAttributes), label, - content: childPointers.slice(contentOffset).map((child) => this.copyMarkup(child)) + content: children.slice(contentOffset) }; } - private content(node: number): readonly Markup[] { - return this.childPointers(node).map((child) => this.copyMarkup(child)); - } - - private childPointers(node: number): number[] { + childPointers(node: number): number[] { const result: number[] = []; for ( let child = this.native.es_node_first_child(node); diff --git a/packages/es-markdown-core/tests/packaging.mjs b/packages/es-markdown-core/tests/packaging.mjs index 4a4decc..5d2d94a 100644 --- a/packages/es-markdown-core/tests/packaging.mjs +++ b/packages/es-markdown-core/tests/packaging.mjs @@ -76,6 +76,35 @@ try { ); if (consumer.status !== 0) throw new Error(consumer.stderr || `consumer exited ${consumer.status}`); console.log("consumer: packed npm artifact imported and parsed successfully"); + + // Every README snippet that imports the package runs against the + // packed artifact, so documented import names and API shapes cannot + // drift from the real exports. Two outcomes are tolerated: a + // ReferenceError (snippets may reference documented consumer-side + // identifiers such as a socket or renderer), and a timeout (snippets + // may model long-running loops). A SyntaxError — including a missing + // export name at module link time — always fails. + const readmes = [path.resolve(packageDirectory, "../../README.md"), path.join(packageDirectory, "README.md")]; + for (const readme of readmes) { + const text = await readFile(readme, "utf8"); + const snippets = [...text.matchAll(/```js\n([\s\S]*?)```/g)] + .map((match) => match[1]) + .filter((snippet) => snippet.includes("@nouprax/es-markdown-core")); + if (snippets.length === 0) throw new Error(`no runnable package snippet found in ${readme}`); + for (const snippet of snippets) { + const ran = spawnSync("node", ["--input-type=module", "--eval", snippet], { + cwd: temporary, + encoding: "utf8", + timeout: 10_000 + }); + const timedOut = ran.signal === "SIGTERM"; + const externalReference = ran.status !== 0 && /ReferenceError/.test(ran.stderr ?? ""); + if (ran.status !== 0 && !timedOut && !externalReference) { + throw new Error(`README snippet failed in ${readme}:\n${ran.stderr}`); + } + } + } + console.log("consumer: README snippets ran against the packed artifact"); } } finally { await rm(temporary, { recursive: true, force: true }); diff --git a/packages/es-markdown-core/tests/session.test.mjs b/packages/es-markdown-core/tests/session.test.mjs index fc27699..c1ae8a2 100644 --- a/packages/es-markdown-core/tests/session.test.mjs +++ b/packages/es-markdown-core/tests/session.test.mjs @@ -413,3 +413,72 @@ test("sessions: worker threads replay sessions on isolated engine instances", as assert.deepEqual(dumps, references); } }); + +test("sessions: lineages are unique across isolated worker runtimes", async () => { + // Every worker gets a lockstep-identical WASM instance: same allocator + // state, same coarse clocks. Distinct first-session lineages therefore + // prove the host-entropy source, not incidental timing. + const { Worker } = await import("node:worker_threads"); + const lineages = await Promise.all( + Array.from( + { length: 8 }, + () => + new Promise((resolve, reject) => { + const worker = new Worker(new URL("./worker-lineage.mjs", import.meta.url)); + worker.once("message", resolve); + worker.once("error", reject); + }) + ) + ); + assert.equal(new Set(lineages).size, lineages.length); +}); + +test("sessions: adversarial nesting decodes and commits beyond the JS call-stack budget", () => { + // Depth 8192 is four times the depth that overflowed the recursive + // decoder; the explicit-frame decoder must handle it in one-shot parse, + // first commit, and the delta path of a follow-up commit. + const depth = 8192; + const source = "> ".repeat(depth) + "leaf\n"; + const reference = Document.parse(source); + let node = reference; + let levels = 0; + while (node.content.length === 1 && node.content[0].kind === "blockQuote") { + node = node.content[0]; + levels += 1; + } + assert.equal(levels, depth); + + const session = new MarkupSession(); + try { + session.append(source); + session.commit(); + session.replace(depth * 2, depth * 2 + 4, "seed"); + const second = session.commit(); + assert.equal(second.document.dump(), Document.parse("> ".repeat(depth) + "seed\n").dump()); + } finally { + session.close(); + } +}); + +test("sessions: a narrow edit in a wide document relinks instead of re-decoding siblings", () => { + // The delta path must reuse untouched root children as the same objects + // and only replace the edited one — the mechanism that keeps a small + // commit proportional to the delta, not to document width. + const width = 2000; + const session = new MarkupSession(); + try { + session.append("a\n\n".repeat(width)); + const first = session.commit(); + session.replace(0, 1, "b"); + const second = session.commit(); + assert.equal(second.document.content.length, width); + assert.notEqual(second.document.content[0], first.document.content[0]); + assert.equal(second.document.content[0].content[0].literal, "b"); + for (let index = 1; index < width; index += 1) { + assert.equal(second.document.content[index], first.document.content[index]); + } + assert.equal(second.document.dump(), Document.parse("b\n\n" + "a\n\n".repeat(width - 1)).dump()); + } finally { + session.close(); + } +}); diff --git a/packages/es-markdown-core/tests/worker-lineage.mjs b/packages/es-markdown-core/tests/worker-lineage.mjs new file mode 100644 index 0000000..3aa5841 --- /dev/null +++ b/packages/es-markdown-core/tests/worker-lineage.mjs @@ -0,0 +1,13 @@ +// Worker-side half of the lineage uniqueness test: each worker thread +// imports the module fresh — its own WASM instance whose allocator state and +// coarse clocks repeat exactly across workers — and reports the lineage of +// its first session. Only host entropy can keep these distinct. +import { parentPort } from "node:worker_threads"; +import { MarkupSession } from "../dist/index.js"; + +const session = new MarkupSession(); +try { + parentPort.postMessage(session.lineage.toString()); +} finally { + session.close(); +} diff --git a/packages/markdown-core/extensions/ast.c b/packages/markdown-core/extensions/ast.c index cf8ceba..a7f8304 100644 --- a/packages/markdown-core/extensions/ast.c +++ b/packages/markdown-core/extensions/ast.c @@ -1016,66 +1016,138 @@ bool markdown_core_ast_fields_equal(const markdown_core_node *a, const markdown_ } } -// `parent_start_line` is the absolute start line of the node's canonical -// parent (0 for the root call): resolving sealed parent-relative lines with a -// running accumulator keeps the dump linear instead of walking the parent -// chain per node. -static void dump_node(dump_buffer *buffer, const markdown_core_node *node, size_t depth, int parent_start_line) { - markdown_core_node_kind kind = markdown_core_node_get_kind(node); - markdown_core_scope scope; - const markdown_core_node *child; - size_t count = markdown_core_node_child_count(node); - size_t i; - int start_line = node->start_line; - int end_line = node->end_line; - if (kind == MARKDOWN_CORE_KIND_NONE) { +// One preorder emission frame. `parent_start_line` is the absolute start +// line of the node's canonical parent (0 for the root): resolving sealed +// parent-relative lines with the parent's resolved value keeps the dump +// linear instead of walking the parent chain per node. `has_next` records +// whether a following sibling exists — it lands in `more[depth - 1]` when +// the frame is emitted, exactly where the branch-prefix rendering reads it. +typedef struct dump_frame { + const markdown_core_node *node; + int parent_start_line; + size_t depth; + bool has_next; +} dump_frame; + +static bool dump_frames_reserve(dump_buffer *buffer, dump_frame **stack, size_t *capacity, size_t needed) { + dump_frame *grown; + size_t next_capacity; + if (needed <= *capacity) { + return true; + } + next_capacity = *capacity ? *capacity : 64; + while (next_capacity < needed) { + next_capacity *= 2; + } + grown = (dump_frame *)realloc(*stack, next_capacity * sizeof(*grown)); + if (!grown) { buffer->failed = true; + return false; + } + *stack = grown; + *capacity = next_capacity; + return true; +} + +// Depth is input-controlled (nested block quotes nest one node per two input +// bytes), so the canonical dump must not recurse: explicit frames keep the +// public dump as depth-proof as the parser and the native iterator. +static void dump_tree(dump_buffer *buffer, const markdown_core_node *root) { + dump_frame *stack = NULL; + size_t count = 0; + size_t capacity = 0; + if (!dump_frames_reserve(buffer, &stack, &capacity, 1)) { return; } - if (node->flags & MARKDOWN_CORE_NODE__SEALED_RELATIVE) { - // The canonical traversal hides directive-label wrappers, so a hidden - // wrapper between this node and its canonical parent contributes its - // own delta. - start_line += parent_start_line; - if (is_label(node->parent)) { - start_line += node->parent->start_line; + stack[count].node = root; + stack[count].parent_start_line = 0; + stack[count].depth = 0; + stack[count].has_next = false; + count++; + while (count) { + dump_frame frame = stack[--count]; + const markdown_core_node *node = frame.node; + markdown_core_node_kind kind = markdown_core_node_get_kind(node); + markdown_core_scope scope; + const markdown_core_node *child; + size_t child_count = markdown_core_node_child_count(node); + size_t first_pushed; + size_t i; + int start_line = node->start_line; + int end_line = node->end_line; + if (kind == MARKDOWN_CORE_KIND_NONE) { + buffer->failed = true; + break; } - end_line += start_line; - } - scope.start.line = start_line; - scope.start.column = node->start_column; - scope.end.line = end_line; - scope.end.column = node->end_column; - if (depth) { - for (i = 0; i + 1 < depth; i++) { - buffer_cstr(buffer, buffer->more[i] ? "│ " : " "); + if (node->flags & MARKDOWN_CORE_NODE__SEALED_RELATIVE) { + // The canonical traversal hides directive-label wrappers, so a + // hidden wrapper between this node and its canonical parent + // contributes its own delta. + start_line += frame.parent_start_line; + if (is_label(node->parent)) { + start_line += node->parent->start_line; + } + end_line += start_line; } - buffer_cstr(buffer, buffer->more[depth - 1] ? "├── " : "└── "); - } - buffer_cstr(buffer, markdown_core_node_kind_name(kind)); - buffer_cstr(buffer, " scope="); - buffer_i64(buffer, scope.start.line); - buffer_cstr(buffer, ":"); - buffer_i64(buffer, scope.start.column); - buffer_cstr(buffer, ".."); - buffer_i64(buffer, scope.end.line); - buffer_cstr(buffer, ":"); - buffer_i64(buffer, scope.end.column); - dump_fields(buffer, node, kind); - buffer_cstr(buffer, " children="); - buffer_i64(buffer, (int64_t)count); - buffer_cstr(buffer, "\n"); - - child = markdown_core_node_get_first_child(node); - while (child) { - const markdown_core_node *next = markdown_core_node_get_next_sibling(child); - if (!ensure_more(buffer, depth)) { - return; + scope.start.line = start_line; + scope.start.column = node->start_column; + scope.end.line = end_line; + scope.end.column = node->end_column; + if (frame.depth) { + // Ancestor slots below depth - 1 keep the values written when + // those ancestors were emitted; descendants only ever write + // deeper slots, and siblings rewrite a slot only after this + // subtree has fully emitted. + if (!ensure_more(buffer, frame.depth - 1)) { + break; + } + buffer->more[frame.depth - 1] = frame.has_next; + for (i = 0; i + 1 < frame.depth; i++) { + buffer_cstr(buffer, buffer->more[i] ? "│ " : " "); + } + buffer_cstr(buffer, buffer->more[frame.depth - 1] ? "├── " : "└── "); + } + buffer_cstr(buffer, markdown_core_node_kind_name(kind)); + buffer_cstr(buffer, " scope="); + buffer_i64(buffer, scope.start.line); + buffer_cstr(buffer, ":"); + buffer_i64(buffer, scope.start.column); + buffer_cstr(buffer, ".."); + buffer_i64(buffer, scope.end.line); + buffer_cstr(buffer, ":"); + buffer_i64(buffer, scope.end.column); + dump_fields(buffer, node, kind); + buffer_cstr(buffer, " children="); + buffer_i64(buffer, (int64_t)child_count); + buffer_cstr(buffer, "\n"); + + // Append the children in source order, then reverse the appended + // range: the singly linked child list offers no reverse pass, and + // pops must emit the first child first. + first_pushed = count; + child = markdown_core_node_get_first_child(node); + while (child) { + const markdown_core_node *next = markdown_core_node_get_next_sibling(child); + if (!dump_frames_reserve(buffer, &stack, &capacity, count + 1)) { + break; + } + stack[count].node = child; + stack[count].parent_start_line = start_line; + stack[count].depth = frame.depth + 1; + stack[count].has_next = next != NULL; + count++; + child = next; + } + if (buffer->failed) { + break; + } + for (i = 0; i < (count - first_pushed) / 2; i++) { + dump_frame swapped = stack[first_pushed + i]; + stack[first_pushed + i] = stack[count - 1 - i]; + stack[count - 1 - i] = swapped; } - buffer->more[depth] = next != NULL; - dump_node(buffer, child, depth + 1, start_line); - child = next; } + free(stack); } bool markdown_core_document_dump( @@ -1092,7 +1164,7 @@ bool markdown_core_document_dump( } *output = NULL; *length = 0; - dump_node(&buffer, document->root, 0, 0); + dump_tree(&buffer, document->root); free(buffer.more); if (buffer.failed) { free(buffer.data); diff --git a/packages/markdown-core/extensions/session.c b/packages/markdown-core/extensions/session.c index 54b99e2..a6bf556 100644 --- a/packages/markdown-core/extensions/session.c +++ b/packages/markdown-core/extensions/session.c @@ -1,7 +1,23 @@ +#if defined(_WIN32) && !defined(_CRT_RAND_S) +// rand_s is the linkage-free CSPRNG on Windows and must be requested before +// the first stdlib.h include. +#define _CRT_RAND_S +#endif +#if (defined(__EMSCRIPTEN__) || defined(__wasi__)) && !defined(_GNU_SOURCE) +// musl only declares getentropy outside strict-standard mode. +#define _GNU_SOURCE +#endif + #include #include #include +#if defined(__EMSCRIPTEN__) || defined(__wasi__) +#include +#elif !defined(__APPLE__) && !defined(_WIN32) +#include +#endif + #include "session_internal.h" #include "directive.h" @@ -551,6 +567,36 @@ static bool commit_internal( return true; } +// One 64-bit read from the host CSPRNG. Sessions stay free of any library- +// owned RNG state: every source below is the platform's own, shared-nothing +// entropy service. +static bool session_host_entropy(uint64_t *value) { +#if defined(__EMSCRIPTEN__) || defined(__wasi__) + // Standalone WASM lowers getentropy to the WASI random_get import; hosts + // without the import report failure here instead of trapping. + return getentropy(value, sizeof(*value)) == 0; +#elif defined(__APPLE__) + arc4random_buf(value, sizeof(*value)); + return true; +#elif defined(_WIN32) + unsigned int low = 0; + unsigned int high = 0; + if (rand_s(&low) != 0 || rand_s(&high) != 0) { + return false; + } + *value = ((uint64_t)high << 32) | (uint64_t)low; + return true; +#else + FILE *source = fopen("/dev/urandom", "rb"); + bool complete = false; + if (source) { + complete = fread(value, sizeof(*value), 1, source) == 1; + fclose(source); + } + return complete; +#endif +} + // --- public API ------------------------------------------------------------- markdown_core_session *markdown_core_session_open_with_mem( @@ -595,11 +641,19 @@ markdown_core_session *markdown_core_session_open_with_mem( session->revision = 0; session->record_lookups = true; - // Purely local entropy: no global RNG state. The lineage only has to make - // accidental cross-session id equality vanishingly unlikely. + // The address/time/clock mix alone is deterministic for the first + // session of lockstep-started isolated runtimes (one WASM instance per + // worker reproduces the same allocator state and coarse clocks), so the + // host CSPRNG carries the cross-runtime uniqueness contract. The local + // mix stays folded in as a best-effort fallback when the host read + // fails. uint64_t entropy = (uint64_t)(uintptr_t)session; + uint64_t host_entropy = 0; entropy ^= mix64((uint64_t)time(NULL)); entropy ^= mix64((uint64_t)clock()) << 1; + if (session_host_entropy(&host_entropy)) { + entropy ^= host_entropy; + } session->lineage = mix64(entropy); if (!commit_internal(session, true, NULL, error)) { diff --git a/packages/markdown-core/tests/CMakeLists.txt b/packages/markdown-core/tests/CMakeLists.txt index 143c8dd..6cbf2de 100755 --- a/packages/markdown-core/tests/CMakeLists.txt +++ b/packages/markdown-core/tests/CMakeLists.txt @@ -411,6 +411,10 @@ set(MARKDOWN_CORE_PATHOLOGICAL_CASES foreach(case IN LISTS MARKDOWN_CORE_PATHOLOGICAL_CASES) markdown_core_add_test(pathological_${case} pathological 30 pathological_runner --case ${case}) endforeach() +# The mid-chain marker flips re-kind the full 50000-deep quote chain twice, +# each commit re-verified by a full dump-equality pass, so this case earns +# the complexity-tier budget instead of the per-case default. +set_tests_properties(pathological_session_quotes_deep PROPERTIES TIMEOUT 120) set(MARKDOWN_CORE_COMPLEXITY_CASES valid_long_quoted_value diff --git a/packages/markdown-core/tests/api/main.c b/packages/markdown-core/tests/api/main.c index d01a557..b14747f 100644 --- a/packages/markdown-core/tests/api/main.c +++ b/packages/markdown-core/tests/api/main.c @@ -1757,6 +1757,40 @@ static void session_append_id_stability(test_batch_runner *runner) { markdown_core_error_free(error); } +/* The lineage contract requires collision resistance beyond the local + * address/time mix: sequential open/free pairs revisit the same allocator + * address within one wall-clock second, so distinct lineages here prove the + * host-entropy source is live. */ +static void session_lineage_entropy(test_batch_runner *runner) { + enum { SESSIONS = 64 }; + uint64_t lineages[SESSIONS]; + markdown_core_error *error = NULL; + bool distinct = true; + bool nonzero = true; + int i; + int j; + + for (i = 0; i < SESSIONS; i++) { + markdown_core_session *session = markdown_core_session_open(NULL, &error); + OK(runner, session != NULL, "entropy session opens"); + if (!session) { + markdown_core_error_free(error); + return; + } + lineages[i] = markdown_core_session_lineage(session); + markdown_core_session_free(session); + } + for (i = 0; i < SESSIONS; i++) { + nonzero = nonzero && lineages[i] != 0; + for (j = i + 1; j < SESSIONS; j++) { + distinct = distinct && lineages[i] != lineages[j]; + } + } + OK(runner, nonzero, "every lineage is nonzero"); + OK(runner, distinct, "sequential same-address sessions never share a lineage"); + markdown_core_error_free(error); +} + static void session_suffix_id_stability(test_batch_runner *runner) { markdown_core_error *error = NULL; markdown_core_session *session = markdown_core_session_open(NULL, &error); @@ -2238,6 +2272,7 @@ int main(void) { autolink_source_pos(runner); session_streaming_equivalence(runner); session_append_id_stability(runner); + session_lineage_entropy(runner); session_suffix_id_stability(runner); session_utf8_split_append(runner); session_edit_errors(runner); diff --git a/packages/markdown-core/tests/runners/pathological_runner.c b/packages/markdown-core/tests/runners/pathological_runner.c index 4271dda..279e6f2 100644 --- a/packages/markdown-core/tests/runners/pathological_runner.c +++ b/packages/markdown-core/tests/runners/pathological_runner.c @@ -16,9 +16,9 @@ * sessions via the shared replay harness: every commit checks the session * dump against a one-shot parse of the same text, folds the delta stream * into an id->revision mirror, and (with footnotes enabled) compares - * footnote queries against a fresh session. Session trees pass through - * the recursive dump on every commit, so their nesting depths stay well - * below the one-shot cases'. + * footnote queries against a fresh session. The canonical dump is + * iterative like every other traversal, so session cases run at the same + * adversarial depths as the one-shot cases. */ #include #include @@ -894,16 +894,18 @@ static int case_session_backtick_runs(pc_context *context) { return result; } -/* 1024-deep block quotes: the open chain spans the whole document on every - * commit. The innermost text edit rides the full chain; the mid-chain - * marker flip re-kinds level 64 and everything below it into a list and - * back. */ +/* 50000-deep block quotes — the same depth as the one-shot cases now that + * the canonical dump verifying every commit is iterative. The open chain + * spans the whole document on every commit. The innermost text edit rides + * the full chain; the mid-chain marker flip re-kinds level 64 and + * everything below it into a list and back. */ static int case_session_quotes_deep(pc_context *context) { + enum { QUOTE_DEPTH = 50000 }; markdown_core_parse_options options; sr_replay replay; int result = -1; - if (pc_build(context, NULL, "> ", 1024, "a") != 0) { + if (pc_build(context, NULL, "> ", QUOTE_DEPTH, "a") != 0) { return -1; } markdown_core_parse_options_init(&options); @@ -911,23 +913,24 @@ static int case_session_quotes_deep(pc_context *context) { return -1; } if (ps_splice(&replay, 0, 0, context->input) != 0 || - ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, 1024, "BlockQuote") != 0) { + ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, QUOTE_DEPTH, "BlockQuote") != 0) { goto done; } - /* Innermost text, under 1024 open quotes. */ - if (ps_splice(&replay, 2048, 2049, "b") != 0 || ps_splice(&replay, 2048, 2049, "a") != 0) { + /* Innermost text, under QUOTE_DEPTH open quotes. */ + if (ps_splice(&replay, QUOTE_DEPTH * 2, QUOTE_DEPTH * 2 + 1, "b") != 0 || + ps_splice(&replay, QUOTE_DEPTH * 2, QUOTE_DEPTH * 2 + 1, "a") != 0) { goto done; } /* Level 64's marker becomes a list bullet (list markers only open below * the engine's MAX_LIST_DEPTH, so the flip sits shallow): 64 quotes - * above, one list item holding the remaining 959 quotes below. */ + * above, one list item holding the remaining quotes below. */ if (ps_splice(&replay, 64 * 2, 64 * 2 + 2, "- ") != 0 || - ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, 1023, "BlockQuote") != 0 || + ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, QUOTE_DEPTH - 1, "BlockQuote") != 0 || ps_expect_kind(&replay, MARKDOWN_CORE_KIND_LIST, 1, "List") != 0) { goto done; } if (ps_splice(&replay, 64 * 2, 64 * 2 + 2, "> ") != 0 || - ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, 1024, "BlockQuote") != 0 || + ps_expect_kind(&replay, MARKDOWN_CORE_KIND_BLOCK_QUOTE, QUOTE_DEPTH, "BlockQuote") != 0 || ps_expect_kind(&replay, MARKDOWN_CORE_KIND_LIST, 0, "List") != 0) { goto done; } From 91455ec1a0a5792791d9bf0d6ca3fa818a694247 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Mon, 27 Jul 2026 23:57:09 -0500 Subject: [PATCH 02/12] [Fix] Swift iterative walker, memoized rebuild depths, LocalizedError (#46 #49 #52) Co-Authored-By: Claude Fable 5 --- .../MarkdownCoreBenchmarks/main.swift | 40 +++++++++++++++++++ .../Sources/MarkdownCore/Document.swift | 8 ++++ .../MarkdownCore/Session/MarkupSession.swift | 31 ++++++++++---- .../MarkdownCore/Walker/MarkupWalker.swift | 29 +++++++++++--- .../MarkdownCoreSuites.swift | 18 +++++++++ .../MarkdownCoreTests/SessionSuites.swift | 32 +++++++++++++++ 6 files changed, 145 insertions(+), 13 deletions(-) diff --git a/packages/swift-markdown-core/Benchmarks/MarkdownCoreBenchmarks/main.swift b/packages/swift-markdown-core/Benchmarks/MarkdownCoreBenchmarks/main.swift index 551d8f5..dbea2a9 100644 --- a/packages/swift-markdown-core/Benchmarks/MarkdownCoreBenchmarks/main.swift +++ b/packages/swift-markdown-core/Benchmarks/MarkdownCoreBenchmarks/main.swift @@ -78,3 +78,43 @@ func benchmarkSession(_ workload: String, unit: String, units: Int) throws { } try benchmarkSession("streamed_document", unit: unit, units: 500) + +func benchmarkDeepEdit(_ workload: String, depth: Int) throws { + // A one-byte edit at the innermost leaf of a deep quote chain: the + // rebuild ordering must stay proportional to the touched path, not turn + // quadratic through per-entry ancestor walks. + let session = try MarkupSession() + try session.append(String(repeating: "> ", count: depth) + "a\n") + _ = try session.commit() + func replay(_ round: Int) throws { + try session.replace((depth * 2)..<(depth * 2 + 1), with: round % 2 == 0 ? "b" : "a") + _ = try session.commit() + } + + for round in 0.. Int { + var chain: [UInt64] = [] + var current: OpaquePointer? = node + var resolved = -1 + while let pointer = current { + let rawID = markdown_core_node_get_id(pointer) + if let cached = depths[rawID] { + resolved = cached + break + } + chain.append(rawID) + current = markdown_core_node_get_parent(pointer) + } + for rawID in chain.reversed() { + resolved += 1 + depths[rawID] = resolved + } + return resolved + } for id in [delta.added, delta.changed, delta.bubbled].joined() { guard let node = markdown_core_session_node_by_id(session, id.rawValue) else { preconditionFailure("delta names a node the session cannot resolve") } - var depth = 0 - var parent = markdown_core_node_get_parent(node) - while let current = parent { - depth += 1 - parent = markdown_core_node_get_parent(current) - } - rebuilds.append(Rebuild(rawID: id.rawValue, node: node, depth: depth)) + rebuilds.append(Rebuild(rawID: id.rawValue, node: node, depth: depth(of: node))) } // Children before parents: a rebuilt parent assembles its child // values from the mirror. diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift index 9f0cc64..240e47f 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift @@ -24,18 +24,35 @@ public struct MarkupWalker: Sendable { try walk(node: node, document: document, visit: visit) } + private enum Frame { + case enter(any Markup) + case exit(any Markup, Scope) + } + private func walk( node: any Markup, document: Document, visit: (WalkEvent, any Markup, Scope) throws -> Void ) rethrows { - let scope = document.scope(of: node) - try visit(.entering, node, scope) - var visitor = ChildrenVisitor() - for child in node.accept(&visitor) { - try walk(node: child, document: document, visit: visit) + // Nesting depth is input-controlled, so the traversal runs over an + // explicit frame stack: a document that parsed must also walk, + // whatever its depth. Children are pushed reversed so pops preserve + // the recursive `.entering`/`.exiting` order exactly. + var stack: [Frame] = [.enter(node)] + while let frame = stack.popLast() { + switch frame { + case .exit(let node, let scope): + try visit(.exiting, node, scope) + case .enter(let node): + let scope = document.scope(of: node) + try visit(.entering, node, scope) + stack.append(.exit(node, scope)) + var visitor = ChildrenVisitor() + for child in node.accept(&visitor).reversed() { + stack.append(.enter(child)) + } + } } - try visit(.exiting, node, scope) } } diff --git a/packages/swift-markdown-core/Tests/MarkdownCoreTests/MarkdownCoreSuites.swift b/packages/swift-markdown-core/Tests/MarkdownCoreTests/MarkdownCoreSuites.swift index 72272e7..55ac2e5 100644 --- a/packages/swift-markdown-core/Tests/MarkdownCoreTests/MarkdownCoreSuites.swift +++ b/packages/swift-markdown-core/Tests/MarkdownCoreTests/MarkdownCoreSuites.swift @@ -1,3 +1,4 @@ +import Foundation import MarkdownCore import Testing @@ -34,6 +35,23 @@ import Testing func empty() throws { #expect(try Document.parse("").children.isEmpty) } + + @Test("ParseError carries its native message through every presentation path") + func parseErrorPresentation() throws { + let session = try MarkupSession() + do { + try session.replace(5..<9, with: "beyond the stored text") + Issue.record("an out-of-range edit must throw") + } catch let error as ParseError { + #expect(error.code == .invalidArgument) + #expect(!error.message.isEmpty) + // String interpolation and Foundation presentation must agree: + // localizedDescription previously degraded to a bare domain/code. + #expect(String(describing: error) == error.message) + #expect(error.localizedDescription == error.message) + #expect((error as NSError).localizedDescription == error.message) + } + } } @Suite("ownership") struct OwnershipSuite { diff --git a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift index 55fb32a..281009b 100644 --- a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift +++ b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift @@ -297,3 +297,35 @@ private final class ConflationDriver { } } } + +@Suite("depth") struct DepthSuite { + @Test("adversarial nesting walks, dumps, and commits beyond the call-stack budget") + func adversarialNestingDepth() throws { + // 4096 nested quotes overflowed the recursive walker; the explicit + // frame stack must keep parse, walk, dump, and the delta path of an + // incremental commit working at the same depth. + let depth = 4096 + let source = String(repeating: "> ", count: depth) + "leaf\n" + let document = try Document.parse(source) + + var quoteEnters = 0 + var events = 0 + MarkupWalker().walk(document) { event, node, _ in + events += 1 + if event == .entering, node is BlockQuote { quoteEnters += 1 } + } + #expect(quoteEnters == depth) + // Every node enters exactly once and exits exactly once: the + // document, the quote chain, and the innermost paragraph and text. + #expect(events == 2 * (depth + 3)) + #expect(document.dump().contains("BlockQuote")) + + let session = try MarkupSession() + try session.append(source) + _ = try session.commit() + try session.replace((depth * 2)..<(depth * 2 + 4), with: "seed") + let second = try session.commit() + let reference = try Document.parse(String(repeating: "> ", count: depth) + "seed\n") + #expect(second.document.dump() == reference.dump()) + } +} From a2d9fbdb9a8e5e978b1a1e48c78731dbeb059d24 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:03:31 -0500 Subject: [PATCH 03/12] [Fix] Kotlin atomic scope materialization, iterative walker, scope-free visitor (#41 #47 #51) Co-Authored-By: Claude Fable 5 --- .../com/nouprax/markdown/core/Spin.android.kt | 7 ++ .../markdown/core/session/ScopeResolver.kt | 58 +++++++-- .../com/nouprax/markdown/core/session/Spin.kt | 5 + .../markdown/core/walker/MarkupWalker.kt | 103 ++++++++++----- .../markdown/core/WalkerTraversalTest.kt | 68 ++++++++++ .../com/nouprax/markdown/core/Spin.jvm.kt | 5 + .../core/ScopeMaterializationJvmTest.kt | 119 ++++++++++++++++++ .../com/nouprax/markdown/core/Spin.native.kt | 7 ++ 8 files changed, 334 insertions(+), 38 deletions(-) create mode 100644 packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/Spin.android.kt create mode 100644 packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Spin.kt create mode 100644 packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt create mode 100644 packages/kotlin-markdown-core/src/jvmMain/kotlin/com/nouprax/markdown/core/Spin.jvm.kt create mode 100644 packages/kotlin-markdown-core/src/jvmTest/kotlin/com/nouprax/markdown/core/ScopeMaterializationJvmTest.kt create mode 100644 packages/kotlin-markdown-core/src/nativePlatformMain/kotlin/com/nouprax/markdown/core/Spin.native.kt diff --git a/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/Spin.android.kt b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/Spin.android.kt new file mode 100644 index 0000000..2efa081 --- /dev/null +++ b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/Spin.android.kt @@ -0,0 +1,7 @@ +package com.nouprax.markdown.core + +internal actual fun materializeWaitHint() { + // Thread.onSpinWait needs API 30+/ART support; yielding is the portable + // pause for this bounded window. + Thread.yield() +} diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/ScopeResolver.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/ScopeResolver.kt index 15e256f..d5a0e29 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/ScopeResolver.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/ScopeResolver.kt @@ -28,21 +28,38 @@ internal class ScopeResolver private constructor( ) { private object Detached - // CSession (pending) | Map (materialized) | - // Detached. Reads between the owning session's mutating calls are safe - // by the native contract; the atomic keeps concurrent readers and the - // session's detach consistent with each other. + private object Materializing + + // CSession (pending) | Materializing (one reader owns the native + // scopes() call) | Map (materialized) | Detached. + // The Materializing state is what makes the native call atomic with the + // owning session's detach: a writer that wants to commit or close spins + // in detach() until the in-flight reader publishes its table, so the + // native session can never be mutated or freed under a reader. private val state = AtomicReference(initial) /** * Called by the owning session before the native tree is replaced or - * freed. A materialized resolver keeps answering from its cache. + * freed. Waits for an in-flight materialization to publish, then leaves + * a materialized resolver answering from its cache. */ fun detach() { while (true) { - val current = state.load() - if (current !is CSession) return - if (state.compareAndSet(current, Detached)) return + when (val current = state.load()) { + is CSession -> { + if (state.compareAndSet(current, Detached)) { + return + } + } + + Materializing -> { + materializeWaitHint() + } + + else -> { + return + } + } } } @@ -68,7 +85,25 @@ internal class ScopeResolver private constructor( while (true) { when (val current = state.load()) { is CSession -> { - state.compareAndSet(current, WireDecoder.decodeScopes(current.scopes())) + if (state.compareAndSet(current, Materializing)) { + materializeProbe?.invoke() + val table = + try { + WireDecoder.decodeScopes(current.scopes()) + } catch (failure: Throwable) { + // The snapshot is still current: hand the + // session back so another reader may retry, + // and let a spinning detach reclaim it. + state.store(current) + throw failure + } + state.store(table) + return table[rawValue] + } + } + + Materializing -> { + materializeWaitHint() } Detached -> { @@ -90,6 +125,11 @@ internal class ScopeResolver private constructor( * exposed snapshot swaps in a live or materialized resolver. */ val unresolvable = ScopeResolver(Detached) + /** Test seam: runs on the materializing reader between winning the + * state and issuing the native call, so interleaving tests can hold + * the reader exactly inside the window detach must respect. */ + var materializeProbe: (() -> Unit)? = null + fun live(session: CSession): ScopeResolver = ScopeResolver(session) fun materialized(table: Map): ScopeResolver = ScopeResolver(table) diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Spin.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Spin.kt new file mode 100644 index 0000000..c82b7b3 --- /dev/null +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Spin.kt @@ -0,0 +1,5 @@ +package com.nouprax.markdown.core + +/** A polite busy-wait pause while another thread finishes a bounded native + * call; the wait window is one scope-table materialization. */ +internal expect fun materializeWaitHint() diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/walker/MarkupWalker.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/walker/MarkupWalker.kt index 18f1cca..5f4a033 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/walker/MarkupWalker.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/walker/MarkupWalker.kt @@ -6,13 +6,25 @@ public enum class WalkEvent { } public object MarkupWalker { + /** + * Walks the document depth-first, dispatching each node to [visitor] in + * preorder. Scope-free by construction: a structural visitor neither + * pays scope materialization nor depends on the snapshot's resolver + * state, so a retained snapshot traverses regardless of whether it ever + * resolved scopes. + */ public fun walk( document: Document, visitor: MarkupVisitor, ) { - walk(document) { event, node, _ -> - if (event == WalkEvent.ENTERING) { - node.accept(visitor) + val stack = ArrayDeque() + stack.addLast(document) + while (stack.isNotEmpty()) { + val node = stack.removeLast() + node.accept(visitor) + val children = node.childValues() + for (index in children.indices.reversed()) { + stack.addLast(children[index]) } } } @@ -32,82 +44,114 @@ public object MarkupWalker { from: Markup, visit: (WalkEvent, Markup, Scope) -> Unit, ) { - val scope = document.scope(from) - visit(WalkEvent.ENTERING, from, scope) - from.walkChildren { child -> walk(document, child, visit) } - visit(WalkEvent.EXITING, from, scope) + // Nesting depth is input-controlled, so the traversal runs over an + // explicit frame stack: a document that parsed must also walk, + // whatever its depth. Children are pushed reversed so pops preserve + // the recursive ENTERING/EXITING order exactly. + val stack = ArrayDeque() + stack.addLast(WalkFrame.Enter(from)) + while (stack.isNotEmpty()) { + when (val frame = stack.removeLast()) { + is WalkFrame.Exit -> { + visit(WalkEvent.EXITING, frame.node, frame.scope) + } + + is WalkFrame.Enter -> { + val node = frame.node + val scope = document.scope(node) + visit(WalkEvent.ENTERING, node, scope) + stack.addLast(WalkFrame.Exit(node, scope)) + val children = node.childValues() + for (index in children.indices.reversed()) { + stack.addLast(WalkFrame.Enter(children[index])) + } + } + } + } } - private fun Markup.walkChildren(walkChild: (Markup) -> Unit) { + private sealed interface WalkFrame { + class Enter( + val node: Markup, + ) : WalkFrame + + class Exit( + val node: Markup, + val scope: Scope, + ) : WalkFrame + } + + private fun Markup.childValues(): kotlin.collections.List = when (this) { is Document -> { - content.forEach(walkChild) + content } is BlockQuote -> { - content.forEach(walkChild) + content } is Paragraph -> { - content.forEach(walkChild) + content } is Heading -> { - content.forEach(walkChild) + content } is List -> { - items.forEach(walkChild) + items } is ListItem -> { - content.forEach(walkChild) + content } is Table -> { - walkChild(header) - rows.forEach(walkChild) + buildList { + add(header) + addAll(rows) + } } is TableRow -> { - cells.forEach(walkChild) + cells } is TableCell -> { - content.forEach(walkChild) + content } is DirectiveBlock -> { - label?.forEach(walkChild) - content.forEach(walkChild) + label.orEmpty() + content } is FootnoteDefinition -> { - content.forEach(walkChild) + content } is Emphasis -> { - content.forEach(walkChild) + content } is Strong -> { - content.forEach(walkChild) + content } is Strikethrough -> { - content.forEach(walkChild) + content } is Link -> { - content.forEach(walkChild) + content } is Image -> { - content.forEach(walkChild) + content } is Directive -> { - label?.forEach(walkChild) + label.orEmpty() } is ThematicBreak, @@ -121,7 +165,8 @@ public object MarkupWalker { is HTML, is Formula, is FootnoteReference, - -> {} + -> { + emptyList() + } } - } } diff --git a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt new file mode 100644 index 0000000..85ef790 --- /dev/null +++ b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt @@ -0,0 +1,68 @@ +package com.nouprax.markdown.core + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class WalkerTraversalTest { + @Test + fun scopeFreeVisitorTraversesAnUnmaterializedSupersededSnapshot() { + MarkupSession().use { session -> + session.append("First\n\nSecond\n") + val first = session.commit() + session.append("\nThird\n") + session.commit() + + // The retained snapshot never resolved scopes while it was + // current. Its immutable tree is complete, so the structural + // visitor overload must traverse it — and must therefore never + // request scope materialization. + val recording = RecordingVisitor() + MarkupWalker.walk(first.document, recording) + assertEquals(listOf("Document", "Paragraph", "Text", "Paragraph", "Text"), recording.visited) + + // The scopeful overload keeps the documented superseded-snapshot + // failure. + assertFailsWith { + MarkupWalker.walk(first.document) { _, _, _ -> } + } + } + } + + @Test + fun adversarialNestingWalksAndDumpsBeyondTheCallStackBudget() { + // 3072 nested quotes overflowed the recursive walker on the default + // JVM stack; the explicit frame stack must keep walking, dumping, + // and the delta path of an incremental commit working at 4096. + val depth = 4096 + val source = "> ".repeat(depth) + "leaf\n" + val document = Document.parse(source) + + var quoteEnters = 0 + var events = 0 + MarkupWalker.walk(document) { event, node, _ -> + events += 1 + if (event == WalkEvent.ENTERING && node is BlockQuote) { + quoteEnters += 1 + } + } + assertEquals(depth, quoteEnters) + // Every node enters exactly once and exits exactly once: the + // document, the quote chain, and the innermost paragraph and text. + assertEquals(2 * (depth + 3), events) + assertTrue(document.dump().contains("BlockQuote")) + + val structural = RecordingVisitor() + MarkupWalker.walk(document, structural) + assertEquals(depth + 3, structural.visited.size) + + MarkupSession().use { session -> + session.append(source) + session.commit() + session.replace(depth * 2, depth * 2 + 4, "seed") + val second = session.commit() + assertEquals(Document.parse("> ".repeat(depth) + "seed\n").dump(), second.document.dump()) + } + } +} diff --git a/packages/kotlin-markdown-core/src/jvmMain/kotlin/com/nouprax/markdown/core/Spin.jvm.kt b/packages/kotlin-markdown-core/src/jvmMain/kotlin/com/nouprax/markdown/core/Spin.jvm.kt new file mode 100644 index 0000000..a3fe5cf --- /dev/null +++ b/packages/kotlin-markdown-core/src/jvmMain/kotlin/com/nouprax/markdown/core/Spin.jvm.kt @@ -0,0 +1,5 @@ +package com.nouprax.markdown.core + +internal actual fun materializeWaitHint() { + Thread.onSpinWait() +} diff --git a/packages/kotlin-markdown-core/src/jvmTest/kotlin/com/nouprax/markdown/core/ScopeMaterializationJvmTest.kt b/packages/kotlin-markdown-core/src/jvmTest/kotlin/com/nouprax/markdown/core/ScopeMaterializationJvmTest.kt new file mode 100644 index 0000000..3c81082 --- /dev/null +++ b/packages/kotlin-markdown-core/src/jvmTest/kotlin/com/nouprax/markdown/core/ScopeMaterializationJvmTest.kt @@ -0,0 +1,119 @@ +package com.nouprax.markdown.core + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ScopeMaterializationJvmTest { + @Test + fun commitWaitsForAnInFlightMaterialization() { + // The deterministic interleaving from the atomic-materialization + // contract: a reader that has acquired the live session for its + // native scopes() call is paused inside that window, and a writer's + // commit must not mutate the native tree until the reader publishes. + val session = MarkupSession() + try { + session.append("alpha\n\nbeta\n") + val first = session.commit() + val document = first.document + + val readerInside = CountDownLatch(1) + val releaseReader = CountDownLatch(1) + ScopeResolver.materializeProbe = { + readerInside.countDown() + releaseReader.await() + } + try { + var scope: Scope? = null + val reader = + thread { + scope = document.scope(document.content[0]) + } + assertTrue(readerInside.await(5, TimeUnit.SECONDS)) + // Only the paused reader's materialization takes the probe. + ScopeResolver.materializeProbe = null + + session.replace(0, 1, "A") + val committed = CountDownLatch(1) + val writer = + thread { + session.commit() + committed.countDown() + } + // The writer detaches the previous resolver first; the + // detach must spin behind the paused reader instead of + // letting the native commit overlap the native scopes() + // call. + assertFalse(committed.await(300, TimeUnit.MILLISECONDS)) + + releaseReader.countDown() + reader.join() + writer.join() + assertTrue(committed.await(5, TimeUnit.SECONDS)) + + // The reader materialized from the still-unchanged tree and + // answers at the retained snapshot's revision. + assertEquals(1, scope?.start?.line) + } finally { + ScopeResolver.materializeProbe = null + releaseReader.countDown() + } + } finally { + session.close() + } + } + + @Test + fun closeWaitsForAnInFlightMaterialization() { + val session = MarkupSession() + var closed = false + try { + session.append("gamma\n") + val document = session.commit().document + + val readerInside = CountDownLatch(1) + val releaseReader = CountDownLatch(1) + ScopeResolver.materializeProbe = { + readerInside.countDown() + releaseReader.await() + } + try { + var scope: Scope? = null + val reader = + thread { + scope = document.scope(document.content[0]) + } + assertTrue(readerInside.await(5, TimeUnit.SECONDS)) + ScopeResolver.materializeProbe = null + + val freed = CountDownLatch(1) + val closer = + thread { + session.close() + closed = true + freed.countDown() + } + // close() must not free the native session under the + // paused reader. + assertFalse(freed.await(300, TimeUnit.MILLISECONDS)) + + releaseReader.countDown() + reader.join() + closer.join() + assertTrue(freed.await(5, TimeUnit.SECONDS)) + assertEquals(1, scope?.start?.line) + } finally { + ScopeResolver.materializeProbe = null + releaseReader.countDown() + } + } finally { + if (!closed) { + session.close() + } + } + } +} diff --git a/packages/kotlin-markdown-core/src/nativePlatformMain/kotlin/com/nouprax/markdown/core/Spin.native.kt b/packages/kotlin-markdown-core/src/nativePlatformMain/kotlin/com/nouprax/markdown/core/Spin.native.kt new file mode 100644 index 0000000..798be65 --- /dev/null +++ b/packages/kotlin-markdown-core/src/nativePlatformMain/kotlin/com/nouprax/markdown/core/Spin.native.kt @@ -0,0 +1,7 @@ +package com.nouprax.markdown.core + +import platform.posix.usleep + +internal actual fun materializeWaitHint() { + usleep(50u) +} From 05a7ed6c32c3b95b6d2d5b376225c8fdf3db622d Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:05:16 -0500 Subject: [PATCH 04/12] [Fix] Explicit snapshot materialization API across ES, Kotlin, Swift (#50) Co-Authored-By: Claude Fable 5 --- docs/specs/sessions-and-deltas.md | 8 +++++++ .../es-markdown-core/src/model/document.ts | 10 +++++++++ .../es-markdown-core/src/session/snapshot.ts | 6 +++++ .../es-markdown-core/src/wire/node-decoder.ts | 6 ++--- .../es-markdown-core/tests/session.test.mjs | 18 +++++++++++++++ .../nouprax/markdown/core/model/Document.kt | 12 ++++++++++ .../markdown/core/WalkerTraversalTest.kt | 20 +++++++++++++++++ .../MarkdownCore/Session/ScopeResolver.swift | 11 ++++++++++ .../MarkdownCoreTests/SessionSuites.swift | 22 +++++++++++++++++++ 9 files changed, 110 insertions(+), 3 deletions(-) diff --git a/docs/specs/sessions-and-deltas.md b/docs/specs/sessions-and-deltas.md index 7b2b076..a30ecad 100644 --- a/docs/specs/sessions-and-deltas.md +++ b/docs/specs/sessions-and-deltas.md @@ -113,6 +113,14 @@ successful commit does. Requesting a scope from a snapshot that was superseded before it ever materialized is a documented programmer error (platforms trap), as is passing a node of a different session or revision. +A caller that retains a snapshot across commits makes the contract explicit +with `materialize()` on the snapshot: it performs the same one-time +resolution immediately, so the retained value's usability no longer depends +on whether some other read happened to run while the snapshot was current. +Structural traversal never depends on materialization: the scope-free +visitor overload of `MarkupWalker` walks any retained snapshot regardless of +resolver state. + ## Edits - `edit(byteStart, byteEnd, replacement)` replaces the byte range diff --git a/packages/es-markdown-core/src/model/document.ts b/packages/es-markdown-core/src/model/document.ts index 6b6b92d..7fa3d6e 100644 --- a/packages/es-markdown-core/src/model/document.ts +++ b/packages/es-markdown-core/src/model/document.ts @@ -21,6 +21,16 @@ export interface Document extends MarkupBase<"document"> { * different snapshots.) */ readonly scope: (node: Markup) => Scope; + /** + * Resolves and caches every scope of this snapshot now, making the + * retained value self-contained regardless of later commits or session + * close — the explicit form of the materialization that `scope`, a + * walk, or `dump` would perform implicitly on first use. Call while the + * snapshot is current (before the owning session's next successful + * commit). Idempotent; a one-shot `Document.parse` result is always + * materialized. + */ + readonly materialize: () => void; /** Returns the canonical diagnostic dump for this document. */ readonly dump: () => string; } diff --git a/packages/es-markdown-core/src/session/snapshot.ts b/packages/es-markdown-core/src/session/snapshot.ts index d1d2c04..b43f543 100644 --- a/packages/es-markdown-core/src/session/snapshot.ts +++ b/packages/es-markdown-core/src/session/snapshot.ts @@ -32,6 +32,12 @@ export function adopt(value: DocumentValue, resolver: ScopeResolver): Document { return entry.scope; } }); + Object.defineProperty(document, "materialize", { + enumerable: false, + value(): void { + resolver.materialize(); + } + }); Object.defineProperty(document, "dump", { enumerable: false, value(this: Document): string { diff --git a/packages/es-markdown-core/src/wire/node-decoder.ts b/packages/es-markdown-core/src/wire/node-decoder.ts index 956d132..956e851 100644 --- a/packages/es-markdown-core/src/wire/node-decoder.ts +++ b/packages/es-markdown-core/src/wire/node-decoder.ts @@ -8,9 +8,9 @@ import type { NativeExports } from "../runtime/native.js"; import type { ScopeEntry } from "../session/scope-resolver.js"; import { kinds, type NativeKind } from "./kinds.js"; -/** A decoded document before snapshot adoption wires its scope and dump - * mediators to a resolver. */ -export type DocumentValue = Omit; +/** A decoded document before snapshot adoption wires its scope, dump, and + * materialize mediators to a resolver. */ +export type DocumentValue = Omit; /** * One decode pass over the native committed tree. One-shot parses decode diff --git a/packages/es-markdown-core/tests/session.test.mjs b/packages/es-markdown-core/tests/session.test.mjs index c1ae8a2..220db8f 100644 --- a/packages/es-markdown-core/tests/session.test.mjs +++ b/packages/es-markdown-core/tests/session.test.mjs @@ -482,3 +482,21 @@ test("sessions: a narrow edit in a wide document relinks instead of re-decoding session.close(); } }); + +test("sessions: an explicitly materialized snapshot stays usable across commits and close", () => { + const session = new MarkupSession(); + try { + session.append("First\n\nSecond\n"); + const first = session.commit(); + // The explicit contract: materialize while current, stay + // self-contained forever after. + first.document.materialize(); + session.append("\nThird\n"); + session.commit(); + session.close(); + assert.equal(first.document.scope(first.document.content[1]).start.line, 3); + assert.ok(first.document.dump().includes("Paragraph")); + } finally { + session.close(); + } +}); diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt index a070d28..de437f9 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt @@ -38,6 +38,18 @@ public class Document internal constructor( return entry.scope } + /** + * Resolves and caches every scope of this snapshot now, making the + * retained value self-contained regardless of later commits or session + * close — the explicit form of the materialization that [scope], a + * walk, or [dump] would perform implicitly on first use. Call while the + * snapshot is current (before the owning session's next successful + * commit). Idempotent; a one-shot [parse] result is always materialized. + */ + public fun materialize() { + resolver.materialize() + } + /** Returns the canonical diagnostic dump for this document. */ public fun dump(): String = MarkupDumper.dump(this) diff --git a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt index 85ef790..5d3a02a 100644 --- a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt +++ b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt @@ -66,3 +66,23 @@ class WalkerTraversalTest { } } } + +class SnapshotMaterializationTest { + @Test + fun anExplicitlyMaterializedSnapshotStaysUsableAcrossCommitsAndClose() { + val session = MarkupSession() + val first = + session.use { + it.append("First\n\nSecond\n") + val commit = it.commit() + // The explicit contract: materialize while current, stay + // self-contained forever after. + commit.document.materialize() + it.append("\nThird\n") + it.commit() + commit + } + assertEquals(3, first.document.scope(first.document.content[1]).start.line) + assertTrue(first.document.dump().contains("Paragraph")) + } +} diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Session/ScopeResolver.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Session/ScopeResolver.swift index 10ce16b..701ded4 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Session/ScopeResolver.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Session/ScopeResolver.swift @@ -109,6 +109,17 @@ final class ScopeResolver: Sendable { } extension Document { + /// Resolves and caches every scope of this snapshot now, making the + /// retained value self-contained regardless of later commits or session + /// deinitialization — the explicit form of the materialization that + /// `scope(of:)`, a walk, or `dump()` would perform implicitly on first + /// use. Call while the snapshot is current (before the owning session's + /// next successful commit). Idempotent; a one-shot `Document.parse` + /// result is always materialized. + public func materialize() { + resolver.materialize() + } + /// Resolves the absolute scope of `node` within this snapshot, O(1) /// after the snapshot's one-time materialization. /// diff --git a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift index 281009b..ebe8cef 100644 --- a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift +++ b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift @@ -329,3 +329,25 @@ private final class ConflationDriver { #expect(second.document.dump() == reference.dump()) } } + +@Suite("materialization") struct MaterializationSuite { + @Test("an explicitly materialized snapshot stays usable across commits and deinit") + func explicitMaterialization() throws { + var retained: Document? + do { + let session = try MarkupSession() + try session.append("First\n\nSecond\n") + let first = try session.commit() + // The explicit contract: materialize while current, stay + // self-contained forever after. + first.document.materialize() + try session.append("\nThird\n") + _ = try session.commit() + retained = first.document + } + let document = try #require(retained) + let second = try #require(document.children[1] as? Paragraph) + #expect(document.scope(of: second).start.line == 3) + #expect(document.dump().contains("Paragraph")) + } +} From b00671a148d4cdc5bad71f74f9ce0fc867ba5377 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:09:26 -0500 Subject: [PATCH 05/12] [Fix] Document the Swift public API and enforce strict doc lint (#53) Co-Authored-By: Claude Fable 5 --- .../Sources/MarkdownCore/Document.swift | 29 +++++++++++++++++++ .../MarkdownCore/Markup/BlockQuote.swift | 5 ++++ .../Sources/MarkdownCore/Markup/Code.swift | 6 ++++ .../MarkdownCore/Markup/CodeBlock.swift | 11 +++++++ .../MarkdownCore/Markup/Directive.swift | 11 +++++++ .../MarkdownCore/Markup/DirectiveBlock.swift | 11 +++++++ .../MarkdownCore/Markup/Emphasis.swift | 5 ++++ .../MarkdownCore/Markup/Footnote.swift | 13 +++++++++ .../Sources/MarkdownCore/Markup/Formula.swift | 8 +++++ .../MarkdownCore/Markup/FormulaBlock.swift | 8 +++++ .../Sources/MarkdownCore/Markup/HTML.swift | 6 ++++ .../MarkdownCore/Markup/HTMLBlock.swift | 6 ++++ .../Sources/MarkdownCore/Markup/Heading.swift | 6 ++++ .../Sources/MarkdownCore/Markup/Image.swift | 7 +++++ .../MarkdownCore/Markup/LineBreak.swift | 5 ++++ .../Sources/MarkdownCore/Markup/Link.swift | 7 +++++ .../Sources/MarkdownCore/Markup/List.swift | 15 ++++++++++ .../Sources/MarkdownCore/Markup/Markup.swift | 19 ++++++++++++ .../MarkdownCore/Markup/Paragraph.swift | 5 ++++ .../MarkdownCore/Markup/SoftBreak.swift | 5 ++++ .../MarkdownCore/Markup/Strikethrough.swift | 5 ++++ .../Sources/MarkdownCore/Markup/Strong.swift | 5 ++++ .../Sources/MarkdownCore/Markup/Table.swift | 19 ++++++++++++ .../Sources/MarkdownCore/Markup/Text.swift | 6 ++++ .../MarkdownCore/Markup/ThematicBreak.swift | 5 ++++ .../Sources/MarkdownCore/Session/Commit.swift | 8 +++++ .../MarkdownCore/Session/MarkupSession.swift | 3 ++ .../MarkdownCore/Walker/MarkupVisitor.swift | 3 ++ .../MarkdownCore/Walker/MarkupWalker.swift | 5 ++++ scripts/format-swift.sh | 4 ++- 30 files changed, 250 insertions(+), 1 deletion(-) diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Document.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Document.swift index 7811ae6..e0f2cce 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Document.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Document.swift @@ -1,19 +1,33 @@ import Foundation import MarkdownCoreC +/// The feature switches for one parse or session, fixed for its lifetime. +/// Every option defaults to `true`. public struct ParseOptions: Sendable, Hashable { + /// Replaces straight quotes, dashes, and ellipses with typographic forms. public let smartPunctuation: Bool + /// Parses footnote definitions and references. public let footnotes: Bool + /// Removes HTML comments instead of passing them through. public let stripHTMLComments: Bool + /// Parses pipe tables. public let tables: Bool + /// Parses `~~strikethrough~~` spans. public let strikethrough: Bool + /// Recognizes bare URLs and email addresses as links. public let autolinks: Bool + /// Parses `[ ]`/`[x]` task-list item markers. public let taskLists: Bool + /// Parses formula spans and blocks. public let formulas: Bool + /// Recognizes `$…$` and `$$…$$` formula delimiters. public let dollarFormulaDelimiters: Bool + /// Recognizes `\(…\)` and `\[…\]` formula delimiters. public let latexFormulaDelimiters: Bool + /// Parses inline and container directives. public let directives: Bool + /// Creates a fixed option set; every switch defaults to `true`. public init( smartPunctuation: Bool = true, footnotes: Bool = true, @@ -41,17 +55,24 @@ public struct ParseOptions: Sendable, Hashable { } } +/// The category of a native parse or session failure. public enum ParseErrorCode: Int32, Sendable { case invalidArgument = 1 case allocationFailed = 2 case `internal` = 3 } +/// A native parse or session failure, carrying the engine's message and, +/// when the input position is known, the failing scope. public struct ParseError: Error, Sendable, CustomStringConvertible { + /// The failure category. public let code: ParseErrorCode + /// The engine's actionable description of the failure. public let message: String + /// The failing input extent, when the engine could attribute one. public let scope: Scope? + /// The engine's message, so string interpolation prints it directly. public var description: String { message } } @@ -74,13 +95,21 @@ extension ParseError: LocalizedError { /// first time any of these is used and is self-contained from then on; see /// `scope(of:)` for the exact rules. public struct Document: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this document's content last changed. public let revision: UInt64 + /// The document's top-level blocks in source order. public let children: [any Markup] var resolver: ScopeResolver + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } + /// Parses `source` in one shot and returns a self-contained immutable + /// snapshot; throws `ParseError` when the engine rejects the input. + /// Semantically identical to committing the same text through a + /// `MarkupSession`. public static func parse(_ source: String, options: ParseOptions = .init()) throws -> Document { // A one-shot parse is literally a single-commit session. Scopes // materialize eagerly because the session dies with this call and diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/BlockQuote.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/BlockQuote.swift index 7ef6910..5be045a 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/BlockQuote.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/BlockQuote.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// A `>`-prefixed quotation block containing block children. public struct BlockQuote: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Code.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Code.swift index f441f7b..1228bc4 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Code.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Code.swift @@ -1,11 +1,17 @@ import MarkdownCoreC +/// An inline code span. public struct Code: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The code span's literal text. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/CodeBlock.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/CodeBlock.swift index 9111ee0..ce1aa2a 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/CodeBlock.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/CodeBlock.swift @@ -1,15 +1,26 @@ import MarkdownCoreC +/// An indented or fenced code block. public struct CodeBlock: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The full info string after the opening fence, if any. public let info: String? + /// The first word of the info string — the conventional language tag. public let language: String? + /// The code block's literal text. public let literal: String + /// Whether the block was fenced rather than indented. public let isFenced: Bool + /// Whether a fenced block's closing fence was present; streaming input + /// parsed mid-block reports `false`. public let isClosed: Bool + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Directive.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Directive.swift index 48f1b17..d442204 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Directive.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Directive.swift @@ -1,14 +1,25 @@ import MarkdownCoreC +/// An inline directive (`:name[label]{attributes}`). public struct Directive: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Whether the construct is `embedded` in surrounding inline content or + /// stands alone as its own block. public let mode: PlacementMode + /// The directive's name. public let name: String + /// The raw attribute text between the braces, if any. public let attributes: String? + /// The number of leading `children` that form the directive's label; + /// nil when the directive declares no label. public let labelCount: Int? + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/DirectiveBlock.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/DirectiveBlock.swift index 8ac9ad1..2cd52ff 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/DirectiveBlock.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/DirectiveBlock.swift @@ -1,14 +1,25 @@ import MarkdownCoreC +/// A container directive block (`:::name[label]{attributes}`). public struct DirectiveBlock: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Whether the construct is `embedded` in surrounding inline content or + /// stands alone as its own block. public let mode: PlacementMode + /// The directive's name. public let name: String + /// The raw attribute text between the braces, if any. public let attributes: String? + /// The number of leading `children` that form the directive's label; + /// nil when the directive declares no label. public let labelCount: Int? + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Emphasis.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Emphasis.swift index 5aeae2a..fc1b4b7 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Emphasis.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Emphasis.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// Emphasized (typically italic) inline content. public struct Emphasis: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Footnote.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Footnote.swift index b258793..cba795c 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Footnote.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Footnote.swift @@ -1,11 +1,18 @@ import MarkdownCoreC +/// A footnote definition (`[^label]: …`); the owning session's footnote +/// queries answer numbering and back-references. public struct FootnoteDefinition: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// The footnote's normalized label without the `[^` `]` delimiters. public let label: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } @@ -23,12 +30,18 @@ extension FootnoteDefinition { } } +/// A reference (`[^label]`) that resolves to a footnote definition. public struct FootnoteReference: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The footnote's normalized label without the `[^` `]` delimiters. public let label: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Formula.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Formula.swift index 332a136..cab2472 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Formula.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Formula.swift @@ -1,12 +1,20 @@ import MarkdownCoreC +/// An inline formula (the math extension). public struct Formula: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// Whether the construct is `embedded` in surrounding inline content or + /// stands alone as its own block. public let mode: PlacementMode + /// The formula source between the delimiters. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/FormulaBlock.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/FormulaBlock.swift index 51fff53..d3207a7 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/FormulaBlock.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/FormulaBlock.swift @@ -1,12 +1,20 @@ import MarkdownCoreC +/// A standalone formula block (the math extension). public struct FormulaBlock: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// Whether the construct is `embedded` in surrounding inline content or + /// stands alone as its own block. public let mode: PlacementMode + /// The formula source between the delimiters. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTML.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTML.swift index c71bc2a..9ec43de 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTML.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTML.swift @@ -1,11 +1,17 @@ import MarkdownCoreC +/// A run of raw inline HTML, passed through unparsed. public struct HTML: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The raw HTML text. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTMLBlock.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTMLBlock.swift index 912ebb0..ccef3da 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTMLBlock.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/HTMLBlock.swift @@ -1,11 +1,17 @@ import MarkdownCoreC +/// A block of raw HTML, passed through unparsed. public struct HTMLBlock: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The raw HTML text. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Heading.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Heading.swift index 96d2af3..52689b5 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Heading.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Heading.swift @@ -1,11 +1,17 @@ import MarkdownCoreC +/// An ATX or setext heading. public struct Heading: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// The heading level, 1 through 6. public let level: Int32 + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Image.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Image.swift index 2cc42a4..18854f7 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Image.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Image.swift @@ -1,12 +1,19 @@ import MarkdownCoreC +/// An image whose children are its inline description. public struct Image: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// The image source URL, if present. public let source: String? + /// The optional image title. public let title: String? + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/LineBreak.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/LineBreak.swift index 7ed2be6..3d344e6 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/LineBreak.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/LineBreak.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// A hard line break that renders as an explicit new line. public struct LineBreak: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Link.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Link.swift index 3255d51..93c7583 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Link.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Link.swift @@ -1,12 +1,19 @@ import MarkdownCoreC +/// A hyperlink whose children are its inline caption. public struct Link: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// The link destination URL, if present. public let destination: String? + /// The optional link title. public let title: String? + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/List.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/List.swift index b13e69a..22e1996 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/List.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/List.swift @@ -1,18 +1,27 @@ import MarkdownCoreC +/// Whether a list is bulleted or ordered. public enum ListFlavor: String, Sendable { case bullet case ordered } +/// A bullet or ordered list of `ListItem` children. public struct List: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Whether the list is bulleted or ordered. public let flavor: ListFlavor + /// An ordered list's starting number; nil for bullet lists. public let start: Int64? + /// Whether the list renders tight (no paragraph spacing between items). public let isTight: Bool + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } @@ -34,12 +43,18 @@ extension List { } } +/// One item of a `List`. public struct ListItem: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// A task-list item's checkbox state; nil for plain items. public let isChecked: Bool? + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Markup.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Markup.swift index 84bda46..2c0bee2 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Markup.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Markup.swift @@ -1,19 +1,28 @@ import MarkdownCoreC +/// A one-based line/column source coordinate. public struct Position: Sendable, Hashable { + /// The one-based line number. public let line: Int32 + /// The one-based column number. public let column: Int32 + /// Creates a coordinate from one-based line and column numbers. public init(line: Int32, column: Int32) { self.line = line self.column = column } } +/// A node's absolute source extent: start and end coordinates, both +/// inclusive of the construct's own markers. public struct Scope: Sendable, Hashable { + /// The extent's first coordinate. public let start: Position + /// The extent's last coordinate. public let end: Position + /// Creates an extent from its boundary coordinates. public init(start: Position, end: Position) { self.start = start self.end = end @@ -26,9 +35,14 @@ public struct Scope: Sendable, Hashable { /// an identity. Stable across incremental commits while the node remains the /// same kind of thing at the same place. public struct MarkupID: Sendable, Hashable { + /// The owning session's random salt; ids from different sessions never + /// compare equal even when raw values collide. public let lineage: UInt64 + /// The id's value within its lineage: unique in the owning session and + /// never reused. public let rawValue: UInt64 + /// Creates an identity from a session lineage and a raw id value. public init(lineage: UInt64, rawValue: UInt64) { self.lineage = lineage self.rawValue = rawValue @@ -55,16 +69,21 @@ public protocol Markup: Sendable, Identifiable, Hashable where ID == MarkupID { } extension Markup { + /// Two nodes are equal exactly when they share `id` and `revision`, + /// which the engine guarantees implies identical content. public static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id && lhs.revision == rhs.revision } + /// Hashes the identity/revision pair that also defines equality. public func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(revision) } } +/// Whether a construct is embedded in inline content or stands alone +/// as its own block. public enum PlacementMode: String, Sendable { case embedded case standalone diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Paragraph.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Paragraph.swift index a424708..09ddbaa 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Paragraph.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Paragraph.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// A paragraph of inline content. public struct Paragraph: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/SoftBreak.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/SoftBreak.swift index a3e29c1..d2197fe 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/SoftBreak.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/SoftBreak.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// A soft line break: a newline that renders as collapsible whitespace. public struct SoftBreak: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strikethrough.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strikethrough.swift index 5c252de..79b63e0 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strikethrough.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strikethrough.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// Struck-through inline content (the `~~` extension). public struct Strikethrough: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strong.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strong.swift index d1f61e8..c6824dd 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strong.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Strong.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// Strongly emphasized (typically bold) inline content. public struct Strong: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The node's direct children in source order. public let children: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Table.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Table.swift index 2fead26..3acd3cc 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Table.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Table.swift @@ -1,5 +1,6 @@ import MarkdownCoreC +/// A table column's declared alignment. public enum TableAlignment: String, Sendable { case none case left @@ -7,13 +8,20 @@ public enum TableAlignment: String, Sendable { case right } +/// A pipe table (the tables extension) with one header row. public struct Table: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Per-column alignments, one entry per column. public let alignments: [TableAlignment] + /// The single header row. public let header: TableRow + /// The body rows, header excluded. public let rows: [TableRow] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } @@ -45,12 +53,18 @@ extension Table { } } +/// One row of a `Table`. public struct TableRow: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Whether this is the table's header row. public let isHeader: Bool + /// The row's cells in column order. public let cells: [TableCell] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } @@ -69,11 +83,16 @@ extension TableRow { } } +/// One cell of a `TableRow`. public struct TableCell: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// The cell's inline content. public let content: [any Markup] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Text.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Text.swift index a47c8fd..5e6a52d 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Text.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/Text.swift @@ -1,11 +1,17 @@ import MarkdownCoreC +/// A run of literal inline text. public struct Text: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// The decoded text content. public let literal: String + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/ThematicBreak.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/ThematicBreak.swift index d91edc7..9faf8b1 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Markup/ThematicBreak.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Markup/ThematicBreak.swift @@ -1,10 +1,15 @@ import MarkdownCoreC +/// A thematic break (horizontal rule). public struct ThematicBreak: Markup { + /// The node's session-scoped identity; see `MarkupID`. public let id: MarkupID + /// The commit revision at which this node's content last changed. public let revision: UInt64 + /// Always empty: this node is a leaf. public let children: [any Markup] = [] + /// Dispatches this node to `visitor`'s matching `visit` overload. public func accept(_ visitor: inout V) -> V.Result { visitor.visit(self) } } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Session/Commit.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Session/Commit.swift index 673a273..12adb5f 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Session/Commit.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Session/Commit.swift @@ -3,7 +3,9 @@ import MarkdownCoreC /// The result of one session commit: the new immutable snapshot and the /// exact difference from the previous revision. public struct Commit: Sendable { + /// The new committed snapshot. public let document: Document + /// The exact difference from the previous revision. public let delta: Delta } @@ -14,11 +16,17 @@ public struct Commit: Sendable { /// of removed nodes are retired and never reused. A pure positional shift is /// not a change and produces no entry. public struct Delta: Sendable, Hashable { + /// The revision the session held before this commit. public let beforeRevision: UInt64 + /// The revision this commit produced. public let afterRevision: UInt64 + /// Nodes that did not exist at the previous revision. public let added: [MarkupID] + /// Nodes that no longer exist; their ids are retired, never reused. public let removed: [MarkupID] + /// Nodes whose own fields or direct child list changed. public let changed: [MarkupID] + /// Ancestors whose revision advanced only because a descendant changed. public let bubbled: [MarkupID] } diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Session/MarkupSession.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Session/MarkupSession.swift index 27b1c65..d6107a7 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Session/MarkupSession.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Session/MarkupSession.swift @@ -18,6 +18,7 @@ public final class MarkupSession { var mirror: [UInt64: any Markup] = [:] private var resolver: ScopeResolver? + /// The session's options, normalized and immutable for its lifetime. public let options: ParseOptions /// Per-session random salt; nodes from different sessions never compare @@ -28,6 +29,8 @@ public final class MarkupSession { /// the first commit. public private(set) var document: Document + /// Opens an empty session at revision 0; throws `ParseError` when the + /// native session cannot be allocated. public init(options: ParseOptions = .init()) throws { var nativeOptions = options.native var nativeError: OpaquePointer? diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupVisitor.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupVisitor.swift index 5990a2a..28a9150 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupVisitor.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupVisitor.swift @@ -1,3 +1,6 @@ +/// Typed double-dispatch over the closed set of markup node kinds: one +/// `visit` overload per concrete node type, selected by +/// `Markup.accept(_:)`. public protocol MarkupVisitor { associatedtype Result mutating func visit(_ node: Document) -> Result diff --git a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift index 240e47f..1026d80 100644 --- a/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift +++ b/packages/swift-markdown-core/Sources/MarkdownCore/Walker/MarkupWalker.swift @@ -1,9 +1,14 @@ +/// Whether a walk callback fires on entering or leaving a node. public enum WalkEvent: Sendable { case entering case exiting } +/// A read-only depth-first traversal that supplies each event with the +/// node's resolved absolute scope. Traversal is iterative: documents walk +/// at any nesting depth that parses. public struct MarkupWalker: Sendable { + /// Creates a walker; walkers are stateless and reusable. public init() {} /// Walks the document depth-first, supplying each event with the node's diff --git a/scripts/format-swift.sh b/scripts/format-swift.sh index 6b2443a..a4005a1 100755 --- a/scripts/format-swift.sh +++ b/scripts/format-swift.sh @@ -10,7 +10,9 @@ fi case "${1:-}" in --check) - swift_format_args="lint" + # --strict promotes findings (including missing public API + # documentation) to errors so CI fails instead of logging warnings. + swift_format_args="lint --strict" ;; "") swift_format_args="--in-place" From 3fdbd2e924867f5ef317c249c69bfe30657219dd Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:11:13 -0500 Subject: [PATCH 06/12] [Fix] Bind Central deployment to its source run; serialize release concurrency by tag (#42 #43) Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 46 +++++++++++++++++++++++++++++------ scripts/audit-ci-policy.sh | 17 +++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f32493f..b4004db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,17 +14,16 @@ on: description: Tag release run containing the validated artifacts required: true type: string - central-deployment-id: - description: Central deployment id from the source run's Maven stage - required: true - type: string permissions: actions: read contents: read concurrency: - group: release-${{ github.ref }} + # Key on the effective release tag: a tag push and a manual resume for + # the same release share one lock, while resumes for different tags + # dispatched from the same branch do not collide. + group: release-${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release-tag) || github.ref }} cancel-in-progress: false jobs: @@ -262,7 +261,23 @@ jobs: run: | deployment=$(scripts/central-portal.sh upload build/markdown-core-maven-central.zip) echo "deployment-id=$deployment" >>"$GITHUB_OUTPUT" + mkdir -p build/central-deployment + { + echo "deployment-id=$deployment" + echo "release-tag=$GITHUB_REF_NAME" + echo "version=$(cat VERSION)" + } >build/central-deployment/central-deployment.env scripts/wait-central-deployment.sh "$deployment" VALIDATED + - name: Publish the deployment binding for manual resume + # Uploaded even when the VALIDATED wait above fails: a resume + # must recover the deployment this run created, and only this + # run's own record can prove which deployment that is. + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: release-central-deployment + path: build/central-deployment + if-no-files-found: ignore npm-publish: name: Publish Release - ES / npm @@ -377,13 +392,31 @@ jobs: - name: Validate the source run against the tag # A stale or mistyped run id must not attach another version's # artifacts to this tag: the source run must be the push- - # triggered release run of exactly this tag and commit. + # triggered release run of exactly this tag and commit, and it + # must have finished — a resume racing a still-publishing run + # would double-publish. run: | run_info=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID") test "$(jq -r '.path' <<<"$run_info")" = ".github/workflows/release.yml" test "$(jq -r '.event' <<<"$run_info")" = "push" test "$(jq -r '.head_branch' <<<"$run_info")" = "$RELEASE_TAG" test "$(jq -r '.head_sha' <<<"$run_info")" = "$(git rev-parse HEAD)" + test "$(jq -r '.status' <<<"$run_info")" = "completed" + - name: Bind the Central deployment produced by the source run + # The deployment id is not an operator input: it is downloaded + # from the validated source run itself, so a resume can never + # pair this tag's artifacts with another release's Central + # deployment. The recorded tag and version are cross-checked + # against this run's checkout of the protected tag. + run: | + gh run download "$SOURCE_RUN_ID" --name release-central-deployment --dir central-deployment + deployment_id=$(sed -n 's/^deployment-id=//p' central-deployment/central-deployment.env) + bound_tag=$(sed -n 's/^release-tag=//p' central-deployment/central-deployment.env) + bound_version=$(sed -n 's/^version=//p' central-deployment/central-deployment.env) + test -n "$deployment_id" + test "$bound_tag" = "$RELEASE_TAG" + test "$bound_version" = "$(cat VERSION)" + echo "DEPLOYMENT_ID=$deployment_id" >>"$GITHUB_ENV" - name: Download the validated release artifacts run: | mkdir -p release-npm release-files @@ -411,7 +444,6 @@ jobs: # idempotent here: an already-published deployment # short-circuits, so resumes from any failure point converge. env: - DEPLOYMENT_ID: ${{ inputs.central-deployment-id }} MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} run: | diff --git a/scripts/audit-ci-policy.sh b/scripts/audit-ci-policy.sh index 553ea75..c723e1d 100755 --- a/scripts/audit-ci-policy.sh +++ b/scripts/audit-ci-policy.sh @@ -287,6 +287,23 @@ search 'npm publish \./release-npm/\*\.tgz --access public' "$release" search '^ resume-publish:$' "$release" search "if: github.event_name == 'workflow_dispatch'" "$release" search 'gh run download "\$SOURCE_RUN_ID" --name release-npm-package' "$release" +# Tag publication and manual resume for the same release must share one +# concurrency lock: the group derives from the effective release tag for +# both events, never from the dispatch branch ref. +grep -Fq "group: release-\${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.release-tag) || github.ref }}" "$release" +# A resume must reject a source run that is still publishing. +grep -Fq "test \"\$(jq -r '.status' <<<\"\$run_info\")\" = \"completed\"" "$release" +# The Central deployment id is bound to the source run, never operator +# input: the stage records it as a run artifact and the resume downloads +# and cross-checks it against the protected tag and version. +if search 'central-deployment-id' "$release"; then + echo "release resume must not accept a free-form Central deployment id" >&2 + exit 1 +fi +grep -Fq 'name: release-central-deployment' "$release" +search 'gh run download "\$SOURCE_RUN_ID" --name release-central-deployment' "$release" +grep -Fq 'test "$bound_tag" = "$RELEASE_TAG"' "$release" +grep -Fq 'test "$bound_version" = "$(cat VERSION)"' "$release" grep -Fq 'test -s "docs/releases/$(cat VERSION).md"' "$release" grep -Fq -- '--notes-file "docs/releases/$(cat VERSION).md"' "$release" if search -- '--generate-notes' "$release"; then From 63f52a7cbe4c531eb04746ba23e9f56e1c32e1ad Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:17:34 -0500 Subject: [PATCH 07/12] [Fix] Kotlin build: closed host model, jdk-release=17, no published coroutines, Android host loader (#56 #57 #58 #60) Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 +- packages/kotlin-markdown-core/README.md | 17 ++ .../kotlin-markdown-core/build.gradle.kts | 184 +++++++++++------- packages/kotlin-markdown-core/gradle.lockfile | 26 +-- .../nouprax/markdown/core/CBridge.android.kt | 50 ++++- scripts/audit-maven-publications.mjs | 19 ++ scripts/check-kotlin-consumers.sh | 7 + 7 files changed, 228 insertions(+), 85 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86cc3e7..a61355e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -542,7 +542,13 @@ jobs: with: cache: gradle distribution: temurin - java-version: 26 + # Two JDKs: the last listed becomes the default JAVA_HOME + # for Gradle and AGP, while 17 is exported as + # JAVA_HOME_17_X64 so the Maven consumer exercises the + # published artifact on the advertised JVM 17 floor. + java-version: | + 17 + 26 - uses: actions/setup-node@v7 with: node-version: 26.5.0 @@ -554,6 +560,8 @@ jobs: - name: Install Android build dependencies run: sdkmanager "platforms;android-36" "cmake;3.22.1" "ndk;28.2.13676358" - name: Verify Kotlin publication consumers + env: + MARKDOWN_CORE_MAVEN_CONSUMER_JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: pnpm check:kotlin-consumers kotlin-android-test-build: diff --git a/packages/kotlin-markdown-core/README.md b/packages/kotlin-markdown-core/README.md index c71ab00..5972494 100644 --- a/packages/kotlin-markdown-core/README.md +++ b/packages/kotlin-markdown-core/README.md @@ -20,6 +20,23 @@ JVM-only Gradle and Maven consumers can use `com.nouprax:kotlin-markdown-core-jvm:2.0.0`. Published targets are Android API 21 or later, JVM 17, macOS arm64, and Linux x64. +### Android local and Robolectric tests + +On a device or emulator the Android artifact loads its bundled native +library automatically. Local unit tests (including Robolectric) run on the +host JVM, where the Android artifact needs a host build of +`markdown_core_kotlin`. Provide it one of two ways: + +- pass `-Dmarkdown.core.hostNativeLibrary=/path/to/libmarkdown_core_kotlin.dylib` + (or `.so`) to the test JVM, or +- put a host build on the test classpath at + `com/nouprax/markdown/core/native/-/`, the same layout + the JVM artifact uses (for example + `com/nouprax/markdown/core/native/macos-arm64/libmarkdown_core_kotlin.dylib`). + +Without either, the first parse fails with an `IllegalStateException` that +names both remedies. + ## Parse Markdown ```kotlin diff --git a/packages/kotlin-markdown-core/build.gradle.kts b/packages/kotlin-markdown-core/build.gradle.kts index dad901f..757db86 100644 --- a/packages/kotlin-markdown-core/build.gradle.kts +++ b/packages/kotlin-markdown-core/build.gradle.kts @@ -186,27 +186,57 @@ val generatedCanonicalAstSource = layout.buildDirectory.file( "generated/canonicalAstCommonTest/kotlin/com/nouprax/markdown/core/CanonicalAstCases.kt", ) -val hostOs = System.getProperty("os.name").lowercase() -val hostArchitecture = System.getProperty("os.arch").lowercase() -val androidManagedDeviceTestAbi = - when (hostArchitecture) { - "aarch64", "arm64" -> "arm64-v8a" - "amd64", "x86_64" -> "x86_64" - else -> error("Unsupported Android managed-device host architecture: $hostArchitecture") - } -val jvmNativeBuildDirectory = layout.buildDirectory.dir("native/jvm") -val jvmNativeResourceDirectory = layout.buildDirectory.dir("generated/jvmResources") -val desktopPlatform = - when { - System.getProperty("os.name").lowercase().contains("mac") && - hostArchitecture in setOf("aarch64", "arm64") -> "macos-arm64" - - System.getProperty("os.name").lowercase().contains("mac") -> "macos-x64" +// The one closed model of supported build hosts. Every host-dependent +// decision — JNI resource path, native library file name, Kotlin/Native +// test target, managed-device ABI, publish-local target — derives from this +// object, and a host outside the support matrix stops configuration here +// instead of silently producing artifacts labeled for another platform. +data class HostTriple( + val os: String, + val architecture: String, +) { + val platform: String get() = "$os-$architecture" + val kotlinNativeTarget: String get() = if (os == "macos") "macosArm64" else "linuxX64" + val managedDeviceAbi: String get() = if (architecture == "arm64") "arm64-v8a" else "x86_64" + val nativeLibraryFileName: String + get() = if (os == "macos") "libmarkdown_core_kotlin.dylib" else "libmarkdown_core_kotlin.so" +} - System.getProperty("os.name").lowercase().contains("windows") -> "windows-x64" +val supportedHostTriples = + setOf( + HostTriple("macos", "arm64"), + HostTriple("linux", "x64"), + ) - else -> "linux-x64" +val hostTriple = + run { + val osName = System.getProperty("os.name").lowercase() + val architectureName = System.getProperty("os.arch").lowercase() + val os = + when { + osName.contains("mac") -> "macos" + osName.contains("linux") -> "linux" + osName.contains("windows") -> "windows" + else -> osName + } + val architecture = + when (architectureName) { + "aarch64", "arm64" -> "arm64" + "amd64", "x86_64" -> "x64" + else -> architectureName + } + val triple = HostTriple(os, architecture) + require(triple in supportedHostTriples) { + "Unsupported build host: $osName/$architectureName. Supported hosts: " + + supportedHostTriples.joinToString { it.platform } + "." + } + triple } + +val androidManagedDeviceTestAbi = hostTriple.managedDeviceAbi +val jvmNativeBuildDirectory = layout.buildDirectory.dir("native/jvm") +val jvmNativeResourceDirectory = layout.buildDirectory.dir("generated/jvmResources") +val desktopPlatform = hostTriple.platform val nativeOutputDirectory = jvmNativeResourceDirectory.map { it.dir("com/nouprax/markdown/core/native/$desktopPlatform") @@ -291,18 +321,8 @@ fun KotlinNativeTarget.configureNativeBridge() { } } -val hostNativeTest = - when { - hostOs.contains("mac") -> "macosArm64Test" - hostOs.contains("linux") -> "linuxX64Test" - else -> null - } -val hostNativeConformanceTest = - when { - hostOs.contains("mac") -> "macosArm64ConformanceTest" - hostOs.contains("linux") -> "linuxX64ConformanceTest" - else -> null - } +val hostNativeTest = "${hostTriple.kotlinNativeTarget}Test" +val hostNativeConformanceTest = "${hostTriple.kotlinNativeTarget}ConformanceTest" val configureJvmNative = tasks.register("configureJvmNative") { @@ -361,7 +381,13 @@ kotlin { } jvm { - compilerOptions.jvmTarget.set(JvmTarget.JVM_17) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + // jvmTarget only pins the classfile version; -Xjdk-release also + // pins the JDK API surface, so a reference to a post-17 Java API + // fails compilation instead of shipping silently. + freeCompilerArgs.add("-Xjdk-release=17") + } withSourcesJar(publish = true) testRuns["test"].executionTask.configure { useJUnitPlatform() @@ -434,7 +460,6 @@ kotlin { sourceSets { commonMain.dependencies { api(libs.kotlin.stdlib) - api(libs.kotlinx.coroutines.core) } commonTest { kotlin.srcDir(layout.buildDirectory.dir("generated/canonicalAstCommonTest/kotlin")) @@ -532,15 +557,8 @@ val hostNativeLibraryPath = nativeOutputDirectory .get() .asFile - .resolve( - if (desktopPlatform.startsWith("macos")) { - "libmarkdown_core_kotlin.dylib" - } else if (desktopPlatform.startsWith("windows")) { - "markdown_core_kotlin.dll" - } else { - "libmarkdown_core_kotlin.so" - }, - ).absolutePath + .resolve(hostTriple.nativeLibraryFileName) + .absolutePath tasks.withType().matching { it.name == "testAndroidHostTest" }.configureEach { dependsOn(buildJvmNative) filter.excludeTestsMatching("*AstTest*") @@ -658,41 +676,79 @@ tasks.register("verifyKotlinNativePackaging") { val runtimeAarFile = androidRuntimeAar.get().asFile val desktopOutputDirectory = nativeOutputDirectory.get().asFile val expectedDesktopPlatform = desktopPlatform + val desktopLibrary = hostTriple.nativeLibraryFileName + val expectedDesktopArchitecture = + if (hostTriple.os == "macos") "macho-${hostTriple.architecture}" else "elf-${hostTriple.architecture}" inputs.file(runtimeAarFile) doLast { - val expectedAndroidEntries = - setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64").map { - "jni/$it/libmarkdown_core_kotlin.so" + // Reads enough of an ELF or Mach-O header to name the machine architecture, + // so packaging checks verify what a native library is, not merely that a + // file with the right name exists. + fun binaryArchitecture(bytes: ByteArray): String { + require(bytes.size >= 20) { "native library header is truncated" } + fun u16(offset: Int) = (bytes[offset].toInt() and 0xff) or ((bytes[offset + 1].toInt() and 0xff) shl 8) + fun u32(offset: Int) = + (bytes[offset].toInt() and 0xff) or ((bytes[offset + 1].toInt() and 0xff) shl 8) or + ((bytes[offset + 2].toInt() and 0xff) shl 16) or ((bytes[offset + 3].toInt() and 0xff) shl 24) + return when { + bytes[0] == 0x7f.toByte() && bytes[1] == 'E'.code.toByte() && + bytes[2] == 'L'.code.toByte() && bytes[3] == 'F'.code.toByte() -> + when (u16(18)) { + 0x3e -> "elf-x64" + 0xb7 -> "elf-arm64" + 0x28 -> "elf-arm32" + 0x03 -> "elf-x86" + else -> "elf-unknown-${u16(18)}" + } + + u32(0) == 0xfeedfacf.toInt() -> + when (u32(4)) { + 0x0100000c -> "macho-arm64" + 0x01000007 -> "macho-x64" + else -> "macho-unknown-${u32(4)}" + } + + else -> "unknown" } + } + + val expectedAndroidArchitectures = + mapOf( + "arm64-v8a" to "elf-arm64", + "armeabi-v7a" to "elf-arm32", + "x86" to "elf-x86", + "x86_64" to "elf-x64", + ) ZipFile(runtimeAarFile).use { archive -> - val entries = - archive - .entries() - .asSequence() - .map { it.name } - .toSet() - check(entries.containsAll(expectedAndroidEntries)) { - "Android runtime AAR is missing: ${expectedAndroidEntries - entries}" + for ((abi, expectedArchitecture) in expectedAndroidArchitectures) { + val entry = + archive.getEntry("jni/$abi/libmarkdown_core_kotlin.so") + ?: error("Android runtime AAR is missing jni/$abi/libmarkdown_core_kotlin.so") + val header = archive.getInputStream(entry).use { it.readNBytes(20) } + val architecture = binaryArchitecture(header) + check(architecture == expectedArchitecture) { + "Android $abi payload has architecture $architecture, expected $expectedArchitecture" + } } } - val desktopLibrary = - when { - expectedDesktopPlatform.startsWith("macos") -> "libmarkdown_core_kotlin.dylib" - expectedDesktopPlatform.startsWith("windows") -> "markdown_core_kotlin.dll" - else -> "libmarkdown_core_kotlin.so" - } - check(desktopOutputDirectory.resolve(desktopLibrary).isFile) { + val desktopFile = desktopOutputDirectory.resolve(desktopLibrary) + check(desktopFile.isFile) { "JVM native payload is missing for $expectedDesktopPlatform" } + val desktopArchitecture = binaryArchitecture(desktopFile.inputStream().use { it.readNBytes(20) }) + check(desktopArchitecture == expectedDesktopArchitecture) { + "JVM native payload for $expectedDesktopPlatform has architecture " + + "$desktopArchitecture, expected $expectedDesktopArchitecture" + } } } tasks.withType().configureEach { when { - name.contains("LinuxX64") && !hostOs.contains("linux") -> enabled = false - name.contains("MacosArm64") && !hostOs.contains("mac") -> enabled = false + name.contains("LinuxX64") && hostTriple.os != "linux" -> enabled = false + name.contains("MacosArm64") && hostTriple.os != "macos" -> enabled = false } } @@ -703,11 +759,7 @@ tasks.register("publishKotlinToMavenLocal") { "publishKotlinMultiplatformPublicationToMavenLocal", "publishJvmPublicationToMavenLocal", "publishAndroidPublicationToMavenLocal", - if (hostOs.contains("mac")) { - "publishMacosArm64PublicationToMavenLocal" - } else { - "publishLinuxX64PublicationToMavenLocal" - }, + "publish${hostTriple.kotlinNativeTarget.replaceFirstChar { it.uppercase() }}PublicationToMavenLocal", ":packages:kotlin-markdown-core:android-runtime:publishToMavenLocal", ) } diff --git a/packages/kotlin-markdown-core/gradle.lockfile b/packages/kotlin-markdown-core/gradle.lockfile index e8625ad..76e460b 100644 --- a/packages/kotlin-markdown-core/gradle.lockfile +++ b/packages/kotlin-markdown-core/gradle.lockfile @@ -216,7 +216,7 @@ org.jetbrains.kotlin:kotlin-scripting-jvm:2.4.0=kotlinCompilerPluginClasspathAnd org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21=unified-test-platform-android-test-plugin,unified-test-platform-core org.jetbrains.kotlin:kotlin-stdlib-common:1.9.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-launcher org.jetbrains.kotlin:kotlin-stdlib-common:2.2.10=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-gradle-work-action -org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=allBenchmarkSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,androidMainResolvableDependenciesMetadata,appleMainResolvableDependenciesMetadata,commonMainResolvableDependenciesMetadata,jvmBenchmarkResolvableDependenciesMetadata,jvmMainResolvableDependenciesMetadata,ktlintReporter,linuxMainResolvableDependenciesMetadata,linuxX64MainResolvableDependenciesMetadata,macosArm64MainResolvableDependenciesMetadata,macosMainResolvableDependenciesMetadata,metadataCommonMainCompileClasspath,metadataCompileClasspath,metadataNativeMainCompileClasspath,nativeMainResolvableDependenciesMetadata +org.jetbrains.kotlin:kotlin-stdlib-common:2.2.21=ktlintReporter org.jetbrains.kotlin:kotlin-stdlib-common:2.4.0=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestResolvableDependenciesMetadata,androidHostTestResolvableDependenciesMetadata,appleTestResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,jvmTestResolvableDependenciesMetadata,linuxTestResolvableDependenciesMetadata,linuxX64TestResolvableDependenciesMetadata,macosArm64TestResolvableDependenciesMetadata,macosTestResolvableDependenciesMetadata,nativeTestResolvableDependenciesMetadata org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.20=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-core,unified-test-platform-launcher @@ -235,23 +235,23 @@ org.jetbrains.kotlin:kotlin-test:2.4.0=allTestSourceSetsCompileDependenciesMetad org.jetbrains.kotlin:kotlin-tooling-core:2.4.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath org.jetbrains.kotlin:swift-export-embeddable:2.4.0=swiftExportClasspathResolvable org.jetbrains.kotlinx:atomicfu-jvm:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jetbrains.kotlinx:atomicfu-linuxx64:0.26.1=linuxX64CInterop,linuxX64CompilationDependenciesMetadata,linuxX64CompileKlibraries,linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,resolvableLinuxX64CompilationApi -org.jetbrains.kotlinx:atomicfu-macosarm64:0.26.1=macosArm64CInterop,macosArm64CompilationDependenciesMetadata,macosArm64CompileKlibraries,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries,resolvableMacosArm64CompilationApi +org.jetbrains.kotlinx:atomicfu-linuxx64:0.26.1=linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries +org.jetbrains.kotlinx:atomicfu-macosarm64:0.26.1=macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries org.jetbrains.kotlinx:atomicfu:0.22.0=unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-gradle-work-action,unified-test-platform-launcher -org.jetbrains.kotlinx:atomicfu:0.23.1=allBenchmarkSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestResolvableDependenciesMetadata,androidHostTestResolvableDependenciesMetadata,androidMainResolvableDependenciesMetadata,appleMainResolvableDependenciesMetadata,appleTestResolvableDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,jvmBenchmarkResolvableDependenciesMetadata,jvmMainResolvableDependenciesMetadata,jvmTestResolvableDependenciesMetadata,linuxMainResolvableDependenciesMetadata,linuxTestResolvableDependenciesMetadata,linuxX64MainResolvableDependenciesMetadata,linuxX64TestResolvableDependenciesMetadata,macosArm64MainResolvableDependenciesMetadata,macosArm64TestResolvableDependenciesMetadata,macosMainResolvableDependenciesMetadata,macosTestResolvableDependenciesMetadata,metadataCommonMainCompileClasspath,metadataCompileClasspath,metadataNativeMainCompileClasspath,nativeMainResolvableDependenciesMetadata,nativeTestResolvableDependenciesMetadata,swiftPMDependenciesMetadataClasspath -org.jetbrains.kotlinx:atomicfu:0.26.1=linuxX64CInterop,linuxX64CompilationDependenciesMetadata,linuxX64CompileKlibraries,linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,macosArm64CInterop,macosArm64CompilationDependenciesMetadata,macosArm64CompileKlibraries,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries,resolvableLinuxX64CompilationApi,resolvableMacosArm64CompilationApi -org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2=androidCompileClasspath,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,androidMainLintChecksClasspath,androidRuntimeClasspath,jvmBenchmarkCompileClasspath,jvmBenchmarkRuntimeClasspath,jvmCompileClasspath,jvmMainCompileClasspath,jvmMainRuntimeClasspath,jvmRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath +org.jetbrains.kotlinx:atomicfu:0.23.1=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestResolvableDependenciesMetadata,androidHostTestResolvableDependenciesMetadata,appleTestResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,jvmTestResolvableDependenciesMetadata,linuxTestResolvableDependenciesMetadata,linuxX64TestResolvableDependenciesMetadata,macosArm64TestResolvableDependenciesMetadata,macosTestResolvableDependenciesMetadata,nativeTestResolvableDependenciesMetadata +org.jetbrains.kotlinx:atomicfu:0.26.1=linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2=androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestResolvableDependenciesMetadata org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2=androidCompileClasspath,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,androidMainLintChecksClasspath,androidRuntimeClasspath,jvmBenchmarkCompileClasspath,jvmBenchmarkRuntimeClasspath,jvmCompileClasspath,jvmMainCompileClasspath,jvmMainRuntimeClasspath,jvmRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2=androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset,swiftExportClasspathResolvable org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestResolvableDependenciesMetadata org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle -org.jetbrains.kotlinx:kotlinx-coroutines-core-linuxx64:1.10.2=linuxX64CInterop,linuxX64CompilationDependenciesMetadata,linuxX64CompileKlibraries,linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,resolvableLinuxX64CompilationApi -org.jetbrains.kotlinx:kotlinx-coroutines-core-macosarm64:1.10.2=macosArm64CInterop,macosArm64CompilationDependenciesMetadata,macosArm64CompileKlibraries,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries,resolvableMacosArm64CompilationApi -org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2=allBenchmarkSourceSetsCompileDependenciesMetadata,allSourceSetsCompileDependenciesMetadata,allTestSourceSetsCompileDependenciesMetadata,androidCompileClasspath,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestResolvableDependenciesMetadata,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestResolvableDependenciesMetadata,androidHostTestRuntimeClasspath,androidMainLintChecksClasspath,androidMainResolvableDependenciesMetadata,androidRuntimeClasspath,appleMainResolvableDependenciesMetadata,appleTestResolvableDependenciesMetadata,commonMainResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,jvmBenchmarkCompileClasspath,jvmBenchmarkResolvableDependenciesMetadata,jvmBenchmarkRuntimeClasspath,jvmCompileClasspath,jvmMainCompileClasspath,jvmMainResolvableDependenciesMetadata,jvmMainRuntimeClasspath,jvmRuntimeClasspath,jvmTestCompileClasspath,jvmTestResolvableDependenciesMetadata,jvmTestRuntimeClasspath,linuxMainResolvableDependenciesMetadata,linuxTestResolvableDependenciesMetadata,linuxX64CInterop,linuxX64CompilationDependenciesMetadata,linuxX64CompileKlibraries,linuxX64MainResolvableDependenciesMetadata,linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,linuxX64TestResolvableDependenciesMetadata,macosArm64CInterop,macosArm64CompilationDependenciesMetadata,macosArm64CompileKlibraries,macosArm64MainResolvableDependenciesMetadata,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries,macosArm64TestResolvableDependenciesMetadata,macosMainResolvableDependenciesMetadata,macosTestResolvableDependenciesMetadata,metadataCommonMainCompileClasspath,metadataCompileClasspath,metadataNativeMainCompileClasspath,nativeMainResolvableDependenciesMetadata,nativeTestResolvableDependenciesMetadata,resolvableLinuxX64CompilationApi,resolvableMacosArm64CompilationApi,swiftPMDependenciesMetadataClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-linuxx64:1.10.2=linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries +org.jetbrains.kotlinx:kotlinx-coroutines-core-macosarm64:1.10.2=macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestResolvableDependenciesMetadata,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestResolvableDependenciesMetadata,androidHostTestRuntimeClasspath,appleTestResolvableDependenciesMetadata,commonTestResolvableDependenciesMetadata,jvmTestCompileClasspath,jvmTestResolvableDependenciesMetadata,jvmTestRuntimeClasspath,linuxTestResolvableDependenciesMetadata,linuxX64TestCInterop,linuxX64TestCompilationDependenciesMetadata,linuxX64TestCompileKlibraries,linuxX64TestResolvableDependenciesMetadata,macosArm64TestCInterop,macosArm64TestCompilationDependenciesMetadata,macosArm64TestCompileKlibraries,macosArm64TestResolvableDependenciesMetadata,macosTestResolvableDependenciesMetadata,nativeTestResolvableDependenciesMetadata org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3=unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0=androidLintTool,unified-test-platform-android-test-plugin-result-listener-gradle org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.10.2=androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath @@ -264,8 +264,8 @@ org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.3=swiftExportClasspathR org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1=ktlintReporter org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.4.1=ktlintReporter org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1=ktlintReporter -org.jetbrains:annotations:13.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathAndroidDeviceTest,kotlinCompilerPluginClasspathAndroidHostTest,kotlinCompilerPluginClasspathAndroidMain,kotlinCompilerPluginClasspathJvmBenchmark,kotlinCompilerPluginClasspathJvmMain,kotlinCompilerPluginClasspathJvmTest,kotlinCompilerPluginClasspathMetadataCommonMain,kotlinCompilerPluginClasspathMetadataMain,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset,swiftExportClasspathResolvable -org.jetbrains:annotations:23.0.0=allTestSourceSetsCompileDependenciesMetadata,androidCompileClasspath,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestResolvableDependenciesMetadata,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,androidLintTool,androidMainLintChecksClasspath,androidRuntimeClasspath,jvmBenchmarkCompileClasspath,jvmBenchmarkRuntimeClasspath,jvmCompileClasspath,jvmMainCompileClasspath,jvmMainRuntimeClasspath,jvmRuntimeClasspath,jvmTestCompileClasspath,jvmTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher +org.jetbrains:annotations:13.0=androidCompileClasspath,androidMainLintChecksClasspath,androidRuntimeClasspath,jvmBenchmarkCompileClasspath,jvmBenchmarkRuntimeClasspath,jvmCompileClasspath,jvmMainCompileClasspath,jvmMainRuntimeClasspath,jvmRuntimeClasspath,kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathAndroidDeviceTest,kotlinCompilerPluginClasspathAndroidHostTest,kotlinCompilerPluginClasspathAndroidMain,kotlinCompilerPluginClasspathJvmBenchmark,kotlinCompilerPluginClasspathJvmMain,kotlinCompilerPluginClasspathJvmTest,kotlinCompilerPluginClasspathMetadataCommonMain,kotlinCompilerPluginClasspathMetadataMain,kotlinKlibCommonizerClasspath,ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset,swiftExportClasspathResolvable +org.jetbrains:annotations:23.0.0=allTestSourceSetsCompileDependenciesMetadata,androidDeviceTestCompileClasspath,androidDeviceTestLintChecksClasspath,androidDeviceTestResolvableDependenciesMetadata,androidDeviceTestRuntimeClasspath,androidHostTestCompileClasspath,androidHostTestLintChecksClasspath,androidHostTestRuntimeClasspath,androidLintTool,jvmTestCompileClasspath,jvmTestRuntimeClasspath,unified-test-platform-android-device-provider-ddmlib,unified-test-platform-android-driver-instrumentation,unified-test-platform-android-test-plugin,unified-test-platform-android-test-plugin-host-additional-test-output,unified-test-platform-android-test-plugin-host-apk-installer,unified-test-platform-android-test-plugin-host-coverage,unified-test-platform-android-test-plugin-host-device-info,unified-test-platform-android-test-plugin-host-emulator-control,unified-test-platform-android-test-plugin-host-logcat,unified-test-platform-android-test-plugin-result-listener-gradle,unified-test-platform-core,unified-test-platform-gradle-work-action,unified-test-platform-launcher org.junit.jupiter:junit-jupiter-api:5.10.1=allTestSourceSetsCompileDependenciesMetadata,jvmTestCompileClasspath,jvmTestResolvableDependenciesMetadata,jvmTestRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.10.1=jvmTestRuntimeClasspath org.junit.platform:junit-platform-commons:1.10.1=allTestSourceSetsCompileDependenciesMetadata,jvmTestCompileClasspath,jvmTestResolvableDependenciesMetadata,jvmTestRuntimeClasspath @@ -279,4 +279,4 @@ org.ow2.asm:asm-commons:9.9=androidLintTool org.ow2.asm:asm-tree:9.9=androidLintTool org.ow2.asm:asm:9.9=androidLintTool org.slf4j:slf4j-api:2.0.7=ktlint -empty=androidApis,androidDeviceTestImplementationDependenciesMetadata,androidHostTestImplementationDependenciesMetadata,androidJdkImage,androidMainImplementationDependenciesMetadata,androidTestUtil,annotationProcessorClasspath,appleMainImplementationDependenciesMetadata,appleTestImplementationDependenciesMetadata,benchmarkKotlinScriptDefExtensions,commonMainImplementationDependenciesMetadata,commonTestImplementationDependenciesMetadata,coreLibraryDesugaring,deviceTestKotlinScriptDefExtensions,hostTestKotlinScriptDefExtensions,jvmBenchmarkAnnotationProcessor,jvmBenchmarkImplementationDependenciesMetadata,jvmMainAnnotationProcessor,jvmMainImplementationDependenciesMetadata,jvmTestAnnotationProcessor,jvmTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathLinuxX64Main,kotlinCompilerPluginClasspathLinuxX64Test,kotlinCompilerPluginClasspathMacosArm64Main,kotlinCompilerPluginClasspathMacosArm64Test,kotlinCompilerPluginClasspathMetadataNativeMain,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,lintChecks,lintPublish,linuxMainImplementationDependenciesMetadata,linuxTestImplementationDependenciesMetadata,linuxX64MainImplementationDependenciesMetadata,linuxX64TestImplementationDependenciesMetadata,macosArm64MainImplementationDependenciesMetadata,macosArm64TestImplementationDependenciesMetadata,macosMainImplementationDependenciesMetadata,macosTestImplementationDependenciesMetadata,nativeMainImplementationDependenciesMetadata,nativeTestImplementationDependenciesMetadata,resolvableLinuxX64TestCompilationApi,resolvableMacosArm64TestCompilationApi,swiftPMDependenciesForLockFilesMetadataClasspath,testKotlinScriptDefExtensions +empty=androidApis,androidDeviceTestImplementationDependenciesMetadata,androidHostTestImplementationDependenciesMetadata,androidJdkImage,androidMainImplementationDependenciesMetadata,androidTestUtil,annotationProcessorClasspath,appleMainImplementationDependenciesMetadata,appleMainResolvableDependenciesMetadata,appleTestImplementationDependenciesMetadata,benchmarkKotlinScriptDefExtensions,commonMainImplementationDependenciesMetadata,commonTestImplementationDependenciesMetadata,coreLibraryDesugaring,deviceTestKotlinScriptDefExtensions,hostTestKotlinScriptDefExtensions,jvmBenchmarkAnnotationProcessor,jvmBenchmarkImplementationDependenciesMetadata,jvmMainAnnotationProcessor,jvmMainImplementationDependenciesMetadata,jvmTestAnnotationProcessor,jvmTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathLinuxX64Main,kotlinCompilerPluginClasspathLinuxX64Test,kotlinCompilerPluginClasspathMacosArm64Main,kotlinCompilerPluginClasspathMacosArm64Test,kotlinCompilerPluginClasspathMetadataNativeMain,kotlinNativeCompilerPluginClasspath,kotlinScriptDefExtensions,lintChecks,lintPublish,linuxMainImplementationDependenciesMetadata,linuxMainResolvableDependenciesMetadata,linuxTestImplementationDependenciesMetadata,linuxX64MainImplementationDependenciesMetadata,linuxX64MainResolvableDependenciesMetadata,linuxX64TestImplementationDependenciesMetadata,macosArm64MainImplementationDependenciesMetadata,macosArm64MainResolvableDependenciesMetadata,macosArm64TestImplementationDependenciesMetadata,macosMainImplementationDependenciesMetadata,macosMainResolvableDependenciesMetadata,macosTestImplementationDependenciesMetadata,nativeMainImplementationDependenciesMetadata,nativeMainResolvableDependenciesMetadata,nativeTestImplementationDependenciesMetadata,resolvableLinuxX64TestCompilationApi,resolvableMacosArm64TestCompilationApi,swiftPMDependenciesForLockFilesMetadataClasspath,testKotlinScriptDefExtensions diff --git a/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt index e261020..955533d 100644 --- a/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt +++ b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt @@ -50,14 +50,54 @@ internal actual class CSession actual constructor( } private object AndroidNativeLoader { - private val loaded: Unit = + private val loaded: Unit = load() + + fun ensureLoaded() = loaded + + private fun load() { if (System.getProperty("java.vm.name").orEmpty().contains("Dalvik", ignoreCase = true)) { System.loadLibrary("markdown_core_kotlin") - } else { - System.load(requireNotNull(System.getProperty("markdown.core.hostNativeLibrary"))) + return } - - fun ensureLoaded() = loaded + // Host-JVM execution (Robolectric and other local unit tests). Three + // supported layers: an explicit library path, a host build of the + // library on the classpath at the desktop resource layout, and an + // actionable failure naming both remedies. + val explicit = System.getProperty("markdown.core.hostNativeLibrary") + if (explicit != null) { + System.load(explicit) + return + } + val os = System.getProperty("os.name").lowercase() + val architecture = System.getProperty("os.arch").lowercase() + val platform = + when { + os.contains("mac") && architecture in setOf("aarch64", "arm64") -> "macos-arm64" + os.contains("mac") && architecture in setOf("x86_64", "amd64") -> "macos-x64" + os.contains("linux") && architecture in setOf("x86_64", "amd64") -> "linux-x64" + os.contains("windows") && architecture in setOf("x86_64", "amd64") -> "windows-x64" + else -> null + } + val filename = System.mapLibraryName("markdown_core_kotlin") + val resource = platform?.let { "/com/nouprax/markdown/core/native/$it/$filename" } + val stream = resource?.let { AndroidNativeLoader::class.java.getResourceAsStream(it) } + if (stream == null) { + throw IllegalStateException( + "Markdown Core's Android artifact is running on a host JVM without its native " + + "library. Either set -Dmarkdown.core.hostNativeLibrary=/path/to/$filename " + + "to a host build of markdown_core_kotlin, or put a host build on the test " + + "classpath at ${resource ?: "com/nouprax/markdown/core/native//$filename"}.", + ) + } + val directory = java.nio.file.Files.createTempDirectory("markdown-core-") + val library = directory.resolve(filename) + // deleteOnExit removes entries in reverse registration order, so the + // directory must be registered before its child. + directory.toFile().deleteOnExit() + stream.use { java.nio.file.Files.copy(it, library) } + library.toFile().deleteOnExit() + System.load(library.toAbsolutePath().toString()) + } } internal object JvmNative { diff --git a/scripts/audit-maven-publications.mjs b/scripts/audit-maven-publications.mjs index c017598..f4531f5 100755 --- a/scripts/audit-maven-publications.mjs +++ b/scripts/audit-maven-publications.mjs @@ -151,6 +151,19 @@ function auditPom(file, coordinate) { !/(?:junit|kotlin-test|testng|mockito|kotest|hamcrest|opentest4j)/iu.test(pom), `${path.basename(file)} publishes a test framework dependency` ); + // The library's only external dependency is the Kotlin standard + // library: every declared POM dependency must come from the project + // itself or be kotlin-stdlib, so an accidental api(...) addition fails + // the audit instead of widening every consumer's dependency graph. + for (const match of pom.matchAll(/[\s\S]*?<\/dependency>/gu)) { + const dependency = match[0]; + const group = dependency.match(/(.*?)<\/groupId>/u)?.[1]; + const artifact = dependency.match(/(.*?)<\/artifactId>/u)?.[1]; + assert.ok( + group === "com.nouprax" || (group === "org.jetbrains.kotlin" && artifact?.startsWith("kotlin-stdlib")), + `${path.basename(file)} publishes unexpected dependency ${group}:${artifact}` + ); + } } function auditModule(file) { @@ -180,6 +193,12 @@ function auditModule(file) { !/(?:junit|kotlin-test|testng|mockito|kotest|hamcrest|opentest4j)/iu.test(coordinate), `${path.basename(file)} publishes test dependency ${coordinate}` ); + assert.ok( + dependency.group === "com.nouprax" || + (dependency.group === "org.jetbrains.kotlin" && + String(dependency.module ?? "").startsWith("kotlin-stdlib")), + `${path.basename(file)} publishes unexpected dependency ${coordinate}` + ); } } } diff --git a/scripts/check-kotlin-consumers.sh b/scripts/check-kotlin-consumers.sh index 146c24e..5a2a33d 100755 --- a/scripts/check-kotlin-consumers.sh +++ b/scripts/check-kotlin-consumers.sh @@ -40,6 +40,13 @@ fi -p packages/kotlin-markdown-core/consumers/jvm-gradle run "$gradle" --warning-mode=fail "$property" "-PconsumerRepository=$repository" \ -p packages/kotlin-markdown-core/consumers/android assembleDebug +# The Maven consumer runs on the advertised JVM floor when the caller +# provides one (CI passes a JDK 17 home); Gradle consumers keep the +# toolchain JDK for AGP. +if [ -n "${MARKDOWN_CORE_MAVEN_CONSUMER_JAVA_HOME:-}" ]; then + export JAVA_HOME="$MARKDOWN_CORE_MAVEN_CONSUMER_JAVA_HOME" + "$JAVA_HOME/bin/java" -version 2>&1 | head -1 +fi MAVEN_USER_HOME="$root/build/maven-user-home" \ MAVEN_OPTS="${MAVEN_OPTS:+$MAVEN_OPTS }--enable-native-access=ALL-UNNAMED" \ "$root/mvnw" --batch-mode --no-transfer-progress \ From f5a2fed5eaa91ce8acf3b6cbff0860bd1acdaffc Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:35:30 -0500 Subject: [PATCH 08/12] [Fix] Kotlin publishing: Dokka javadoc, Java facade, JVM ABI snapshot gate (#54 #55 #59) Co-Authored-By: Claude Fable 5 --- gradle/libs.versions.toml | 2 + gradle/verification-keyring.keys | 365 +++++++ gradle/verification-metadata.xml | 904 +++++++++++++++++- .../kotlin-markdown-core/build.gradle.kts | 156 ++- .../src/main/java/consumer/Main.java | 65 +- packages/kotlin-markdown-core/jvm-abi.txt | 587 ++++++++++++ .../nouprax/markdown/core/CBridge.android.kt | 9 +- .../nouprax/markdown/core/model/Document.kt | 7 + .../com/nouprax/markdown/core/model/Markup.kt | 4 + .../nouprax/markdown/core/model/MarkupID.kt | 21 +- .../markdown/core/model/ParseOptions.kt | 30 +- .../nouprax/markdown/core/session/Commit.kt | 6 + .../markdown/core/session/MarkupSession.kt | 281 +++--- .../markdown/core/WalkerTraversalTest.kt | 7 +- scripts/audit-maven-publications.mjs | 28 +- 15 files changed, 2286 insertions(+), 186 deletions(-) create mode 100644 packages/kotlin-markdown-core/jvm-abi.txt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a60dd8..21c7fd1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ jdk = "26" jvm-bytecode = "17" kotlin = "2.4.0" kotlin-stdlib = "2.2.21" +dokka = "2.1.0" kotlinx-coroutines = "1.10.2" ktlintCli = "1.8.0" ktlintPlugin = "14.2.0" @@ -16,6 +17,7 @@ android-library = { id = "com.android.library", version.ref = "agp" } android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintPlugin" } [libraries] diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index f242ffe..bd08705 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -484,6 +484,83 @@ vMxKy4GRZS18bXDI3vS6gRDNJDCqBYIhp13Os9k+ZpnwK3PPIHv4l1I0i0EHZKk= =E1B4 -----END PGP PUBLIC KEY BLOCK----- +pub BF1518E0160788A2 +uid Karl Heinz Marbaise (ASF Key) + +sub C163B490C5CDC967 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsNNBFT3VuYBIADPQxdM6fJajMSyeiKbfpSjllBkGA16DE9IFJ76B6281k8sfya2 +k6UOAKNIprxY3JCRulbnkn3BcdbY1vZDhaf/fbdkvJ+o/XVzrxojq1jS3tvSq95L +qOzITCHK1rSApWUuVFTlvdhJy8rVlAVfiV5Qkb2EFBQtqQPIdyjRDk7NkM5CfzQj +E54xPCAM/oVtr7bCAjtUKkOjXYjv/L33pSOeig1+Dah1OjDpkqmUspiFWJKJfYyw +1MvR/lZTAm+aZpfx40vlBIkhBtJBsufjBwxLOJXUzPHC8io103K9EjHgpxeuKj21 +HvbT4EbL8jMEts4uvkjGhQoa0yZ/X0//VOA+s9vNE4egPtQSZR7gT8y12s1x9EE4 +nn7XGCfYYbbkpFGLKKHr37wRkzQ0ABzLwPuOZMvq0jAYtS4BA5BRzT63JTTAH1gT +O141lSmRc36Xxqa0/KFN+UEsk2tNZy8c+KA9zRYW/rZfPz90S7V+BZqBfE/oyMwf +394FOVAcpldYWmlBzQQsrHIZ7c5Z3gygN8naQuHcqO89SlYOkvMk2jxmvvCi7qzG +W8j61lwrzV/HytULYRW/3gCbbjtBKP8IgeYVTzE+JTzZbd4FvUXv5jWtN8cxcBG7 +A1UaFG7n3XbXdGaifQ/qNrXUOQxqeFv1PTdxNhPSRG9/TvVmuGsTXXll++gc3J6P +nCW378EE9wqn9ti20TIaBus1+teZv3BjwFd5msSytrvDewLYrhqDyOaBR01ux4Ea +5FYAidqIJ9UqmGfLCJy+xF2re1Ra6Vmfk/jlkCCIyIp57+K+o0a6XIC9+fZXmdqY +PmCThaqTJxQix54Di+FmFmceiURMx70dyCs5Py51vlszDoOttJxGpU2qkVGLdnlu +BItKLvzt5e30HOnpj8KC37/2u7ahWBfFtwrflIWoC92DElp+dVdDfSw0wwvxadY+ +7/nGon/IAi+Nk7vQn0ngJA9QA5gm+bPuC4w1H+Nw2mUmoUvB/fd/fYS/bm2ggbyw +mNtaP1s9AQsKZ82SkRQgWiHi4RgV6xSSKCzA3sprYjGGFG3hCaRfdcXnfN75yi9p +rQv/H6GFvjKDcVRYCAWwIRRixDBnSFJuAPVlGUer2GLzimy+RMJx1owXjigW9ZZf +5Cm4+0JRESZqRUReY2hcUD5GE9bUCCiheHma8shU1/FDRYY87OFdXmkKSPZKSghO +4ANGScK0LWx9L5dXqlQM1uhrM+SW3Pd4eKmmsVVj1YhAgrJcYPJKEXWftuG2VdcM ++U2tTuYLAQz1uSUf1ShB60xTdFYu355qQfRFey9GAG20iPNY65ktkxWXbLBqCeCm +XyWiPPHo53w0GPbbKt1J0ycE/5HB3iKg5UQrABEBAAG0NUthcmwgSGVpbnogTWFy +YmFpc2UgKEFTRiBLZXkpIDxraG1hcmJhaXNlQGFwYWNoZS5vcmc+zsNNBFT3VuYB +IACYmSu8e3gsb5h8CBt9xPi8RDfMWgOAhDCWV8trJwycStlhSh1hUQ0MdH9CPELX +oi7DmKo0SG/+zPQfJ26zLumKUB9j27UqgHZJRxXqyYFaVizmo+mWrOtiwR2dpF1g +P37VT1OsELcLQYl5YKaJFLMCapjoZhUXZ7wleUY5dYijoaH74glU2TdFvcN2u46p +O0vqTnWe7YdANBPfVs1lrZr4Mml0OEVVGuUnBBxIAtuvIi6C/Sgbzd7AL/kXUJD5 +dqtWtXwO0OeBASBPjb5PfStjN56k8qJ8H4M9PflBc2jy+982j/aTZGAVsXw9hGJK +wwI25AI59MAKHzbuiqs3Vfm2xNqZ6PipvkhQ0HdlnKkMIkUsyr9sdQKWsF7JX5gS +cQEJ2jFptIYsD4548ijp/kMS7Lt2/DW0gh87S5HODy4VAhcUTLSzXMdsy9TYktJy +/cRC1cR9yNMp4/1mF9McdBvcQimGmF2Atlwr9OGJsF8weX/pQCgV4lFv7rLo0T/k +JzEa2bOSJrs1mrm/XHdwcyVEBA4rHXwQShOXyEym6tBWg0C4GhCdzCsjZTKtkn+8 +wAcyDx7V1z+jhMICZOtCNJCdcSV3OEg76b/vpYsiJxncEu6KG6S0ej/BY9U6laRd +Zx7ShV/9mLlOHgFHBZ6ZFT7ThpDU9SUTAtty/N/60jgDBiXzItIiOADU0eLw1+av +5bFGCoTI1uGqJkd5PHvTU/UT6zokzcEbNe1VCj16slCaalYIZMA1xjoBcz+fXM1D +JDTXCR4omNN/S9jlBIQ6frs5easBbIwLPqVYjh1oGdqvC16qbycKVYoYHBoWZMrf +EWjUcimYhL7fQOJb4aM3eBk7hXzdK80yVelkXxEjEZUGK3n5nO7z63rr7eXXtltA +FwtI+0s/QUpv6FeuWltSwEUH9FMWESsNgtkGhSM6VLOTx81VuHprlYL7IszZc8rH +od07BeQBnRaw+w2gp8+HRssYpK8nYaDp+jKC/94df61h0mlhzCe+QsQOGY+QZ8dA +u/z5pbVvGMRMpIsxvX0o2vWKNmRSmazdFKg8GoyWDOYIf42gGZuBGOaaNLCcviYS +MXp/1nwhcgoLEhe2kv/jrWT5+VhyFOTBzKEAfLNdZIiMHaxB7gjuop9zuwZzWuBl +6VwWYYFm+cGlBHaSn45R+Iy39Ol7SlthH4AdmeQLFNY6iiTzSIL7LcFvObJGRq5s +Q11DYERDheIsGmDqhhLwDJEWezCZbpcjbV5VfUik3pNB74ipJ+jBisd/9xcwgWKi +niLH1N3el95Ll2AqFj7qIFoUuL2NvMoD1bi5/e3RU8n3G3BIOhph/s5Kz6Sh0xgB +HbooL7MVqe/vHJwcrL3cve9pABEBAAHCw3wEGAEKAA8FAlT3VuYCGwwFCQPCZwAA +IQkQvxUY4BYHiKIWIQSunlP8KP8qsQEic9C/FRjgFgeIomCmH/sEtcYzJC3qPx4R +IAqw87ODgPWpDDCeuizLRI1Z049h17Ptllq2uCXDBJeOx1tHh78SFF3bLreDN1qO +kuSL/uc3KD82SIpVQoh3xIoY6WeR/HijBTgNWNuy4ub+azUG/zeM1edaCEKOJvbc +o4BI+NRqFPD9Wk2Is/mBZ2cjAbhPGguzqJgsdf96U4+s0DTI3PnkixTEQ4fa54g0 ++XtoTXlNQ8iN+gQKrobye2dr+G2xJBDA80Bn1bKSc7jqCB/MrOlChBLcJBPebtnW +MR320JpytJripcanEpZVshg11ZxSmXpUZAfl+UPeVOqJJdZbl1T2DxkojYnvKSL+ +w+9ocfXXBgUasbLL5nVEQHJcBLfI4eb9l2jdRR3DdBvdUO0xC09Kwz6pw72ptNg4 +KQHu+to6eq96sdAWTfAKHfNyB2MrJhsLtRVdV5tjDe7mpONbW0EArL3LC/TyEH1g +W9XWQoYQqBpGC//OCkIg+Aah+mwJP8HtoWwPcrSZprrpDjOuIwCTh6QHaAL+P9RO +hVg0Eejniq59jvb+aJDuSwEAMGKA9M5v6CMnHNRb29Ul7xnx9BWH82SD8uilAYIq +rtn/7FCQaFoL5h65xtJHtbm9fFvS41llCvQ4ts59x8opcqrkKBUcCQdJYJIuhauE +A8a6dwPAqfuZQA/d6/R4R3ixuGSjLsJucvEiY7mc2znLcJ8oaK/UjW5BN6aC16eJ +6KFeN6gFezmsd7cJpcZ52JVMUXaMWbpCmZwxPqZWUndZG6wmShSghwy/ZJci0mFU +T9mM02SET2imuM+duam6ISnRS9lnf7Zh5IEnjViKTgwZWgApgw3rUxKrhlJhSAQ/ +8OP7DEUWFmhoRi/CdE/jhKHw4ZWBk3T3u3QJp76tFODqslVq3H9jM6Z266dpGZWS +yU5a/P4HBt/XQiJITGe0ljqFXslQaRyiGs0hO7XhPkDT/bH9PJnzC2t5OGzZufId +Tgw5FthEN5CW2nhQ8TfEoiM6FxnvT8cz4SbeJ71SysAf063Renq/wuBo01FGyarU +thanrfQq7DebLUIK3D9w4V9Up/AQUHUt/LtZk9T1bN0r1716ym0joXq66rQDbBj8 +b8tjKcTDJB/BIynsgjKuM9PM03OFYbkjfcDk/UHbhXnr7+0o4naEWXOeBs8WLscr +Pz0ceeJiw8iAc2TsqDQSDCIGQdQiNfSXaxFh1ZHC5IlR5Rvm03meVlPqjBfVh5N1 +DjirNire/h1uiCIg+iQt4Syrb/Ev9A8zRmsVMaZ9LRTEq2m4UENd21UclLmDU369 +q3x4Gr+2VwUUVJc3ErUZmhgwUDQMOFdU3MXUVViyjGTpI/YS12D5/mppKyqICYAc +v7q0QO7B +=9OLO +-----END PGP PUBLIC KEY BLOCK----- + pub BF984B4145EA13F7 uid Egor Andreevici @@ -569,6 +646,34 @@ dfV4cuo= =qNfM -----END PGP PUBLIC KEY BLOCK----- +pub C9FBAA83A8753994 +uid Tatu Saloranta (cowtowncoder) + +sub AFF3E378166B1F0F +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsBNBFeWvEwBCAC7oSQ7XqcGDc6YL4KAGvDVZYigcJmv0y5hWT4wv9ABP4Jhzr1H +NDmmGyWzhzTeMxwuZnc9vhxCQRwyxj3gGI5lYPEARswbi2fWk//78/3Wk+YMHJw3 +/1EO3VqvvDUt39gbaSqMCQNHctnFdb2QYZ7nRFTQeCqG/wyMdB05beqEnWEXzjeP +FDF9y6gXkELn0lxUm2TKO8tU3h96TCuutDKJ0aE00lOeh/MbEaGHEbIU8kdfui6U +znZ1X80EWbkCY8cKxEZHKD0aONSVHXwE6nETvFW9/9+K+sj/I7ytlyxwHsaQpi1H +6aRGnq013VsIECrwkhmXBsLLXNjmhER+LkcDABEBAAG0NVRhdHUgU2Fsb3JhbnRh +IChjb3d0b3duY29kZXIpIDx0YXR1LnNhbG9yYW50YUBpa2kuZmk+zsBNBFeWvEwB +CADfbjqRN1GvSf9VkjDDWBqX3hILiPx3DKPiSFmSsAoiLSEkP8gRDBDy6po/Oez9 +q+bgb9Sk+iGifJvbVNZr7+88LSxQECsrVL+ZgTAZ2LkqABQJ8XYh/dTO6BMJ5rSL +H/YN6bO9V4NjYod67W5uYox+aCp6keE7ia7eBHck2wyqxikCqvVzmAduPCkkNSRu +OwLkOguduXfwb7Cg1RnacMMEfDd9t+a6ytY/8JFu01G++VVxFHO3vs//RINrm0O4 +xKPkT1+Ocd4+h3DlhhRvRXvI+MKeM0ud0OpUVCBKCZqbFKTVgoRpsKC925ZRUSqb +myBcIkqyYjzD1adaZdkq4PLFABEBAAHCwGUEGAECAA8FAleWvEwCGwwFCQeEzgAA +CgkQyfuqg6h1OZQ6swf/Vm0ndBcvdK0qCoubR/WOsynS1wdQ2rGKJC5oVsUN4YVC +Zhg9OMhwMSO3EOBPdxtq4A8bSZp/8ZWmHLohE8QHD8AgaQBtRZyNkvMRiN157XGk +PEBRWdHw6XAvx/lE31W+19qFRnAE8BbERE3gieJcG4CKWy5CyzXnjSM+znZvDQ8J +7MfG+LxKbr7zUHQH9ZWsk8V9D+GXDgpuyZc4ct1tNDLcn6784FubcdrD3RLMiOAY +SSgKSgoELRzB8zZds+WKFuiAYXZSdzCbjJJ2VqnJnQtYHN7Z5r3ySqbG7w3rRen+ +Ett6PX9Hrvc1MTy/UUdb3s08C0wD0x+ZzFzqUXKTZg== +=ZkpY +-----END PGP PUBLIC KEY BLOCK----- + pub CA80D1F0EB6CA4BA uid Sylwester Lachiewicz @@ -1182,6 +1287,38 @@ UUyruTMY5XMGD/VGCOLweOotdxJF6J5yWErznxlExP5YBIHvIQljCyU= =mqgl -----END PGP PUBLIC KEY BLOCK----- +pub F42E87F9665015C9 +uid Jonathan Hedley + +sub 6064B04A9DC688E0 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDiBEtsF2oRBACcai1CJgjBfgteTh61OuTg4dxFwvLSxXy8uM1ouJw5sMx+OKR9 +Uq6pAZ1+NAUckUrha9J6qhQ+WQtaO5PI1Cz2f9rY+FBRx3O+jeTaCgGxM8mGUM5e +9lFqWQOAuCIWB1XPzoy5iTRDquD2q9NrgldpcwLX3EVtloIPKF7QLq72cwCgrb5X +R25dB8PUdZKUt2TtJbjB+SMD/1UzAPirgX0/RpL9wUR1i14yIrTfpFP/yM9PE4ij +qcZ1yafVdw64E1k5W4k+Pyl4D8DvSJvbJHvYjg8/G9V66WzaKcv+987fetUuePvY +/rwxBPztqq8y6+hjBc8QVhZGWmAoGGEFO6MIGsSyN5ohqPMpNXkczIo+NMvDxGzz +ld5ZA/9awGTsigBdpBK2F6GOmbvBv+Xebu9rbaJvBvP+npNx01s/f5sHPCxmBTFk +m1vtaMdZ29RovrWPSZRj8WWes0bcisw80250r1CBlYzGzqEVZ7b0Hh2RfkfaxbYh +wikyfTfA2iX8TUGBgirsZbyegjUadElhwFNDASnvLTEuQKeVLLQlSm9uYXRoYW4g +SGVkbGV5IDxqb25hdGhhbkBoZWRsZXkubmV0Ps7BTQRLbBdqEAgA0sZ0JZvWoKIG +b+o6MOwI6p3uMb+iWBwdYfoh2RPnUZdBwGhJjp32CiTt2Y3qYEcqC5NvF5FWdx1m +5KOQe1O+QFoqPKnC1bPj9uZOjLVql7x5tSwCePIaMNB+fMxEh5hYwLWtBz8nrdCP +gwm+nAwecoE8YfrpmrXZk/YLak54FOeEwLYaP8E4u2FHiEqN+WmKMjIRwLzVpYAr +WRCbTLhSSKyRBy7UxEovUH9mIa4YuU4Pb2R64LwopMHCBm5ow0U8kCw8vpW40GrB +c/2eaIeXCX2XJ77E9s9ZPgW6MoJ6Ic1xV6voLJKIEV8t44deKNSwDfVNZHxyemaK +a8/GgpjU5wADBQf/UzL5lXRmyTdJqRvHIfUV3g4A3X77d3vOroab8KKw4MFy2LiT +ioN7btKKxE97Jjp21YZFd7Kpmfu2i/kr9QVJo+DSxe2p2xcQozyS+layPK8h/61L +hyh8vjzV5AUWA5Zup+P7Jh/WRlh9Gxs0k0vimYMFKImw3mZr4EA8UCj2e85XIHNH +Bd0B1VIukq4OjU4QhRrutNebIy3GZ35ylcaXT5v18Rq/iRJAuJFoCzXUaE90/V9/ +2ob8A1CYEKGLocvOQgBsj7+2gP5WOP+WxI4TWPENRKMVchVBE8zV+7YZiahPCwOQ +r9TQWMaUIJxZ85yr7O8DhJOBX3B7EHIfpoADXcJJBBgRAgAJBQJLbBdqAhsMAAoJ +EPQuh/lmUBXJfs8An3O2/IQ/ThzLrM/2Ue3Spd2u5wN+AKCHU4hSTSkXM1gG3c9e +857IPkVBuQ== +=kF7L +-----END PGP PUBLIC KEY BLOCK----- + pub F6D4A1D411E9D1AE uid Christopher Povirk @@ -1736,6 +1873,31 @@ zzMvjuz0NEEhREM8f0ld3G/7Meh/OudSEgtQAmwJ0UMZWJWaZ0FhnLI= =nrrN -----END PGP PUBLIC KEY BLOCK----- +pub 1939A2520BAB1D90 +sub D068F0D7B6A63980 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsBNBFHNxM8BCADYmt+HKkEwu89KQbwV7XIbgwZSfWc7y1HvA2YJpJRXJQsU/Pzv +BhsHnm9ZIScBLIlgE5OUnMNz8ktPDdsFg3j/L0HREXOAqkOFxWx2kANsRo2HmkM3 +67RAu42fJqJcjD2Rs37wMxlSRRGQ+/bp+Bw2HNO1pw7GwrSgmZwzwT4+1pE/TvXQ +Wl+Nhdf3swLyBaSuWHJZT3+JOR0kEGSQuurR+57r6fKDmouWSwAKn1z97JelHuXj +HKZeueCkQvX7dayPP4a1zpoXPcoZhYekFarLWJl411EA3aHIIV8whknsZx/lGGC5 +yF9AVIzHHnhqFC/Fr+GJbwa9oMFXj0pY06ZNABEBAAHOwE0EUc3EzwEIAK6rZ7kR +p3uj0CrhvuTnLHU7nEs+KvoUZKLyhcIys76sJQ7cnhEygcG7tng/EtK8bI6skLwU +aF4fnPliDj/yIigY08p7TvFL/6HL4cLrIXR9uZe5IdvBKYhy23Ie2JXdLk6zH6jq +5+vBE0IA7ljJUQj0PgiIL92kB73Bn6dPayvtApzctajXvGajYNfOLTYc3n1L/Kqa +y+/UwjB5MJVlmFtZ1a/EAxyb5yHld/s3RKEaeEIpjaoPSJwXKOWNAcLdtgcPcsyf +rV4bkgjx7ABzPvf82gYucthyIx4zPZ29hZfktSV61h7cbJL5HGrk39UcSgfstHbf +BQiTY/1kVN9tuHkAEQEAAcLAdgQYAQIACQUCUc3EzwIbDAAhCRAZOaJSC6sdkBYh +BBOsIhOWSr4dHBR8Dhk5olILqx2QQWMIAMTNwm1NvKQd5I8bKQS1ScCkdgzyhmLE +dYW8N2OQaF48xO8FEmkHJL+F1LgydvYB8GhXr3p4IUC2b4PqK44DU5iBzUKcaGKX +FFWnOOBsPRLpsbS3KIrPV2TQcqQaHfrC6ZJPVtTap0D9Q3QKhEgD0Kxv6aD6Hxz7 +LdCLGNulNJLiyGFenN+PxCxmY+ffxLVqZpJMS/zOoXykCs9T3fXzhNB8kE+uMBKF +MSK0CZfcVCVIvm/mxmaztPlL+Q4eRwebjM2XBdEn1q+pvySratwMlfiwJ7s+Ogxf +fLaZOTZh7NjguthuER44Zww2Dtc5eWG0EEng66pC1ejrktxPt0rVTJs= +=T3BM +-----END PGP PUBLIC KEY BLOCK----- + pub 1F7A8F87B9D8F501 uid Download @@ -2262,6 +2424,89 @@ b+oWGNSc/Fv2Nrc2ZlrHnRSHOGobgHmRunB+gHPKax60BM+pUm2dH5Y= =zzVK -----END PGP PUBLIC KEY BLOCK----- +pub 56028DF552BA32E2 +uid Dokka Release + +sub 7EC19439E4D4C2A0 +sub D89D05374952262B +sub B5681E477AD61C38 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQGNBF+7lwIBDACcXIXAwFDoWvCCWn+OImyyJQvSnnte93Mc1ZJtlArkrjeGU7Mu +5giUH+FOyiXlj7CU4G9RTnAzDgM8XPncWOERgRG2dXtO03Li7iUEX4Z8PCUGsTxP +2VKGuCF4Je1ZPGxeKG49N0L3IIBBxfCzumE37LP3diw7Ups8xJUhZE1ecF/Ow4uE +y6lBOyaJM8VJN65GLOdlbjOTKaFKR6aY7lPcEGyClh+SnMcGTocLf9joBpDI8WZM +NQoQlVtiT6ItvbxjxJmA2hsodm1Ix/xX2xo3hdXH+opmsxlNGSK26hOSMKTMQXXR +m96Slky889SPpT+Rnbp+zPSsWqUJBzTT83DAfH7PJ210bRuzHJZzSox/2iiVbm09 +e8rny09ju/OTA5sVvg0ibNscO2wyGsFjoBTFB4x27Bl+4bloBot2lBO7mRkhOIaT +KxDkKPSw6vQnhA3a7p5nGMo20MgNhP09ui9CwO5Yh3kwnA81clldlMcjQMLy35ch +kSoqW9jnqm2CI5EAEQEAAbQjRG9ra2EgUmVsZWFzZSA8ZG9ra2FAamV0YnJhaW5z +LmNvbT65AY0EX7uX3QEMAJpgrB0PwR3KYUthxrU+zvZvh5gR3Wumqs75dDrTsgiz +6uDT05YEmflEO8/lvvZQmdQkv6RP1rRjlQsZEYjt6nlzwhlgn3TFIXagUMUfq39f +Dp0Eq1W4Y2hCfk/jOe8YMknYrFI9TGjAfwX0t/bZ2VYOE1FEk17XBNdCc/yvrgTt +F6XSmEOoCWv+2HGfPNo2M1cwX+wJ43TZO2jxHOx//1DGV/gkLoqa3Yqe3ZNwWKVn +wapnqfwlmrNJC42Y26MtsIa4ktsOURpKgAB1uQ7oKtesvaKh3XwW65tgv0kMeixg +vbRtrpQ3hDk+H9iGtIqx+C/1NvzfoIa8fKiby4D8+rXj5S+gjtwVZrY1t059wQSD +JNCWmD4PG/RoowYzdJmwfDxUMptcygJ6yYfn3psTbGF3HV/0jUzqrKDziLjCsKzh +d2Lxca71ItQYbLDjCsI9diiW7U0s+M2PBImvDU8UWwqnprimLCVhsMrRAgaTb4mU +gFF8MoK4QbRCpWOJz4joCQARAQABiQG8BBgBCgAmAhsgFiEExTaii8ifslAnLJ9/ +VgKN9VK6MuIFAmb5wc4FCQsAkPEACgkQVgKN9VK6MuJziwv/dAAYdjuWkkJd2IDa +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 +CitYYZ2nvxw= +=J39d +-----END PGP PUBLIC KEY BLOCK----- + pub 571A5291E827E1C7 uid Central Repository sync with maven.java.net (Used for signing artifacts that support syncing maven.java.net with the Central Repository) @@ -3270,6 +3515,126 @@ aHo/jdT35nAky2TXxokAn3R9/kTwWykkKH89mxse/54k3fao =xOW/ -----END PGP PUBLIC KEY BLOCK----- +pub 7C30F7B1329DBA87 +uid Ktor Release + +sub 72FF58594F983302 +sub 0588BC69A286FF16 +sub 3967D4EDA591B991 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBF+TCd4BDACbIA94MfIWL0SpvZwBddXgx36Lp9GYOWNgGoQCWSvk9vaMrLaI +rEll0xnoP98CfBQYrVSAmHDMhSLBCjNB3V1Sdz8GRdOG7HUffF7Cqwbm3Fxo3H/h ++Tsrodv23NuvKsDpgglUL6nJy5e/FO8y9dcxLXRRVdPFDhJubi08SiUJy9FQbnfA +yb2LuTzXtjDmjEsMZpdpQUlQkk0xNDkrrq+2miwxemVd35cnVQCFP0K7c4T0ksGg +Rf9A2r45DBbPfvwTL+ZbrGtCssUpCneWhPl79UsMxeY+vJjEggqqqRqbHRn6nOQd +3gKSaEqdALZURPzvkKxLUeUUtMk/tkFdsNe/ea7edk6G3MI4dbUY7p0XLS54S9cB +1JUAHNEFtuJQKGWNuwWO58Yun1EBtOdUEvnIIoQ+CIN/XeKrnEIXE3LSblB8BR3H +bqX54BMe9AzsmDQtc5pUOm2pfvCoiv8xFXQznBg24dGqo2A/jMoUnGj6oRj7k8mt +i9AdPLigldr0S0sAEQEAAbQhS3RvciBSZWxlYXNlIDxrdG9yQGpldGJyYWlucy5j +b20+zsDNBF+TCpABDADRarOqvERlpMCJjNXGZpK5sV7Umndyu1rwVOfEBhINkRX1 +vzzFJFciIfWEZ2c+vSNnXZC+vFuAYtwnHqTWwyodHU+/jwHeEWQ9WcD2buSwJvps +kSei7ZMSWx7zAGWM4ae0FmjJrVHEQhM1CgeDwrxIzJqoOhrC26IorT7bGB5M2Z2n +NStGz9fen71jNeyo0fHvvy9xkcEWsfDd9A5V0odRb5y1yKiHH4Puz+o6Gys43/PQ +Gzf2NBx1sjzQjmJrrufvMIzRWrJwySYJQZkr/qdJyqbKZgbA/BWTmpN9POranNd0 +YO2/lbD7eiDkBflNGnWcb305VVzyZSD1kXXeLAc+y4cQugj+FkA/9Tv2c75sIhXP +QAlZAG3ldj8WSiAlyyVuuWZh3eyxxH8J9LKDXJpBqvNvzucso1PQS2HzKhT98GxX +45LRTsZo6yM5XAFgqw42KqTmcOy97mzluVCos090d25zYwCYsFoSaIX06wYz1GuS +sW/JHXyUwsG6BWScgqMAEQEAAcLA/AQYAQoAJgIbIBYhBDlMtDbFaRb8Ae6kp3ww +97EynbqHBQJm+cFdBQkLKR3NAAoJEHww97EynbqHNPcL/0LeMIWEx9SGbxuBBtIl +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 +R4ysiUxDlndL +=qhEb +-----END PGP PUBLIC KEY BLOCK----- + pub 7FE9900F412D622E sub AE6B5325E74ED034 -----BEGIN PGP PUBLIC KEY BLOCK----- diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7cd6699..c6eb99f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -4,7 +4,10 @@ true true armored - + + + + @@ -26,6 +29,7 @@ + @@ -61,6 +65,7 @@ + @@ -78,8 +83,10 @@ + + @@ -87,6 +94,7 @@ + @@ -95,6 +103,7 @@ + @@ -141,6 +150,14 @@ + + + + + + + + @@ -149,16 +166,24 @@ + + + + + + + + + + + - - - @@ -201,15 +226,15 @@ + + + - - - @@ -240,6 +265,14 @@ + + + + + + + + @@ -248,6 +281,14 @@ + + + + + + + + @@ -256,16 +297,34 @@ + + + + + + + + + + + + + + + + + + @@ -274,6 +333,14 @@ + + + + + + + + @@ -282,6 +349,14 @@ + + + + + + + + @@ -290,6 +365,14 @@ + + + + + + + + @@ -298,6 +381,14 @@ + + + + + + + + @@ -306,6 +397,14 @@ + + + + + + + + @@ -314,6 +413,14 @@ + + + + + + + + @@ -322,6 +429,14 @@ + + + + + + + + @@ -330,6 +445,14 @@ + + + + + + + + @@ -338,6 +461,14 @@ + + + + + + + + @@ -346,6 +477,14 @@ + + + + + + + + @@ -354,6 +493,14 @@ + + + + + + + + @@ -373,6 +520,14 @@ + + + + + + + + @@ -381,6 +536,14 @@ + + + + + + + + @@ -389,6 +552,14 @@ + + + + + + + + @@ -397,6 +568,14 @@ + + + + + + + + @@ -405,6 +584,14 @@ + + + + + + + + @@ -413,6 +600,14 @@ + + + + + + + + @@ -421,6 +616,14 @@ + + + + + + + + @@ -437,6 +640,14 @@ + + + + + + + + @@ -445,6 +656,14 @@ + + + + + + + + @@ -453,6 +672,14 @@ + + + + + + + + @@ -461,6 +688,14 @@ + + + + + + + + @@ -469,27 +704,35 @@ + + + + + + + + + + + - - - + + + - - - @@ -499,6 +742,14 @@ + + + + + + + + @@ -507,6 +758,14 @@ + + + + + + + + @@ -515,6 +774,14 @@ + + + + + + + + @@ -523,6 +790,14 @@ + + + + + + + + @@ -531,6 +806,14 @@ + + + + + + + + @@ -539,6 +822,14 @@ + + + + + + + + @@ -547,6 +838,14 @@ + + + + + + + + @@ -555,6 +854,14 @@ + + + + + + + + @@ -563,6 +870,14 @@ + + + + + + + + @@ -571,6 +886,14 @@ + + + + + + + + @@ -579,6 +902,14 @@ + + + + + + + + @@ -587,6 +918,14 @@ + + + + + + + + @@ -595,6 +934,14 @@ + + + + + + + + @@ -603,6 +950,14 @@ + + + + + + + + @@ -611,6 +966,14 @@ + + + + + + + + @@ -619,6 +982,14 @@ + + + + + + + + @@ -627,6 +998,14 @@ + + + + + + + + @@ -635,6 +1014,14 @@ + + + + + + + + @@ -643,6 +1030,14 @@ + + + + + + + + @@ -651,6 +1046,14 @@ + + + + + + + + @@ -659,6 +1062,14 @@ + + + + + + + + @@ -667,6 +1078,14 @@ + + + + + + + + @@ -675,6 +1094,14 @@ + + + + + + + + @@ -683,6 +1110,14 @@ + + + + + + + + @@ -691,6 +1126,14 @@ + + + + + + + + @@ -699,6 +1142,14 @@ + + + + + + + + @@ -707,6 +1158,14 @@ + + + + + + + + @@ -715,6 +1174,14 @@ + + + + + + + + @@ -723,6 +1190,14 @@ + + + + + + + + @@ -731,6 +1206,14 @@ + + + + + + + + @@ -739,6 +1222,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -956,6 +1521,14 @@ + + + + + + + + @@ -992,6 +1565,11 @@ + + + + + @@ -1032,6 +1610,14 @@ + + + + + + + + @@ -1909,15 +2495,15 @@ + + + - - - @@ -1958,15 +2544,15 @@ + + + - - - @@ -1974,6 +2560,12 @@ + + + + + + @@ -2200,6 +2792,14 @@ + + + + + + + + @@ -2223,6 +2823,14 @@ + + + + + + + + @@ -2294,6 +2902,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2486,6 +3160,14 @@ + + + + + + + + @@ -2571,6 +3253,14 @@ + + + + + + + + @@ -2609,6 +3299,11 @@ + + + + + @@ -2625,6 +3320,11 @@ + + + + + @@ -2635,6 +3335,11 @@ + + + + + @@ -2783,6 +3488,11 @@ + + + + + @@ -2791,6 +3501,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2811,6 +3551,14 @@ + + + + + + + + @@ -2834,6 +3582,14 @@ + + + + + + + + @@ -2874,6 +3630,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2884,6 +3707,11 @@ + + + + + @@ -2892,6 +3720,14 @@ + + + + + + + + @@ -2905,6 +3741,11 @@ + + + + + @@ -2913,22 +3754,38 @@ + + + + + + + + + + + - - - + + + + + + + + @@ -2948,6 +3805,9 @@ + + + diff --git a/packages/kotlin-markdown-core/build.gradle.kts b/packages/kotlin-markdown-core/build.gradle.kts index 757db86..dac02ce 100644 --- a/packages/kotlin-markdown-core/build.gradle.kts +++ b/packages/kotlin-markdown-core/build.gradle.kts @@ -16,6 +16,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest +import java.io.DataInputStream import java.util.zip.ZipFile @CacheableTask @@ -170,12 +171,24 @@ plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.kotlin.multiplatform.library) alias(libs.plugins.ktlint) + alias(libs.plugins.dokka) `maven-publish` } group = "com.nouprax" version = rootProject.file("VERSION").readText().trim() +dokka { + dokkaSourceSets.configureEach { + // The entire public API lives in commonMain; the platform source + // sets hold internal actuals only (and the two Kotlin/Native sets + // share one source root, which Dokka rejects outright). + if (name != "commonMain") { + suppress.set(true) + } + } +} + dependencyLocking { lockAllConfigurations() } @@ -186,6 +199,7 @@ val generatedCanonicalAstSource = layout.buildDirectory.file( "generated/canonicalAstCommonTest/kotlin/com/nouprax/markdown/core/CanonicalAstCases.kt", ) + // The one closed model of supported build hosts. Every host-dependent // decision — JNI resource path, native library file name, Kotlin/Native // test target, managed-device ABI, publish-local target — derives from this @@ -588,6 +602,9 @@ afterEvaluate { val javadocJar = tasks.register("javadocJar") { archiveClassifier.set("javadoc") + // Real generated API reference first; the canonical AST spec and + // README ride along as supplementary content. + from(tasks.named("dokkaGeneratePublicationHtml")) from(repositoryRoot.file("docs/specs/canonical-ast.md")) from(layout.projectDirectory.file("README.md")) } @@ -644,10 +661,135 @@ tasks.withType, + index: Int, + ): String = pool[index] as? String ?: error("constant pool index $index is not utf8") + + fun classSurface(bytes: ByteArray): kotlin.collections.List { + val input = DataInputStream(bytes.inputStream()) + require(input.readInt() == -0x35014542) { "not a class file" } + input.readUnsignedShort() + input.readUnsignedShort() + val poolCount = input.readUnsignedShort() + val pool = arrayOfNulls(poolCount) + val classRefs = IntArray(poolCount) + var slot = 1 + while (slot < poolCount) { + when (val tag = input.readUnsignedByte()) { + 1 -> { + pool[slot] = input.readUTF() + } + + 7 -> { + classRefs[slot] = input.readUnsignedShort() + } + + 8, 16, 19, 20 -> { + input.skipBytes(2) + } + + 15 -> { + input.skipBytes(3) + } + + 3, 4, 9, 10, 11, 12, 17, 18 -> { + input.skipBytes(4) + } + + 5, 6 -> { + input.skipBytes(8) + slot += 1 + } + + else -> { + error("unsupported constant pool tag $tag") + } + } + slot += 1 + } + val access = input.readUnsignedShort() + val thisClass = input.readUnsignedShort() + val className = utf8At(pool, classRefs[thisClass]) + // Non-public and synthetic classes are invisible to Java. + if ((access and 0x0001) == 0 || (access and 0x1000) != 0) { + return emptyList() + } + input.readUnsignedShort() + repeat(input.readUnsignedShort()) { input.skipBytes(2) } + val surface = mutableListOf("class $className") + for (section in listOf("field", "method")) { + repeat(input.readUnsignedShort()) { + val memberAccess = input.readUnsignedShort() + val name = utf8At(pool, input.readUnsignedShort()) + val descriptor = utf8At(pool, input.readUnsignedShort()) + repeat(input.readUnsignedShort()) { + input.skipBytes(2) + input.skipBytes(input.readInt()) + } + val visible = (memberAccess and 0x0005) != 0 + val synthetic = (memberAccess and 0x1000) != 0 + if (visible && !synthetic) { + surface += " $section $className.$name $descriptor" + } + } + } + return surface + } + + val lines = mutableListOf() + ZipFile(jarFile.get().asFile).use { archive -> + for (entry in archive.entries()) { + if (!entry.name.endsWith(".class") || !entry.name.startsWith("com/nouprax/")) { + continue + } + lines += classSurface(archive.getInputStream(entry).use { it.readBytes() }) + } + } + val rendered = lines.sorted().joinToString("\n") + "\n" + if (write) { + snapshotFile.writeText(rendered) + logger.lifecycle("Wrote JVM ABI snapshot: ${snapshotFile.absolutePath}") + return@doLast + } + check(snapshotFile.isFile) { + "jvm-abi.txt is missing; generate it with ./gradlew :packages:kotlin-markdown-core:verifyJvmAbi -PwriteJvmAbi" + } + val expected = snapshotFile.readText() + check(rendered == expected) { + val actualLines = rendered.lines().toSet() + val expectedLines = expected.lines().toSet() + val added = (actualLines - expectedLines).sorted().joinToString("\n") + val removed = (expectedLines - actualLines).sorted().joinToString("\n") + "JVM public ABI drifted from jvm-abi.txt.\nAdded:\n$added\nRemoved:\n$removed\n" + + "If the change is intentional, regenerate with -PwriteJvmAbi." + } + } + } + tasks.register("kotlinTest") { group = "verification" description = "Runs JVM, Android host, and the current host's Kotlin/Native correctness suites." - dependsOn(listOfNotNull("jvmTest", "testAndroidHostTest", hostNativeTest, "verifyKotlinNativePackaging")) + dependsOn( + listOfNotNull("jvmTest", "testAndroidHostTest", hostNativeTest, "verifyKotlinNativePackaging", "verifyJvmAbi"), + ) } tasks.register("allKotlinTests") { @@ -687,13 +829,15 @@ tasks.register("verifyKotlinNativePackaging") { // file with the right name exists. fun binaryArchitecture(bytes: ByteArray): String { require(bytes.size >= 20) { "native library header is truncated" } + fun u16(offset: Int) = (bytes[offset].toInt() and 0xff) or ((bytes[offset + 1].toInt() and 0xff) shl 8) + fun u32(offset: Int) = (bytes[offset].toInt() and 0xff) or ((bytes[offset + 1].toInt() and 0xff) shl 8) or ((bytes[offset + 2].toInt() and 0xff) shl 16) or ((bytes[offset + 3].toInt() and 0xff) shl 24) return when { bytes[0] == 0x7f.toByte() && bytes[1] == 'E'.code.toByte() && - bytes[2] == 'L'.code.toByte() && bytes[3] == 'F'.code.toByte() -> + bytes[2] == 'L'.code.toByte() && bytes[3] == 'F'.code.toByte() -> { when (u16(18)) { 0x3e -> "elf-x64" 0xb7 -> "elf-arm64" @@ -701,15 +845,19 @@ tasks.register("verifyKotlinNativePackaging") { 0x03 -> "elf-x86" else -> "elf-unknown-${u16(18)}" } + } - u32(0) == 0xfeedfacf.toInt() -> + u32(0) == 0xfeedfacf.toInt() -> { when (u32(4)) { 0x0100000c -> "macho-arm64" 0x01000007 -> "macho-x64" else -> "macho-unknown-${u32(4)}" } + } - else -> "unknown" + else -> { + "unknown" + } } } diff --git a/packages/kotlin-markdown-core/consumers/jvm-maven/src/main/java/consumer/Main.java b/packages/kotlin-markdown-core/consumers/jvm-maven/src/main/java/consumer/Main.java index 833647b..f38f827 100644 --- a/packages/kotlin-markdown-core/consumers/jvm-maven/src/main/java/consumer/Main.java +++ b/packages/kotlin-markdown-core/consumers/jvm-maven/src/main/java/consumer/Main.java @@ -1,16 +1,23 @@ package consumer; +import com.nouprax.markdown.core.Commit; import com.nouprax.markdown.core.Document; -import com.nouprax.markdown.core.ParseOptions; +import com.nouprax.markdown.core.FootnoteDefinition; +import com.nouprax.markdown.core.FootnoteInfo; +import com.nouprax.markdown.core.FootnoteQueriesKt; +import com.nouprax.markdown.core.Markup; import com.nouprax.markdown.core.MarkupDumper; +import com.nouprax.markdown.core.MarkupID; +import com.nouprax.markdown.core.MarkupSession; +import com.nouprax.markdown.core.ParseOptions; +import java.util.List; public final class Main { private Main() {} public static void main(String[] args) { - ParseOptions options = new ParseOptions( - true, true, true, true, true, true, true, true, true, true, true); - Document document = Document.Companion.parse("héllo 🚀\n", options); + ParseOptions options = new ParseOptions(); + Document document = Document.parse("héllo 🚀\n", options); if (document.getContent().size() != 1) { throw new IllegalStateException("Document.parse returned unexpected top-level content"); } @@ -18,5 +25,55 @@ public static void main(String[] args) { if (!dump.contains("héllo 🚀")) { throw new IllegalStateException("native payload returned an unexpected document: " + dump); } + + // Identity and revision facade: plain Java reads the unsigned APIs + // through their bit-preserving signed views. + Markup paragraph = document.getContent().get(0); + MarkupID id = paragraph.getId(); + if (id.lineageBits() == 0L) { + throw new IllegalStateException("lineage bits must carry the session salt"); + } + MarkupID rebuilt = MarkupID.fromBits(id.lineageBits(), id.rawValueBits()); + if (!rebuilt.equals(id)) { + throw new IllegalStateException("MarkupID.fromBits must round-trip the identity"); + } + if (paragraph.revisionBits() <= 0L) { + throw new IllegalStateException("a parsed node must carry a positive revision"); + } + if (document.revisionBits() != paragraph.revisionBits()) { + // Both were minted by the same single commit. + throw new IllegalStateException("one-shot parse must commit every node at one revision"); + } + + // Sessions, deltas, and footnote queries from plain Java. + try (MarkupSession session = new MarkupSession()) { + session.append("See [^n].\n\n[^n]: note\n"); + Commit commit = session.commit(); + if (session.lineageBits() == 0L) { + throw new IllegalStateException("session lineage bits must be nonzero"); + } + if (commit.getDelta().beforeRevisionBits() != 0L + || commit.getDelta().afterRevisionBits() != session.revisionBits()) { + throw new IllegalStateException("delta revision bits must bracket the commit"); + } + if (commit.getDelta().getAdded().isEmpty()) { + throw new IllegalStateException("first commit must report added nodes"); + } + MarkupID first = commit.getDelta().getAdded().get(0); + if (session.node(first) == null) { + throw new IllegalStateException("session.node must resolve a delta id"); + } + List footnotes = FootnoteQueriesKt.footnotes(session); + if (footnotes.size() != 1 || !"n".equals(footnotes.get(0).getLabel())) { + throw new IllegalStateException("footnote query must list the winning definition"); + } + FootnoteInfo info = FootnoteQueriesKt.footnote(session, footnotes.get(0).getId()); + if (info == null || info.getNumber() == null || info.getNumber() != 1) { + throw new IllegalStateException("footnote info must number the definition"); + } + if (FootnoteQueriesKt.references(session, footnotes.get(0).getId()).size() != 1) { + throw new IllegalStateException("footnote back-references must list the reference"); + } + } } } diff --git a/packages/kotlin-markdown-core/jvm-abi.txt b/packages/kotlin-markdown-core/jvm-abi.txt new file mode 100644 index 0000000..6c890e5 --- /dev/null +++ b/packages/kotlin-markdown-core/jvm-abi.txt @@ -0,0 +1,587 @@ + field com/nouprax/markdown/core/Document.Companion Lcom/nouprax/markdown/core/Document$Companion; + field com/nouprax/markdown/core/JvmNative.INSTANCE Lcom/nouprax/markdown/core/JvmNative; + field com/nouprax/markdown/core/ListFlavor.BULLET Lcom/nouprax/markdown/core/ListFlavor; + field com/nouprax/markdown/core/ListFlavor.ORDERED Lcom/nouprax/markdown/core/ListFlavor; + field com/nouprax/markdown/core/MarkupDumper.INSTANCE Lcom/nouprax/markdown/core/MarkupDumper; + field com/nouprax/markdown/core/MarkupID.Companion Lcom/nouprax/markdown/core/MarkupID$Companion; + field com/nouprax/markdown/core/MarkupWalker.INSTANCE Lcom/nouprax/markdown/core/MarkupWalker; + field com/nouprax/markdown/core/ParseErrorCode.ALLOCATION_FAILED Lcom/nouprax/markdown/core/ParseErrorCode; + field com/nouprax/markdown/core/ParseErrorCode.INTERNAL Lcom/nouprax/markdown/core/ParseErrorCode; + field com/nouprax/markdown/core/ParseErrorCode.INVALID_ARGUMENT Lcom/nouprax/markdown/core/ParseErrorCode; + field com/nouprax/markdown/core/PlacementMode.EMBEDDED Lcom/nouprax/markdown/core/PlacementMode; + field com/nouprax/markdown/core/PlacementMode.STANDALONE Lcom/nouprax/markdown/core/PlacementMode; + field com/nouprax/markdown/core/ScopeResolver.Companion Lcom/nouprax/markdown/core/ScopeResolver$Companion; + field com/nouprax/markdown/core/TableAlignment.CENTER Lcom/nouprax/markdown/core/TableAlignment; + field com/nouprax/markdown/core/TableAlignment.LEFT Lcom/nouprax/markdown/core/TableAlignment; + field com/nouprax/markdown/core/TableAlignment.NONE Lcom/nouprax/markdown/core/TableAlignment; + field com/nouprax/markdown/core/TableAlignment.RIGHT Lcom/nouprax/markdown/core/TableAlignment; + field com/nouprax/markdown/core/WalkEvent.ENTERING Lcom/nouprax/markdown/core/WalkEvent; + field com/nouprax/markdown/core/WalkEvent.EXITING Lcom/nouprax/markdown/core/WalkEvent; + field com/nouprax/markdown/core/WireDecoder.INSTANCE Lcom/nouprax/markdown/core/WireDecoder; + field com/nouprax/markdown/core/WireKind.BLOCK_QUOTE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.CODE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.CODE_BLOCK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.Companion Lcom/nouprax/markdown/core/WireKind$Companion; + field com/nouprax/markdown/core/WireKind.DIRECTIVE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.DIRECTIVE_BLOCK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.DOCUMENT Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.EMPHASIS Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.FOOTNOTE_DEFINITION Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.FOOTNOTE_REFERENCE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.FORMULA Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.FORMULA_BLOCK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.HEADING Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.HTML Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.HTML_BLOCK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.IMAGE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.LINE_BREAK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.LINK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.LIST Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.LIST_ITEM Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.PARAGRAPH Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.SOFT_BREAK Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.STRIKETHROUGH Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.STRONG Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.TABLE Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.TABLE_CELL Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.TABLE_ROW Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.TEXT Lcom/nouprax/markdown/core/WireKind; + field com/nouprax/markdown/core/WireKind.THEMATIC_BREAK Lcom/nouprax/markdown/core/WireKind; + method com/nouprax/markdown/core/BlockQuote.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/BlockQuote.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/BlockQuote.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/BlockQuote.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/BlockQuote.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/BlockQuote.hashCode ()I + method com/nouprax/markdown/core/BlockQuote.revisionBits ()J + method com/nouprax/markdown/core/CBridge_jvmKt.cParse ([BLcom/nouprax/markdown/core/ParseOptions;)[B + method com/nouprax/markdown/core/CSession. (Lcom/nouprax/markdown/core/ParseOptions;)V + method com/nouprax/markdown/core/CSession.commit ()[B + method com/nouprax/markdown/core/CSession.edit (JJ[B)[B + method com/nouprax/markdown/core/CSession.footnoteInfo-VKZWuLQ (J)[B + method com/nouprax/markdown/core/CSession.footnoteReferences-VKZWuLQ (J)[B + method com/nouprax/markdown/core/CSession.footnotes ()[B + method com/nouprax/markdown/core/CSession.free ()V + method com/nouprax/markdown/core/CSession.length ()J + method com/nouprax/markdown/core/CSession.lineage-s-VKNKU ()J + method com/nouprax/markdown/core/CSession.revision-s-VKNKU ()J + method com/nouprax/markdown/core/CSession.rootId-s-VKNKU ()J + method com/nouprax/markdown/core/CSession.scopes ()[B + method com/nouprax/markdown/core/Code.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Code.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Code.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Code.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/Code.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/Code.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Code.hashCode ()I + method com/nouprax/markdown/core/Code.revisionBits ()J + method com/nouprax/markdown/core/CodeBlock.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/CodeBlock.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/CodeBlock.getClosed ()Z + method com/nouprax/markdown/core/CodeBlock.getFenced ()Z + method com/nouprax/markdown/core/CodeBlock.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/CodeBlock.getInfo ()Ljava/lang/String; + method com/nouprax/markdown/core/CodeBlock.getLanguage ()Ljava/lang/String; + method com/nouprax/markdown/core/CodeBlock.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/CodeBlock.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/CodeBlock.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/CodeBlock.hashCode ()I + method com/nouprax/markdown/core/CodeBlock.revisionBits ()J + method com/nouprax/markdown/core/Commit. (Lcom/nouprax/markdown/core/Document;Lcom/nouprax/markdown/core/Delta;)V + method com/nouprax/markdown/core/Commit.getDelta ()Lcom/nouprax/markdown/core/Delta; + method com/nouprax/markdown/core/Commit.getDocument ()Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/Delta.afterRevisionBits ()J + method com/nouprax/markdown/core/Delta.beforeRevisionBits ()J + method com/nouprax/markdown/core/Delta.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Delta.getAdded ()Ljava/util/List; + method com/nouprax/markdown/core/Delta.getAfterRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Delta.getBeforeRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Delta.getBubbled ()Ljava/util/List; + method com/nouprax/markdown/core/Delta.getChanged ()Ljava/util/List; + method com/nouprax/markdown/core/Delta.getRemoved ()Ljava/util/List; + method com/nouprax/markdown/core/Delta.hashCode ()I + method com/nouprax/markdown/core/Directive.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Directive.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Directive.getAttributes ()Ljava/lang/String; + method com/nouprax/markdown/core/Directive.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Directive.getLabel ()Ljava/util/List; + method com/nouprax/markdown/core/Directive.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/Directive.getName ()Ljava/lang/String; + method com/nouprax/markdown/core/Directive.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Directive.hashCode ()I + method com/nouprax/markdown/core/Directive.revisionBits ()J + method com/nouprax/markdown/core/DirectiveBlock.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/DirectiveBlock.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/DirectiveBlock.getAttributes ()Ljava/lang/String; + method com/nouprax/markdown/core/DirectiveBlock.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/DirectiveBlock.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/DirectiveBlock.getLabel ()Ljava/util/List; + method com/nouprax/markdown/core/DirectiveBlock.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/DirectiveBlock.getName ()Ljava/lang/String; + method com/nouprax/markdown/core/DirectiveBlock.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/DirectiveBlock.hashCode ()I + method com/nouprax/markdown/core/DirectiveBlock.revisionBits ()J + method com/nouprax/markdown/core/Document$Companion.parse (Ljava/lang/String;)Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/Document$Companion.parse (Ljava/lang/String;Lcom/nouprax/markdown/core/ParseOptions;)Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/Document.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Document.dump ()Ljava/lang/String; + method com/nouprax/markdown/core/Document.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Document.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Document.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Document.getResolver$com_nouprax_kotlin_markdown_core ()Lcom/nouprax/markdown/core/ScopeResolver; + method com/nouprax/markdown/core/Document.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Document.hashCode ()I + method com/nouprax/markdown/core/Document.materialize ()V + method com/nouprax/markdown/core/Document.parse (Ljava/lang/String;)Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/Document.parse (Ljava/lang/String;Lcom/nouprax/markdown/core/ParseOptions;)Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/Document.revisionBits ()J + method com/nouprax/markdown/core/Document.scope (Lcom/nouprax/markdown/core/Markup;)Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/Emphasis.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Emphasis.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Emphasis.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Emphasis.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Emphasis.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Emphasis.hashCode ()I + method com/nouprax/markdown/core/Emphasis.revisionBits ()J + method com/nouprax/markdown/core/FootnoteDefinition.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/FootnoteDefinition.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/FootnoteDefinition.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/FootnoteDefinition.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/FootnoteDefinition.getLabel ()Ljava/lang/String; + method com/nouprax/markdown/core/FootnoteDefinition.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/FootnoteDefinition.hashCode ()I + method com/nouprax/markdown/core/FootnoteDefinition.revisionBits ()J + method com/nouprax/markdown/core/FootnoteInfo. (Lcom/nouprax/markdown/core/MarkupID;Ljava/lang/Integer;Ljava/lang/Integer;I)V + method com/nouprax/markdown/core/FootnoteInfo.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/FootnoteInfo.getDefinition ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/FootnoteInfo.getNumber ()Ljava/lang/Integer; + method com/nouprax/markdown/core/FootnoteInfo.getReferenceCount ()I + method com/nouprax/markdown/core/FootnoteInfo.getReferenceOrdinal ()Ljava/lang/Integer; + method com/nouprax/markdown/core/FootnoteInfo.hashCode ()I + method com/nouprax/markdown/core/FootnoteQueriesKt.footnote (Lcom/nouprax/markdown/core/MarkupSession;Lcom/nouprax/markdown/core/MarkupID;)Lcom/nouprax/markdown/core/FootnoteInfo; + method com/nouprax/markdown/core/FootnoteQueriesKt.footnotes (Lcom/nouprax/markdown/core/MarkupSession;)Ljava/util/List; + method com/nouprax/markdown/core/FootnoteQueriesKt.references (Lcom/nouprax/markdown/core/MarkupSession;Lcom/nouprax/markdown/core/MarkupID;)Ljava/util/List; + method com/nouprax/markdown/core/FootnoteReference.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/FootnoteReference.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/FootnoteReference.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/FootnoteReference.getLabel ()Ljava/lang/String; + method com/nouprax/markdown/core/FootnoteReference.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/FootnoteReference.hashCode ()I + method com/nouprax/markdown/core/FootnoteReference.revisionBits ()J + method com/nouprax/markdown/core/Formula.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Formula.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Formula.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Formula.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/Formula.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/Formula.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Formula.hashCode ()I + method com/nouprax/markdown/core/Formula.revisionBits ()J + method com/nouprax/markdown/core/FormulaBlock.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/FormulaBlock.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/FormulaBlock.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/FormulaBlock.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/FormulaBlock.getMode ()Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/FormulaBlock.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/FormulaBlock.hashCode ()I + method com/nouprax/markdown/core/FormulaBlock.revisionBits ()J + method com/nouprax/markdown/core/HTML.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/HTML.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/HTML.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/HTML.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/HTML.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/HTML.hashCode ()I + method com/nouprax/markdown/core/HTML.revisionBits ()J + method com/nouprax/markdown/core/HTMLBlock.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/HTMLBlock.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/HTMLBlock.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/HTMLBlock.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/HTMLBlock.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/HTMLBlock.hashCode ()I + method com/nouprax/markdown/core/HTMLBlock.revisionBits ()J + method com/nouprax/markdown/core/Heading.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Heading.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Heading.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Heading.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Heading.getLevel ()I + method com/nouprax/markdown/core/Heading.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Heading.hashCode ()I + method com/nouprax/markdown/core/Heading.revisionBits ()J + method com/nouprax/markdown/core/Image.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Image.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Image.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Image.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Image.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Image.getSource ()Ljava/lang/String; + method com/nouprax/markdown/core/Image.getTitle ()Ljava/lang/String; + method com/nouprax/markdown/core/Image.hashCode ()I + method com/nouprax/markdown/core/Image.revisionBits ()J + method com/nouprax/markdown/core/ImmutableListKt.immutableList (ILkotlin/jvm/functions/Function1;)Ljava/util/List; + method com/nouprax/markdown/core/ImmutableListKt.immutableMap (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/util/List; + method com/nouprax/markdown/core/JvmNative.parse ([BI)[B + method com/nouprax/markdown/core/JvmNative.sessionCommit (J)[B + method com/nouprax/markdown/core/JvmNative.sessionEdit (JJJ[B)[B + method com/nouprax/markdown/core/JvmNative.sessionFootnoteInfo (JJ)[B + method com/nouprax/markdown/core/JvmNative.sessionFootnoteReferences (JJ)[B + method com/nouprax/markdown/core/JvmNative.sessionFootnotes (J)[B + method com/nouprax/markdown/core/JvmNative.sessionFree (J)V + method com/nouprax/markdown/core/JvmNative.sessionLength (J)J + method com/nouprax/markdown/core/JvmNative.sessionLineage (J)J + method com/nouprax/markdown/core/JvmNative.sessionOpen (I)J + method com/nouprax/markdown/core/JvmNative.sessionRevision (J)J + method com/nouprax/markdown/core/JvmNative.sessionRoot (J)J + method com/nouprax/markdown/core/JvmNative.sessionScopes (J)[B + method com/nouprax/markdown/core/LineBreak.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/LineBreak.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/LineBreak.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/LineBreak.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/LineBreak.hashCode ()I + method com/nouprax/markdown/core/LineBreak.revisionBits ()J + method com/nouprax/markdown/core/Link.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Link.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Link.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Link.getDestination ()Ljava/lang/String; + method com/nouprax/markdown/core/Link.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Link.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Link.getTitle ()Ljava/lang/String; + method com/nouprax/markdown/core/Link.hashCode ()I + method com/nouprax/markdown/core/Link.revisionBits ()J + method com/nouprax/markdown/core/List.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/List.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/List.getFlavor ()Lcom/nouprax/markdown/core/ListFlavor; + method com/nouprax/markdown/core/List.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/List.getItems ()Ljava/util/List; + method com/nouprax/markdown/core/List.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/List.getStart ()Ljava/lang/Long; + method com/nouprax/markdown/core/List.getTight ()Z + method com/nouprax/markdown/core/List.hashCode ()I + method com/nouprax/markdown/core/List.revisionBits ()J + method com/nouprax/markdown/core/ListFlavor.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/ListFlavor.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/ListFlavor; + method com/nouprax/markdown/core/ListFlavor.values ()[Lcom/nouprax/markdown/core/ListFlavor; + method com/nouprax/markdown/core/ListItem.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/ListItem.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/ListItem.getChecked ()Ljava/lang/Boolean; + method com/nouprax/markdown/core/ListItem.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/ListItem.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/ListItem.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/ListItem.hashCode ()I + method com/nouprax/markdown/core/ListItem.revisionBits ()J + method com/nouprax/markdown/core/Markup$DefaultImpls.revisionBits (Lcom/nouprax/markdown/core/Markup;)J + method com/nouprax/markdown/core/Markup.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Markup.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Markup.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Markup.revisionBits ()J + method com/nouprax/markdown/core/MarkupDumper.dump (Lcom/nouprax/markdown/core/Document;)Ljava/lang/String; + method com/nouprax/markdown/core/MarkupID$Companion.fromBits (JJ)Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/MarkupID.component1-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupID.component2-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupID.copy-PWzV0Is (JJ)Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/MarkupID.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/MarkupID.fromBits (JJ)Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/MarkupID.getLineage-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupID.getRawValue-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupID.hashCode ()I + method com/nouprax/markdown/core/MarkupID.lineageBits ()J + method com/nouprax/markdown/core/MarkupID.rawValueBits ()J + method com/nouprax/markdown/core/MarkupID.toString ()Ljava/lang/String; + method com/nouprax/markdown/core/MarkupKt.markupEquals (Lcom/nouprax/markdown/core/Markup;Ljava/lang/Object;)Z + method com/nouprax/markdown/core/MarkupKt.markupHashCode (Lcom/nouprax/markdown/core/Markup;)I + method com/nouprax/markdown/core/MarkupSession. ()V + method com/nouprax/markdown/core/MarkupSession. (Lcom/nouprax/markdown/core/ParseOptions;)V + method com/nouprax/markdown/core/MarkupSession.append (Ljava/lang/String;)V + method com/nouprax/markdown/core/MarkupSession.close ()V + method com/nouprax/markdown/core/MarkupSession.commit ()Lcom/nouprax/markdown/core/Commit; + method com/nouprax/markdown/core/MarkupSession.getDocument ()Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/MarkupSession.getLength ()I + method com/nouprax/markdown/core/MarkupSession.getLineage-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupSession.getMirror$com_nouprax_kotlin_markdown_core ()Ljava/util/HashMap; + method com/nouprax/markdown/core/MarkupSession.getNative$com_nouprax_kotlin_markdown_core ()Lcom/nouprax/markdown/core/CSession; + method com/nouprax/markdown/core/MarkupSession.getOptions ()Lcom/nouprax/markdown/core/ParseOptions; + method com/nouprax/markdown/core/MarkupSession.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/MarkupSession.lineageBits ()J + method com/nouprax/markdown/core/MarkupSession.node (Lcom/nouprax/markdown/core/MarkupID;)Lcom/nouprax/markdown/core/Markup; + method com/nouprax/markdown/core/MarkupSession.replace (IILjava/lang/String;)V + method com/nouprax/markdown/core/MarkupSession.requireOpen$com_nouprax_kotlin_markdown_core ()V + method com/nouprax/markdown/core/MarkupSession.revisionBits ()J + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/BlockQuote;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Code;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/CodeBlock;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Directive;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/DirectiveBlock;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Document;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Emphasis;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/FootnoteDefinition;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/FootnoteReference;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Formula;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/FormulaBlock;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/HTML;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/HTMLBlock;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Heading;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Image;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/LineBreak;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Link;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/List;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/ListItem;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Paragraph;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/SoftBreak;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Strikethrough;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Strong;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Table;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/TableCell;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/TableRow;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/Text;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupVisitor.visit (Lcom/nouprax/markdown/core/ThematicBreak;)Ljava/lang/Object; + method com/nouprax/markdown/core/MarkupWalker$WalkFrame$Enter. (Lcom/nouprax/markdown/core/Markup;)V + method com/nouprax/markdown/core/MarkupWalker$WalkFrame$Enter.getNode ()Lcom/nouprax/markdown/core/Markup; + method com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit. (Lcom/nouprax/markdown/core/Markup;Lcom/nouprax/markdown/core/Scope;)V + method com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit.getNode ()Lcom/nouprax/markdown/core/Markup; + method com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit.getScope ()Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/MarkupWalker.walk (Lcom/nouprax/markdown/core/Document;Lcom/nouprax/markdown/core/Markup;Lkotlin/jvm/functions/Function3;)V + method com/nouprax/markdown/core/MarkupWalker.walk (Lcom/nouprax/markdown/core/Document;Lcom/nouprax/markdown/core/MarkupVisitor;)V + method com/nouprax/markdown/core/MarkupWalker.walk (Lcom/nouprax/markdown/core/Document;Lkotlin/jvm/functions/Function3;)V + method com/nouprax/markdown/core/Paragraph.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Paragraph.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Paragraph.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Paragraph.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Paragraph.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Paragraph.hashCode ()I + method com/nouprax/markdown/core/Paragraph.revisionBits ()J + method com/nouprax/markdown/core/ParseErrorCode.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/ParseErrorCode.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/ParseErrorCode; + method com/nouprax/markdown/core/ParseErrorCode.values ()[Lcom/nouprax/markdown/core/ParseErrorCode; + method com/nouprax/markdown/core/ParseException. (Lcom/nouprax/markdown/core/ParseErrorCode;Ljava/lang/String;Lcom/nouprax/markdown/core/Scope;)V + method com/nouprax/markdown/core/ParseException.getCode ()Lcom/nouprax/markdown/core/ParseErrorCode; + method com/nouprax/markdown/core/ParseException.getMessage ()Ljava/lang/String; + method com/nouprax/markdown/core/ParseException.getScope ()Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/ParseOptions. ()V + method com/nouprax/markdown/core/ParseOptions. (Z)V + method com/nouprax/markdown/core/ParseOptions. (ZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions. (ZZZZZZZZZZZ)V + method com/nouprax/markdown/core/ParseOptions.component1 ()Z + method com/nouprax/markdown/core/ParseOptions.component10 ()Z + method com/nouprax/markdown/core/ParseOptions.component11 ()Z + method com/nouprax/markdown/core/ParseOptions.component2 ()Z + method com/nouprax/markdown/core/ParseOptions.component3 ()Z + method com/nouprax/markdown/core/ParseOptions.component4 ()Z + method com/nouprax/markdown/core/ParseOptions.component5 ()Z + method com/nouprax/markdown/core/ParseOptions.component6 ()Z + method com/nouprax/markdown/core/ParseOptions.component7 ()Z + method com/nouprax/markdown/core/ParseOptions.component8 ()Z + method com/nouprax/markdown/core/ParseOptions.component9 ()Z + method com/nouprax/markdown/core/ParseOptions.copy (ZZZZZZZZZZZ)Lcom/nouprax/markdown/core/ParseOptions; + method com/nouprax/markdown/core/ParseOptions.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/ParseOptions.getAutolinks ()Z + method com/nouprax/markdown/core/ParseOptions.getDirectives ()Z + method com/nouprax/markdown/core/ParseOptions.getDollarFormulaDelimiters ()Z + method com/nouprax/markdown/core/ParseOptions.getFootnotes ()Z + method com/nouprax/markdown/core/ParseOptions.getFormulas ()Z + method com/nouprax/markdown/core/ParseOptions.getLatexFormulaDelimiters ()Z + method com/nouprax/markdown/core/ParseOptions.getSmartPunctuation ()Z + method com/nouprax/markdown/core/ParseOptions.getStrikethrough ()Z + method com/nouprax/markdown/core/ParseOptions.getStripHTMLComments ()Z + method com/nouprax/markdown/core/ParseOptions.getTables ()Z + method com/nouprax/markdown/core/ParseOptions.getTaskLists ()Z + method com/nouprax/markdown/core/ParseOptions.hashCode ()I + method com/nouprax/markdown/core/ParseOptions.toString ()Ljava/lang/String; + method com/nouprax/markdown/core/ParseOptionsKt.toNativeMask (Lcom/nouprax/markdown/core/ParseOptions;)I + method com/nouprax/markdown/core/PlacementMode.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/PlacementMode.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/PlacementMode.values ()[Lcom/nouprax/markdown/core/PlacementMode; + method com/nouprax/markdown/core/Position. (II)V + method com/nouprax/markdown/core/Position.component1 ()I + method com/nouprax/markdown/core/Position.component2 ()I + method com/nouprax/markdown/core/Position.copy (II)Lcom/nouprax/markdown/core/Position; + method com/nouprax/markdown/core/Position.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Position.getColumn ()I + method com/nouprax/markdown/core/Position.getLine ()I + method com/nouprax/markdown/core/Position.hashCode ()I + method com/nouprax/markdown/core/Position.toString ()Ljava/lang/String; + method com/nouprax/markdown/core/ReadOnlyList$Companion.generated (ILkotlin/jvm/functions/Function1;)Lcom/nouprax/markdown/core/ReadOnlyList; + method com/nouprax/markdown/core/ReadOnlyList$Companion.mapped (Ljava/util/List;Lkotlin/jvm/functions/Function1;)Lcom/nouprax/markdown/core/ReadOnlyList; + method com/nouprax/markdown/core/Scope. (Lcom/nouprax/markdown/core/Position;Lcom/nouprax/markdown/core/Position;)V + method com/nouprax/markdown/core/Scope.component1 ()Lcom/nouprax/markdown/core/Position; + method com/nouprax/markdown/core/Scope.component2 ()Lcom/nouprax/markdown/core/Position; + method com/nouprax/markdown/core/Scope.copy (Lcom/nouprax/markdown/core/Position;Lcom/nouprax/markdown/core/Position;)Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/Scope.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Scope.getEnd ()Lcom/nouprax/markdown/core/Position; + method com/nouprax/markdown/core/Scope.getStart ()Lcom/nouprax/markdown/core/Position; + method com/nouprax/markdown/core/Scope.hashCode ()I + method com/nouprax/markdown/core/Scope.toString ()Ljava/lang/String; + method com/nouprax/markdown/core/ScopeEntry.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/ScopeEntry.getScope ()Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/ScopeResolver$Companion.getMaterializeProbe ()Lkotlin/jvm/functions/Function0; + method com/nouprax/markdown/core/ScopeResolver$Companion.getUnresolvable ()Lcom/nouprax/markdown/core/ScopeResolver; + method com/nouprax/markdown/core/ScopeResolver$Companion.live (Lcom/nouprax/markdown/core/CSession;)Lcom/nouprax/markdown/core/ScopeResolver; + method com/nouprax/markdown/core/ScopeResolver$Companion.materialized (Ljava/util/Map;)Lcom/nouprax/markdown/core/ScopeResolver; + method com/nouprax/markdown/core/ScopeResolver$Companion.setMaterializeProbe (Lkotlin/jvm/functions/Function0;)V + method com/nouprax/markdown/core/ScopeResolver.detach ()V + method com/nouprax/markdown/core/ScopeResolver.entry-VKZWuLQ (J)Lcom/nouprax/markdown/core/ScopeEntry; + method com/nouprax/markdown/core/ScopeResolver.materialize ()V + method com/nouprax/markdown/core/ScopeResolver.reattach (Lcom/nouprax/markdown/core/CSession;)V + method com/nouprax/markdown/core/SoftBreak.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/SoftBreak.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/SoftBreak.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/SoftBreak.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/SoftBreak.hashCode ()I + method com/nouprax/markdown/core/SoftBreak.revisionBits ()J + method com/nouprax/markdown/core/Spin_jvmKt.materializeWaitHint ()V + method com/nouprax/markdown/core/Strikethrough.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Strikethrough.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Strikethrough.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Strikethrough.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Strikethrough.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Strikethrough.hashCode ()I + method com/nouprax/markdown/core/Strikethrough.revisionBits ()J + method com/nouprax/markdown/core/Strong.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Strong.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Strong.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/Strong.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Strong.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Strong.hashCode ()I + method com/nouprax/markdown/core/Strong.revisionBits ()J + method com/nouprax/markdown/core/Table.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Table.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Table.getAlignments ()Ljava/util/List; + method com/nouprax/markdown/core/Table.getHeader ()Lcom/nouprax/markdown/core/TableRow; + method com/nouprax/markdown/core/Table.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Table.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Table.getRows ()Ljava/util/List; + method com/nouprax/markdown/core/Table.hashCode ()I + method com/nouprax/markdown/core/Table.revisionBits ()J + method com/nouprax/markdown/core/TableAlignment.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/TableAlignment.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/TableAlignment; + method com/nouprax/markdown/core/TableAlignment.values ()[Lcom/nouprax/markdown/core/TableAlignment; + method com/nouprax/markdown/core/TableCell.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/TableCell.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/TableCell.getContent ()Ljava/util/List; + method com/nouprax/markdown/core/TableCell.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/TableCell.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/TableCell.hashCode ()I + method com/nouprax/markdown/core/TableCell.revisionBits ()J + method com/nouprax/markdown/core/TableRow.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/TableRow.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/TableRow.getCells ()Ljava/util/List; + method com/nouprax/markdown/core/TableRow.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/TableRow.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/TableRow.hashCode ()I + method com/nouprax/markdown/core/TableRow.isHeader ()Z + method com/nouprax/markdown/core/TableRow.revisionBits ()J + method com/nouprax/markdown/core/Text.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/Text.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/Text.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/Text.getLiteral ()Ljava/lang/String; + method com/nouprax/markdown/core/Text.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/Text.hashCode ()I + method com/nouprax/markdown/core/Text.revisionBits ()J + method com/nouprax/markdown/core/ThematicBreak.accept (Lcom/nouprax/markdown/core/MarkupVisitor;)Ljava/lang/Object; + method com/nouprax/markdown/core/ThematicBreak.equals (Ljava/lang/Object;)Z + method com/nouprax/markdown/core/ThematicBreak.getId ()Lcom/nouprax/markdown/core/MarkupID; + method com/nouprax/markdown/core/ThematicBreak.getRevision-s-VKNKU ()J + method com/nouprax/markdown/core/ThematicBreak.hashCode ()I + method com/nouprax/markdown/core/ThematicBreak.revisionBits ()J + method com/nouprax/markdown/core/WalkEvent.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/WalkEvent.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/WalkEvent; + method com/nouprax/markdown/core/WalkEvent.values ()[Lcom/nouprax/markdown/core/WalkEvent; + method com/nouprax/markdown/core/WireDecoder.decodeAck ([B)V + method com/nouprax/markdown/core/WireDecoder.decodeCommit-z13BHRw ([BJLjava/util/Map;)Lcom/nouprax/markdown/core/Delta; + method com/nouprax/markdown/core/WireDecoder.decodeDocument ([B)Lcom/nouprax/markdown/core/Document; + method com/nouprax/markdown/core/WireDecoder.decodeFootnoteInfo-2TYgG_w ([BJ)Lcom/nouprax/markdown/core/FootnoteInfo; + method com/nouprax/markdown/core/WireDecoder.decodeIds ([B)Ljava/util/List; + method com/nouprax/markdown/core/WireDecoder.decodeScopes ([B)Ljava/util/Map; + method com/nouprax/markdown/core/WireDecoderKt.scopeTable (Lcom/nouprax/markdown/core/WireReader;)Ljava/util/Map; + method com/nouprax/markdown/core/WireKind$Companion.from (I)Lcom/nouprax/markdown/core/WireKind; + method com/nouprax/markdown/core/WireKind.getEntries ()Lkotlin/enums/EnumEntries; + method com/nouprax/markdown/core/WireKind.getRawValue ()I + method com/nouprax/markdown/core/WireKind.valueOf (Ljava/lang/String;)Lcom/nouprax/markdown/core/WireKind; + method com/nouprax/markdown/core/WireKind.values ()[Lcom/nouprax/markdown/core/WireKind; + method com/nouprax/markdown/core/WireMarkupDecoderKt.commitBody-z13BHRw (Lcom/nouprax/markdown/core/WireReader;JLjava/util/Map;)Lcom/nouprax/markdown/core/Delta; + method com/nouprax/markdown/core/WireReader. ([B)V + method com/nouprax/markdown/core/WireReader.boolean ()Z + method com/nouprax/markdown/core/WireReader.byte ()B + method com/nouprax/markdown/core/WireReader.getFinished ()Z + method com/nouprax/markdown/core/WireReader.int ()I + method com/nouprax/markdown/core/WireReader.kind ()Lcom/nouprax/markdown/core/WireKind; + method com/nouprax/markdown/core/WireReader.long ()J + method com/nouprax/markdown/core/WireReader.nullableBoolean ()Ljava/lang/Boolean; + method com/nouprax/markdown/core/WireReader.requiredString ()Ljava/lang/String; + method com/nouprax/markdown/core/WireReader.scope ()Lcom/nouprax/markdown/core/Scope; + method com/nouprax/markdown/core/WireReader.string ()Ljava/lang/String; + method com/nouprax/markdown/core/WireReader.ulong-s-VKNKU ()J +class com/nouprax/markdown/core/BlockQuote +class com/nouprax/markdown/core/CBridge_jvmKt +class com/nouprax/markdown/core/CSession +class com/nouprax/markdown/core/Code +class com/nouprax/markdown/core/CodeBlock +class com/nouprax/markdown/core/Commit +class com/nouprax/markdown/core/Delta +class com/nouprax/markdown/core/Directive +class com/nouprax/markdown/core/DirectiveBlock +class com/nouprax/markdown/core/Document +class com/nouprax/markdown/core/Document$Companion +class com/nouprax/markdown/core/Emphasis +class com/nouprax/markdown/core/FootnoteDefinition +class com/nouprax/markdown/core/FootnoteInfo +class com/nouprax/markdown/core/FootnoteQueriesKt +class com/nouprax/markdown/core/FootnoteReference +class com/nouprax/markdown/core/Formula +class com/nouprax/markdown/core/FormulaBlock +class com/nouprax/markdown/core/HTML +class com/nouprax/markdown/core/HTMLBlock +class com/nouprax/markdown/core/Heading +class com/nouprax/markdown/core/Image +class com/nouprax/markdown/core/ImmutableListKt +class com/nouprax/markdown/core/JvmNative +class com/nouprax/markdown/core/LineBreak +class com/nouprax/markdown/core/Link +class com/nouprax/markdown/core/List +class com/nouprax/markdown/core/ListFlavor +class com/nouprax/markdown/core/ListItem +class com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Markup$DefaultImpls +class com/nouprax/markdown/core/MarkupDumper +class com/nouprax/markdown/core/MarkupDumperKt +class com/nouprax/markdown/core/MarkupID +class com/nouprax/markdown/core/MarkupID$Companion +class com/nouprax/markdown/core/MarkupKt +class com/nouprax/markdown/core/MarkupSession +class com/nouprax/markdown/core/MarkupVisitor +class com/nouprax/markdown/core/MarkupWalker +class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Enter +class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit +class com/nouprax/markdown/core/Paragraph +class com/nouprax/markdown/core/ParseErrorCode +class com/nouprax/markdown/core/ParseException +class com/nouprax/markdown/core/ParseOptions +class com/nouprax/markdown/core/ParseOptionsKt +class com/nouprax/markdown/core/PlacementMode +class com/nouprax/markdown/core/Position +class com/nouprax/markdown/core/ReadOnlyList$Companion +class com/nouprax/markdown/core/Scope +class com/nouprax/markdown/core/ScopeEntry +class com/nouprax/markdown/core/ScopeResolver +class com/nouprax/markdown/core/ScopeResolver$Companion +class com/nouprax/markdown/core/SoftBreak +class com/nouprax/markdown/core/Spin_jvmKt +class com/nouprax/markdown/core/Strikethrough +class com/nouprax/markdown/core/Strong +class com/nouprax/markdown/core/Table +class com/nouprax/markdown/core/TableAlignment +class com/nouprax/markdown/core/TableCell +class com/nouprax/markdown/core/TableRow +class com/nouprax/markdown/core/Text +class com/nouprax/markdown/core/ThematicBreak +class com/nouprax/markdown/core/WalkEvent +class com/nouprax/markdown/core/WireDecoder +class com/nouprax/markdown/core/WireDecoderKt +class com/nouprax/markdown/core/WireKind +class com/nouprax/markdown/core/WireKind$Companion +class com/nouprax/markdown/core/WireMarkupDecoderKt +class com/nouprax/markdown/core/WireReader diff --git a/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt index 955533d..33c92cb 100644 --- a/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt +++ b/packages/kotlin-markdown-core/src/androidMain/kotlin/com/nouprax/markdown/core/CBridge.android.kt @@ -89,12 +89,17 @@ private object AndroidNativeLoader { "classpath at ${resource ?: "com/nouprax/markdown/core/native//$filename"}.", ) } - val directory = java.nio.file.Files.createTempDirectory("markdown-core-") + val directory = + java.nio.file.Files + .createTempDirectory("markdown-core-") val library = directory.resolve(filename) // deleteOnExit removes entries in reverse registration order, so the // directory must be registered before its child. directory.toFile().deleteOnExit() - stream.use { java.nio.file.Files.copy(it, library) } + stream.use { + java.nio.file.Files + .copy(it, library) + } library.toFile().deleteOnExit() System.load(library.toAbsolutePath().toString()) } diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt index de437f9..4f976d2 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Document.kt @@ -1,5 +1,8 @@ package com.nouprax.markdown.core +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + public class Document internal constructor( override val id: MarkupID, override val revision: ULong, @@ -54,6 +57,10 @@ public class Document internal constructor( public fun dump(): String = MarkupDumper.dump(this) public companion object { + /** Parses [source] in one shot into a self-contained snapshot; + * statically callable from Java as `Document.parse(...)`. */ + @JvmStatic + @JvmOverloads public fun parse( source: String, options: ParseOptions = ParseOptions(), diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Markup.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Markup.kt index ff86a33..e7d5b7c 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Markup.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/Markup.kt @@ -23,6 +23,10 @@ public sealed interface Markup { */ public val revision: ULong + /** [revision] as a bit-preserving signed value: the Java view of the + * unsigned accessor, whose mangled name Java sources cannot write. */ + public fun revisionBits(): Long = revision.toLong() + public fun accept(visitor: MarkupVisitor): Result } diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/MarkupID.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/MarkupID.kt index 2cc3b1d..e69f35a 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/MarkupID.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/MarkupID.kt @@ -1,5 +1,7 @@ package com.nouprax.markdown.core +import kotlin.jvm.JvmStatic + /** * Session-scoped node identity: [rawValue] is unique within the owning * session and never reused; [lineage] is the session's random salt, so nodes @@ -10,4 +12,21 @@ package com.nouprax.markdown.core public data class MarkupID( public val lineage: ULong, public val rawValue: ULong, -) +) { + /** [lineage] as a bit-preserving signed value: the Java view of the + * unsigned accessor, whose mangled name Java sources cannot write. */ + public fun lineageBits(): Long = lineage.toLong() + + /** [rawValue] as a bit-preserving signed value for Java callers. */ + public fun rawValueBits(): Long = rawValue.toLong() + + public companion object { + /** Builds an identity from bit-preserving signed values — the Java + * counterpart of the unsigned constructor. */ + @JvmStatic + public fun fromBits( + lineage: Long, + rawValue: Long, + ): MarkupID = MarkupID(lineage.toULong(), rawValue.toULong()) + } +} diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/ParseOptions.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/ParseOptions.kt index 341c49f..e50b205 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/ParseOptions.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/model/ParseOptions.kt @@ -1,18 +1,22 @@ package com.nouprax.markdown.core -public data class ParseOptions( - public val smartPunctuation: Boolean = true, - public val footnotes: Boolean = true, - public val stripHTMLComments: Boolean = true, - public val tables: Boolean = true, - public val strikethrough: Boolean = true, - public val autolinks: Boolean = true, - public val taskLists: Boolean = true, - public val formulas: Boolean = true, - public val dollarFormulaDelimiters: Boolean = true, - public val latexFormulaDelimiters: Boolean = true, - public val directives: Boolean = true, -) +import kotlin.jvm.JvmOverloads + +public data class ParseOptions + @JvmOverloads + constructor( + public val smartPunctuation: Boolean = true, + public val footnotes: Boolean = true, + public val stripHTMLComments: Boolean = true, + public val tables: Boolean = true, + public val strikethrough: Boolean = true, + public val autolinks: Boolean = true, + public val taskLists: Boolean = true, + public val formulas: Boolean = true, + public val dollarFormulaDelimiters: Boolean = true, + public val latexFormulaDelimiters: Boolean = true, + public val directives: Boolean = true, + ) internal fun ParseOptions.toNativeMask(): Int = listOf( diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Commit.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Commit.kt index 43b50e5..d43558e 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Commit.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/Commit.kt @@ -25,6 +25,12 @@ public class Delta internal constructor( public val changed: kotlin.collections.List, public val bubbled: kotlin.collections.List, ) { + /** [beforeRevision] as a bit-preserving signed value for Java callers. */ + public fun beforeRevisionBits(): Long = beforeRevision.toLong() + + /** [afterRevision] as a bit-preserving signed value for Java callers. */ + public fun afterRevisionBits(): Long = afterRevision.toLong() + override fun equals(other: Any?): Boolean = other is Delta && other.beforeRevision == beforeRevision && diff --git a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/MarkupSession.kt b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/MarkupSession.kt index 48f71b6..c5a3f17 100644 --- a/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/MarkupSession.kt +++ b/packages/kotlin-markdown-core/src/commonMain/kotlin/com/nouprax/markdown/core/session/MarkupSession.kt @@ -1,5 +1,7 @@ package com.nouprax.markdown.core +import kotlin.jvm.JvmOverloads + /** * The single mutable owner of one Markdown text and its living AST. * @@ -16,152 +18,161 @@ package com.nouprax.markdown.core * text advances, the tree does not). [close] releases the native session; * snapshots, deltas, and materialized scopes remain usable afterwards. */ -public class MarkupSession( - public val options: ParseOptions = ParseOptions(), -) : AutoCloseable { - internal val native: CSession = CSession(options) - - /** - * Per-session random salt; nodes from different sessions never compare - * equal even when their raw ids collide numerically. - */ - public val lineage: ULong = native.lineage() - - internal val mirror: HashMap = HashMap() - private val rootId: ULong = native.rootId() - private var resolver: ScopeResolver - private var closed = false - private var failed = false - - /** - * The last committed snapshot; the empty document at revision 0 until - * the first commit. - */ - public var document: Document - private set - - init { - val resolver = ScopeResolver.live(native) - this.resolver = resolver - // The revision-0 root is always an empty document. - val root = Document(MarkupID(lineage, rootId), 0UL, emptyList(), resolver) - mirror[rootId] = root - document = root - } +public class MarkupSession + @JvmOverloads + constructor( + public val options: ParseOptions = ParseOptions(), + ) : AutoCloseable { + internal val native: CSession = CSession(options) + + /** + * Per-session random salt; nodes from different sessions never compare + * equal even when their raw ids collide numerically. + */ + public val lineage: ULong = native.lineage() + + internal val mirror: HashMap = HashMap() + private val rootId: ULong = native.rootId() + private var resolver: ScopeResolver + private var closed = false + private var failed = false + + /** + * The last committed snapshot; the empty document at revision 0 until + * the first commit. + */ + public var document: Document + private set + + init { + val resolver = ScopeResolver.live(native) + this.resolver = resolver + // The revision-0 root is always an empty document. + val root = Document(MarkupID(lineage, rootId), 0UL, emptyList(), resolver) + mirror[rootId] = root + document = root + } + + /** The revision of the last committed snapshot; 0 before the first + * commit. */ + public val revision: ULong + get() { + requireOpen() + return native.revision() + } - /** The revision of the last committed snapshot; 0 before the first - * commit. */ - public val revision: ULong - get() { + /** [lineage] as a bit-preserving signed value: the Java view of the + * unsigned accessor, whose mangled name Java sources cannot write. */ + public fun lineageBits(): Long = lineage.toLong() + + /** [revision] as a bit-preserving signed value for Java callers. */ + public fun revisionBits(): Long = revision.toLong() + + /** The byte length of the stored text, including uncommitted edits. */ + public val length: Int + get() { + requireOpen() + return native.length().toInt() + } + + /** + * Queues an append of [text]'s UTF-8 bytes at the end of the stored + * text. Nothing is parsed until [commit]. + */ + public fun append(text: String) { requireOpen() - return native.revision() + val end = native.length() + edit(end, end, text) } - /** The byte length of the stored text, including uncommitted edits. */ - public val length: Int - get() { + /** + * Queues a replacement of the byte range `[start, end)` of the stored + * text with [replacement]'s UTF-8 bytes. An empty range inserts; an + * empty [replacement] deletes. Offsets refer to the stored text as + * previously edited; nothing is parsed until [commit]. + */ + public fun replace( + start: Int, + end: Int, + replacement: String, + ) { requireOpen() - return native.length().toInt() + require(start in 0..end) { "invalid edit range [$start, $end)" } + edit(start.toLong(), end.toLong(), replacement) } - /** - * Queues an append of [text]'s UTF-8 bytes at the end of the stored - * text. Nothing is parsed until [commit]. - */ - public fun append(text: String) { - requireOpen() - val end = native.length() - edit(end, end, text) - } + private fun edit( + start: Long, + end: Long, + replacement: String, + ) { + WireDecoder.decodeAck(native.edit(start, end, replacement.encodeToByteArray())) + } - /** - * Queues a replacement of the byte range `[start, end)` of the stored - * text with [replacement]'s UTF-8 bytes. An empty range inserts; an - * empty [replacement] deletes. Offsets refer to the stored text as - * previously edited; nothing is parsed until [commit]. - */ - public fun replace( - start: Int, - end: Int, - replacement: String, - ) { - requireOpen() - require(start in 0..end) { "invalid edit range [$start, $end)" } - edit(start.toLong(), end.toLong(), replacement) - } + /** + * Reparses the pending text incrementally and returns the new snapshot + * with its delta. The snapshot shares every unchanged node value with + * the previous snapshot; the work is proportional to the delta, not the + * document. + */ + public fun commit(): Commit { + requireOpen() + // The previous snapshot's currency ends when the commit starts: + // detach its resolver before the native tree is replaced, so a + // not-yet-materialized snapshot can never cache the new revision's + // positions as its own — a racing reader either materialized from + // the still-unchanged tree or takes the documented + // superseded-snapshot failure. + val previous = resolver + previous.detach() + val delta = + try { + WireDecoder.decodeCommit(native.commit(), lineage, mirror) + } catch (failure: ParseException) { + // The native commit failed transactionally: the tree is + // unchanged at the previous revision, the previous snapshot + // becomes current again, and the commit may be retried. + previous.reattach(native) + throw failure + } catch (failure: Throwable) { + // The native tree may have advanced while the payload or the + // mirror did not; the session can no longer answer + // consistently and refuses further work. + failed = true + throw failure + } + val resolver = ScopeResolver.live(native) + this.resolver = resolver + val root = mirror[rootId] + check(root is Document) { "session committed without a document root" } + val adopted = Document(root.id, root.revision, root.content, resolver) + mirror[rootId] = adopted + document = adopted + return Commit(adopted, delta) + } - private fun edit( - start: Long, - end: Long, - replacement: String, - ) { - WireDecoder.decodeAck(native.edit(start, end, replacement.encodeToByteArray())) - } + /** + * The committed snapshot's current value for [id]; null when no node + * with that identity exists at the committed revision. + */ + public fun node(id: MarkupID): Markup? = if (id.lineage == lineage) mirror[id.rawValue] else null - /** - * Reparses the pending text incrementally and returns the new snapshot - * with its delta. The snapshot shares every unchanged node value with - * the previous snapshot; the work is proportional to the delta, not the - * document. - */ - public fun commit(): Commit { - requireOpen() - // The previous snapshot's currency ends when the commit starts: - // detach its resolver before the native tree is replaced, so a - // not-yet-materialized snapshot can never cache the new revision's - // positions as its own — a racing reader either materialized from - // the still-unchanged tree or takes the documented - // superseded-snapshot failure. - val previous = resolver - previous.detach() - val delta = - try { - WireDecoder.decodeCommit(native.commit(), lineage, mirror) - } catch (failure: ParseException) { - // The native commit failed transactionally: the tree is - // unchanged at the previous revision, the previous snapshot - // becomes current again, and the commit may be retried. - previous.reattach(native) - throw failure - } catch (failure: Throwable) { - // The native tree may have advanced while the payload or the - // mirror did not; the session can no longer answer - // consistently and refuses further work. - failed = true - throw failure + /** + * Releases the native session. Idempotent. Snapshots, deltas, and scopes + * materialized while their snapshot was current remain usable; every + * other member of this class fails after closing. + */ + override fun close() { + if (closed) { + return } - val resolver = ScopeResolver.live(native) - this.resolver = resolver - val root = mirror[rootId] - check(root is Document) { "session committed without a document root" } - val adopted = Document(root.id, root.revision, root.content, resolver) - mirror[rootId] = adopted - document = adopted - return Commit(adopted, delta) - } - - /** - * The committed snapshot's current value for [id]; null when no node - * with that identity exists at the committed revision. - */ - public fun node(id: MarkupID): Markup? = if (id.lineage == lineage) mirror[id.rawValue] else null - - /** - * Releases the native session. Idempotent. Snapshots, deltas, and scopes - * materialized while their snapshot was current remain usable; every - * other member of this class fails after closing. - */ - override fun close() { - if (closed) { - return + closed = true + resolver.detach() + native.free() } - closed = true - resolver.detach() - native.free() - } - internal fun requireOpen() { - check(!closed) { "the session is closed" } - check(!failed) { "the session failed irrecoverably during a commit" } + internal fun requireOpen() { + check(!closed) { "the session is closed" } + check(!failed) { "the session failed irrecoverably during a commit" } + } } -} diff --git a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt index 5d3a02a..34f7608 100644 --- a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt +++ b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt @@ -82,7 +82,12 @@ class SnapshotMaterializationTest { it.commit() commit } - assertEquals(3, first.document.scope(first.document.content[1]).start.line) + assertEquals( + 3, + first.document + .scope(first.document.content[1]) + .start.line, + ) assertTrue(first.document.dump().contains("Paragraph")) } } diff --git a/scripts/audit-maven-publications.mjs b/scripts/audit-maven-publications.mjs index f4531f5..58a8de8 100755 --- a/scripts/audit-maven-publications.mjs +++ b/scripts/audit-maven-publications.mjs @@ -35,14 +35,34 @@ for (const coordinate of requiredCoordinates) { files.some((file) => file.endsWith("-sources.jar")), `${coordinate} is missing sources` ); - assert.ok( - files.some((file) => file.endsWith("-javadoc.jar")), - `${coordinate} is missing javadoc` - ); + const javadoc = files.find((file) => file.endsWith("-javadoc.jar")); + assert.ok(javadoc, `${coordinate} is missing javadoc`); + // The android-runtime coordinate is a JNI payload with no public Kotlin + // API; only the API-carrying coordinates owe a generated reference. + if (!coordinate.includes("android-runtime")) auditJavadocContent(path.join(directory, javadoc)); auditPom(path.join(directory, `${coordinate}-${version}.pom`), coordinate); auditModule(path.join(directory, `${coordinate}-${version}.module`)); } +// A javadoc classifier must carry a usable generated API reference — an +// index and per-type pages for the public package — not merely exist. +function auditJavadocContent(file) { + const listing = execFileSync("unzip", ["-Z1", file], { encoding: "utf8" }); + const entries = listing.trim().split("\n").filter(Boolean); + for (const required of [ + "index.html", + /com\.nouprax\.markdown\.core\/index\.html$/u, + /com\.nouprax\.markdown\.core\/-document\/index\.html$/u, + /com\.nouprax\.markdown\.core\/-markup-session\/index\.html$/u, + /com\.nouprax\.markdown\.core\/-markup-walker\/index\.html$/u + ]) { + assert.ok( + entries.some((entry) => (typeof required === "string" ? entry === required : required.test(entry))), + `${path.basename(file)} lacks generated API documentation entry ${required}` + ); + } +} + if (full) { requireArtifact("kotlin-markdown-core-jvm", ".jar", ["-sources.jar", "-javadoc.jar"]); requireArtifact("kotlin-markdown-core-android", ".aar"); From 2dfcd8b5e742beb07985cdd03656d23cb098ad3c Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 00:41:31 -0500 Subject: [PATCH 09/12] [Infra] Surface pins for reviewed API additions; formatting Co-Authored-By: Claude Fable 5 --- .../es-markdown-core/src/session/markup-session.ts | 12 ++++++++++-- packages/es-markdown-core/src/wire/node-decoder.ts | 7 ++++++- scripts/audit-public-surface.sh | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/es-markdown-core/src/session/markup-session.ts b/packages/es-markdown-core/src/session/markup-session.ts index cb1d9a4..325f0b7 100644 --- a/packages/es-markdown-core/src/session/markup-session.ts +++ b/packages/es-markdown-core/src/session/markup-session.ts @@ -271,7 +271,10 @@ export class MarkupSession { }); value = entry.rawValue === this.rootRawValue - ? adopt({ kind: "document", id: this.identity(entry.rawValue), revision, content: children }, resolver) + ? adopt( + { kind: "document", id: this.identity(entry.rawValue), revision, content: children }, + resolver + ) : decoder.decodeValue(entry.pointer, this.identity(entry.rawValue), revision, children); } else { if (previous === undefined) { @@ -281,7 +284,12 @@ export class MarkupSession { value = entry.rawValue === this.rootRawValue && relinked.kind === "document" ? adopt( - { kind: "document", id: relinked.id, revision: relinked.revision, content: relinked.content }, + { + kind: "document", + id: relinked.id, + revision: relinked.revision, + content: relinked.content + }, resolver ) : relinked; diff --git a/packages/es-markdown-core/src/wire/node-decoder.ts b/packages/es-markdown-core/src/wire/node-decoder.ts index 956e851..42dfa33 100644 --- a/packages/es-markdown-core/src/wire/node-decoder.ts +++ b/packages/es-markdown-core/src/wire/node-decoder.ts @@ -159,7 +159,12 @@ export class NodeDecoder { private decodeChildren(parent: number): Markup[] { const context = this.context!; const stack: DecodeFrame[] = [ - NodeDecoder.frame(parent, context.ids(this.rawId(parent)), this.revisionOf(parent), this.childPointers(parent)) + NodeDecoder.frame( + parent, + context.ids(this.rawId(parent)), + this.revisionOf(parent), + this.childPointers(parent) + ) ]; while (true) { const top = stack[stack.length - 1]!; diff --git a/scripts/audit-public-surface.sh b/scripts/audit-public-surface.sh index 34ecf00..560be22 100755 --- a/scripts/audit-public-surface.sh +++ b/scripts/audit-public-surface.sh @@ -73,6 +73,7 @@ public func append(_ text: String) throws public func commit() throws -> Commit public func footnote(of id: MarkupID) -> FootnoteInfo? public func footnotes() -> [FootnoteDefinition] +public func materialize() public func node(for id: MarkupID) -> (any Markup)? public func references(of definition: MarkupID) -> [FootnoteReference] public func replace(_ range: Range, with text: String) throws @@ -140,10 +141,14 @@ public class MarkupSession public fun MarkupSession.footnote public fun MarkupSession.footnotes public fun MarkupSession.references +public fun afterRevisionBits public fun append +public fun beforeRevisionBits public fun commit +public fun lineageBits public fun node public fun replace +public fun revisionBits public val added public val afterRevision public val beforeRevision @@ -210,6 +215,8 @@ constructor(options: ParseOptions = {}) export class MarkupSession export class ScopeResolver export function adopt(value: DocumentValue, resolver: ScopeResolver): Document +export function relink(previous: Markup, revision: number, swaps: readonly ChildSwap[]): Markup +export interface ChildSwap export interface Commit export interface Delta export interface FootnoteInfo From 51ac29a6ca7c4013235abbf7a1ec9878b2bcc684 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 01:15:21 -0500 Subject: [PATCH 10/12] [Fix] Address Codex review: memory-safe deep session case + hierarchy in the ABI snapshot Co-Authored-By: Claude Fable 5 --- .prettierignore | 4 + .../kotlin-markdown-core/build.gradle.kts | 17 ++- packages/kotlin-markdown-core/jvm-abi.txt | 140 +++++++++--------- packages/markdown-core/tests/CMakeLists.txt | 8 +- .../tests/runners/concurrency_runner.c | 102 ++++++++++++- .../tests/runners/pathological_runner.c | 21 ++- 6 files changed, 205 insertions(+), 87 deletions(-) diff --git a/.prettierignore b/.prettierignore index ba51dfc..cffa73e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,6 +17,10 @@ Makefile packages/markdown-core/tests/fixtures/ packages/markdown-core/tests/canonical-ast/ +# Harness-owned state: local settings and agent worktrees live here, never +# repository sources. +.claude/ + # Generated, copied, and package-manager content packages/markdown-core/core/case_fold_switch.inc packages/markdown-core/core/entities.inc diff --git a/packages/kotlin-markdown-core/build.gradle.kts b/packages/kotlin-markdown-core/build.gradle.kts index dac02ce..cbabb34 100644 --- a/packages/kotlin-markdown-core/build.gradle.kts +++ b/packages/kotlin-markdown-core/build.gradle.kts @@ -732,9 +732,20 @@ val verifyJvmAbi = if ((access and 0x0001) == 0 || (access and 0x1000) != 0) { return emptyList() } - input.readUnsignedShort() - repeat(input.readUnsignedShort()) { input.skipBytes(2) } - val surface = mutableListOf("class $className") + // The type hierarchy is ABI too: dropping an implemented + // interface breaks Java callers without touching members. + val superName = + input.readUnsignedShort().let { index -> + if (index == 0) "-" else utf8At(pool, classRefs[index]) + } + val interfaces = + (0 until input.readUnsignedShort()) + .map { utf8At(pool, classRefs[input.readUnsignedShort()]) } + .sorted() + val surface = + mutableListOf( + "class $className extends $superName implements ${interfaces.joinToString(",")}", + ) for (section in listOf("field", "method")) { repeat(input.readUnsignedShort()) { val memberAccess = input.readUnsignedShort() diff --git a/packages/kotlin-markdown-core/jvm-abi.txt b/packages/kotlin-markdown-core/jvm-abi.txt index 6c890e5..3a02461 100644 --- a/packages/kotlin-markdown-core/jvm-abi.txt +++ b/packages/kotlin-markdown-core/jvm-abi.txt @@ -515,73 +515,73 @@ method com/nouprax/markdown/core/WireReader.scope ()Lcom/nouprax/markdown/core/Scope; method com/nouprax/markdown/core/WireReader.string ()Ljava/lang/String; method com/nouprax/markdown/core/WireReader.ulong-s-VKNKU ()J -class com/nouprax/markdown/core/BlockQuote -class com/nouprax/markdown/core/CBridge_jvmKt -class com/nouprax/markdown/core/CSession -class com/nouprax/markdown/core/Code -class com/nouprax/markdown/core/CodeBlock -class com/nouprax/markdown/core/Commit -class com/nouprax/markdown/core/Delta -class com/nouprax/markdown/core/Directive -class com/nouprax/markdown/core/DirectiveBlock -class com/nouprax/markdown/core/Document -class com/nouprax/markdown/core/Document$Companion -class com/nouprax/markdown/core/Emphasis -class com/nouprax/markdown/core/FootnoteDefinition -class com/nouprax/markdown/core/FootnoteInfo -class com/nouprax/markdown/core/FootnoteQueriesKt -class com/nouprax/markdown/core/FootnoteReference -class com/nouprax/markdown/core/Formula -class com/nouprax/markdown/core/FormulaBlock -class com/nouprax/markdown/core/HTML -class com/nouprax/markdown/core/HTMLBlock -class com/nouprax/markdown/core/Heading -class com/nouprax/markdown/core/Image -class com/nouprax/markdown/core/ImmutableListKt -class com/nouprax/markdown/core/JvmNative -class com/nouprax/markdown/core/LineBreak -class com/nouprax/markdown/core/Link -class com/nouprax/markdown/core/List -class com/nouprax/markdown/core/ListFlavor -class com/nouprax/markdown/core/ListItem -class com/nouprax/markdown/core/Markup -class com/nouprax/markdown/core/Markup$DefaultImpls -class com/nouprax/markdown/core/MarkupDumper -class com/nouprax/markdown/core/MarkupDumperKt -class com/nouprax/markdown/core/MarkupID -class com/nouprax/markdown/core/MarkupID$Companion -class com/nouprax/markdown/core/MarkupKt -class com/nouprax/markdown/core/MarkupSession -class com/nouprax/markdown/core/MarkupVisitor -class com/nouprax/markdown/core/MarkupWalker -class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Enter -class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit -class com/nouprax/markdown/core/Paragraph -class com/nouprax/markdown/core/ParseErrorCode -class com/nouprax/markdown/core/ParseException -class com/nouprax/markdown/core/ParseOptions -class com/nouprax/markdown/core/ParseOptionsKt -class com/nouprax/markdown/core/PlacementMode -class com/nouprax/markdown/core/Position -class com/nouprax/markdown/core/ReadOnlyList$Companion -class com/nouprax/markdown/core/Scope -class com/nouprax/markdown/core/ScopeEntry -class com/nouprax/markdown/core/ScopeResolver -class com/nouprax/markdown/core/ScopeResolver$Companion -class com/nouprax/markdown/core/SoftBreak -class com/nouprax/markdown/core/Spin_jvmKt -class com/nouprax/markdown/core/Strikethrough -class com/nouprax/markdown/core/Strong -class com/nouprax/markdown/core/Table -class com/nouprax/markdown/core/TableAlignment -class com/nouprax/markdown/core/TableCell -class com/nouprax/markdown/core/TableRow -class com/nouprax/markdown/core/Text -class com/nouprax/markdown/core/ThematicBreak -class com/nouprax/markdown/core/WalkEvent -class com/nouprax/markdown/core/WireDecoder -class com/nouprax/markdown/core/WireDecoderKt -class com/nouprax/markdown/core/WireKind -class com/nouprax/markdown/core/WireKind$Companion -class com/nouprax/markdown/core/WireMarkupDecoderKt -class com/nouprax/markdown/core/WireReader +class com/nouprax/markdown/core/BlockQuote extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/CBridge_jvmKt extends java/lang/Object implements +class com/nouprax/markdown/core/CSession extends java/lang/Object implements +class com/nouprax/markdown/core/Code extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/CodeBlock extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Commit extends java/lang/Object implements +class com/nouprax/markdown/core/Delta extends java/lang/Object implements +class com/nouprax/markdown/core/Directive extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/DirectiveBlock extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Document extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Document$Companion extends java/lang/Object implements +class com/nouprax/markdown/core/Emphasis extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/FootnoteDefinition extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/FootnoteInfo extends java/lang/Object implements +class com/nouprax/markdown/core/FootnoteQueriesKt extends java/lang/Object implements +class com/nouprax/markdown/core/FootnoteReference extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Formula extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/FormulaBlock extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/HTML extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/HTMLBlock extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Heading extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Image extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/ImmutableListKt extends java/lang/Object implements +class com/nouprax/markdown/core/JvmNative extends java/lang/Object implements +class com/nouprax/markdown/core/LineBreak extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Link extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/List extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/ListFlavor extends java/lang/Enum implements +class com/nouprax/markdown/core/ListItem extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Markup extends java/lang/Object implements +class com/nouprax/markdown/core/Markup$DefaultImpls extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupDumper extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupDumperKt extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupID extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupID$Companion extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupKt extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupSession extends java/lang/Object implements java/lang/AutoCloseable +class com/nouprax/markdown/core/MarkupVisitor extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupWalker extends java/lang/Object implements +class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Enter extends java/lang/Object implements com/nouprax/markdown/core/MarkupWalker$WalkFrame +class com/nouprax/markdown/core/MarkupWalker$WalkFrame$Exit extends java/lang/Object implements com/nouprax/markdown/core/MarkupWalker$WalkFrame +class com/nouprax/markdown/core/Paragraph extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/ParseErrorCode extends java/lang/Enum implements +class com/nouprax/markdown/core/ParseException extends java/lang/RuntimeException implements +class com/nouprax/markdown/core/ParseOptions extends java/lang/Object implements +class com/nouprax/markdown/core/ParseOptionsKt extends java/lang/Object implements +class com/nouprax/markdown/core/PlacementMode extends java/lang/Enum implements +class com/nouprax/markdown/core/Position extends java/lang/Object implements +class com/nouprax/markdown/core/ReadOnlyList$Companion extends java/lang/Object implements +class com/nouprax/markdown/core/Scope extends java/lang/Object implements +class com/nouprax/markdown/core/ScopeEntry extends java/lang/Object implements +class com/nouprax/markdown/core/ScopeResolver extends java/lang/Object implements +class com/nouprax/markdown/core/ScopeResolver$Companion extends java/lang/Object implements +class com/nouprax/markdown/core/SoftBreak extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Spin_jvmKt extends java/lang/Object implements +class com/nouprax/markdown/core/Strikethrough extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Strong extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Table extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/TableAlignment extends java/lang/Enum implements +class com/nouprax/markdown/core/TableCell extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/TableRow extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/Text extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/ThematicBreak extends java/lang/Object implements com/nouprax/markdown/core/Markup +class com/nouprax/markdown/core/WalkEvent extends java/lang/Enum implements +class com/nouprax/markdown/core/WireDecoder extends java/lang/Object implements +class com/nouprax/markdown/core/WireDecoderKt extends java/lang/Object implements +class com/nouprax/markdown/core/WireKind extends java/lang/Enum implements +class com/nouprax/markdown/core/WireKind$Companion extends java/lang/Object implements +class com/nouprax/markdown/core/WireMarkupDecoderKt extends java/lang/Object implements +class com/nouprax/markdown/core/WireReader extends java/lang/Object implements diff --git a/packages/markdown-core/tests/CMakeLists.txt b/packages/markdown-core/tests/CMakeLists.txt index 6cbf2de..c1c0143 100755 --- a/packages/markdown-core/tests/CMakeLists.txt +++ b/packages/markdown-core/tests/CMakeLists.txt @@ -199,6 +199,10 @@ markdown_core_add_test(facade_concurrent_sessions facade 600 concurrency_runner # and facade failure paths. markdown_core_add_test(regression_registry_lifecycle regression 240 concurrency_runner --case lifecycle) +# The iterative parse/traversal/dump contract on a deliberately small +# thread stack: adversarial nesting must not consume stack anywhere. +markdown_core_add_test(regression_dump_small_stack regression 120 concurrency_runner --case dump_small_stack) + # Large/deep/repeated-input correctness checks are distinct from timed # benchmark workloads over related input shapes. foreach(stress_case large_document deep_nesting repeated_release) @@ -411,10 +415,6 @@ set(MARKDOWN_CORE_PATHOLOGICAL_CASES foreach(case IN LISTS MARKDOWN_CORE_PATHOLOGICAL_CASES) markdown_core_add_test(pathological_${case} pathological 30 pathological_runner --case ${case}) endforeach() -# The mid-chain marker flips re-kind the full 50000-deep quote chain twice, -# each commit re-verified by a full dump-equality pass, so this case earns -# the complexity-tier budget instead of the per-case default. -set_tests_properties(pathological_session_quotes_deep PROPERTIES TIMEOUT 120) set(MARKDOWN_CORE_COMPLEXITY_CASES valid_long_quoted_value diff --git a/packages/markdown-core/tests/runners/concurrency_runner.c b/packages/markdown-core/tests/runners/concurrency_runner.c index 3e708bd..7b74bc0 100644 --- a/packages/markdown-core/tests/runners/concurrency_runner.c +++ b/packages/markdown-core/tests/runners/concurrency_runner.c @@ -87,6 +87,15 @@ static int thread_spawn(thread_handle *handle, thread_entry entry, void *argumen return 0; } +static int thread_spawn_with_stack(thread_handle *handle, thread_entry entry, void *argument, unsigned int stack_size) { + uintptr_t raw = _beginthreadex(NULL, stack_size, entry, argument, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); + if (!raw) { + return 1; + } + *handle = (HANDLE)raw; + return 0; +} + static void thread_join(thread_handle handle) { WaitForSingleObject(handle, INFINITE); CloseHandle(handle); @@ -130,6 +139,25 @@ static int thread_spawn(thread_handle *handle, thread_entry entry, void *argumen return pthread_create(handle, NULL, entry, argument) != 0; } +#ifndef PTHREAD_STACK_MIN +#define PTHREAD_STACK_MIN (16 * 1024) +#endif + +static int thread_spawn_with_stack(thread_handle *handle, thread_entry entry, void *argument, size_t stack_size) { + pthread_attr_t attributes; + int failed; + if (pthread_attr_init(&attributes) != 0) { + return 1; + } + if (stack_size < (size_t)PTHREAD_STACK_MIN) { + stack_size = (size_t)PTHREAD_STACK_MIN; + } + failed = pthread_attr_setstacksize(&attributes, stack_size) != 0 || + pthread_create(handle, &attributes, entry, argument) != 0; + pthread_attr_destroy(&attributes); + return failed; +} + static void thread_join(thread_handle handle) { pthread_join(handle, NULL); } #define THREAD_RETURN void * @@ -730,18 +758,85 @@ static int case_lifecycle(void) { return failed; } +#define SMALL_STACK_QUOTE_DEPTH 4096 + +typedef struct small_stack_context { + int failed; + markdown_core_document *document; + char *input; +} small_stack_context; + +// Worker for dump_small_stack: parse and dump a quote chain whose depth +// would need several times this thread's stack if either path recursed. +// The parse and the dump run here; ownership returns to the spawning +// thread, which frees outside the constrained stack. +static THREAD_RETURN dump_small_stack_worker(void *user) { + small_stack_context *context = (small_stack_context *)user; + size_t input_length = (size_t)SMALL_STACK_QUOTE_DEPTH * 2 + 2; + markdown_core_parse_options options; + markdown_core_error *error = NULL; + context->failed = 1; + context->input = (char *)malloc(input_length + 1); + if (!context->input) { + return THREAD_RESULT; + } + for (size_t level = 0; level < (size_t)SMALL_STACK_QUOTE_DEPTH; level++) { + context->input[level * 2] = '>'; + context->input[level * 2 + 1] = ' '; + } + context->input[input_length - 2] = 'a'; + context->input[input_length - 1] = '\n'; + context->input[input_length] = '\0'; + markdown_core_parse_options_init(&options); + context->document = markdown_core_document_parse((const uint8_t *)context->input, input_length, &options, &error); + if (context->document && !error) { + uint8_t *dump = NULL; + size_t dump_length = 0; + if (markdown_core_document_dump(context->document, &dump, &dump_length, &error) && !error) { + // Every level contributes one line whose prefix grows with + // depth; a truncated dump would be far smaller. + context->failed = dump_length < (size_t)SMALL_STACK_QUOTE_DEPTH * 4; + markdown_core_dump_free(dump); + } + } + markdown_core_error_free(error); + return THREAD_RESULT; +} + +static int case_dump_small_stack(void) { + // The public parse and canonical dump are iterative, so adversarial + // nesting must survive a deliberately small thread stack — 256 KiB is + // far below what recursive descent at this depth would need. + thread_handle thread; + small_stack_context context = {1, NULL, NULL}; + if (thread_spawn_with_stack(&thread, dump_small_stack_worker, &context, 256 * 1024)) { + fprintf(stderr, "concurrency: could not spawn the small-stack thread\n"); + return 1; + } + thread_join(thread); + markdown_core_document_free(context.document); + free(context.input); + if (context.failed) { + fprintf(stderr, "concurrency: deep dump failed on a small thread stack\n"); + } + return context.failed; +} + int main(int argc, char **argv) { const char *case_name = NULL; for (int index = 1; index < argc; index++) { if (strcmp(argv[index], "--case") == 0 && index + 1 < argc) { case_name = argv[++index]; } else { - fprintf(stderr, "usage: concurrency_runner --case first_parse|stress|lifecycle|sessions\n"); + fprintf( + stderr, + "usage: concurrency_runner --case first_parse|stress|lifecycle|sessions|dump_small_stack\n" + ); return 1; } } if (!case_name) { - fprintf(stderr, "usage: concurrency_runner --case first_parse|stress|lifecycle|sessions\n"); + fprintf(stderr, "usage: concurrency_runner --case first_parse|stress|lifecycle|sessions|dump_small_stack\n"); return 1; } if (strcmp(case_name, "first_parse") == 0) { @@ -756,6 +851,9 @@ int main(int argc, char **argv) { if (strcmp(case_name, "sessions") == 0) { return case_sessions(); } + if (strcmp(case_name, "dump_small_stack") == 0) { + return case_dump_small_stack(); + } fprintf(stderr, "unknown case: %s\n", case_name); return 1; } diff --git a/packages/markdown-core/tests/runners/pathological_runner.c b/packages/markdown-core/tests/runners/pathological_runner.c index 279e6f2..11bdb56 100644 --- a/packages/markdown-core/tests/runners/pathological_runner.c +++ b/packages/markdown-core/tests/runners/pathological_runner.c @@ -17,8 +17,9 @@ * dump against a one-shot parse of the same text, folds the delta stream * into an id->revision mirror, and (with footnotes enabled) compares * footnote queries against a fresh session. The canonical dump is - * iterative like every other traversal, so session cases run at the same - * adversarial depths as the one-shot cases. + * iterative like every other traversal; session-case depths are bounded + * only by the dump volume the per-commit verification materializes (dump + * bytes grow quadratically with depth), never by a stack budget. */ #include #include @@ -894,13 +895,17 @@ static int case_session_backtick_runs(pc_context *context) { return result; } -/* 50000-deep block quotes — the same depth as the one-shot cases now that - * the canonical dump verifying every commit is iterative. The open chain - * spans the whole document on every commit. The innermost text edit rides - * the full chain; the mid-chain marker flip re-kinds level 64 and - * everything below it into a list and back. */ +/* 4096-deep block quotes. Depth here is bounded by dump volume, not by + * any stack: the replay harness verifies every commit with two canonical + * dumps whose per-line prefixes grow with depth, so dump bytes are + * quadratic in depth (50000 deep would be ~5 GiB per dump). Stack safety + * at adversarial depth is pinned separately by the small-stack dump case + * in the concurrency runner and the 50000-deep structural one-shot cases. + * The open chain spans the whole document on every commit. The innermost + * text edit rides the full chain; the mid-chain marker flip re-kinds level + * 64 and everything below it into a list and back. */ static int case_session_quotes_deep(pc_context *context) { - enum { QUOTE_DEPTH = 50000 }; + enum { QUOTE_DEPTH = 4096 }; markdown_core_parse_options options; sr_replay replay; int result = -1; From a158882b6547b4a4dee8bfcea4454ef2fb57591c Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 01:36:19 -0500 Subject: [PATCH 11/12] [Fix] Exclude the compiler index store from the Swift product-purity gate Co-Authored-By: Claude Fable 5 --- scripts/check-swift-source-archive.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/check-swift-source-archive.sh b/scripts/check-swift-source-archive.sh index 6ff1b14..bbc97e8 100755 --- a/scripts/check-swift-source-archive.sh +++ b/scripts/check-swift-source-archive.sh @@ -76,7 +76,11 @@ printf '%s\n' \ CLANG_MODULE_CACHE_PATH="$temporary/consumer-module-cache" \ swift run --disable-sandbox --package-path "$consumer" Consumer >/dev/null +# The compiler's index store mirrors SDK header names (Foundation ships +# NSScriptWhoseTests.h, for example), so it is excluded: the gate is about +# this repository's test and benchmark content reaching the product build. if find "$consumer/.build" -type f \ + -not -path '*/index/*' \ \( -iname '*test*' -o -iname '*benchmark*' -o -name 'CanonicalAstCases.swift' \ -o -name manifest.json -o -name '*.ast' \) -print | grep -q .; then echo "product-only Swift consumer built or carried test or benchmark content" >&2 From 25a4d68f2e6103a6ecefc686b6132dab1946c01d Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 28 Jul 2026 02:19:40 -0500 Subject: [PATCH 12/12] [Fix] Calibrate deep-traversal tests to platform resource limits Swift: deep value trees deallocate through recursive ARC releases, so the depth test owns them on a 16 MiB-stack thread and proves the iterative walk/dump on an explicit 512 KiB stack. Kotlin: dump bytes are quadratic in depth and exceed the Android instrumentation heap at 4096, so full-depth checks are structural and dump equality is pinned at 512. Co-Authored-By: Claude Fable 5 --- .../markdown/core/WalkerTraversalTest.kt | 33 +++++++- .../MarkdownCoreTests/SessionSuites.swift | 82 ++++++++++++++----- 2 files changed, 90 insertions(+), 25 deletions(-) diff --git a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt index 34f7608..33842bb 100644 --- a/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt +++ b/packages/kotlin-markdown-core/src/commonTest/kotlin/com/nouprax/markdown/core/WalkerTraversalTest.kt @@ -33,8 +33,13 @@ class WalkerTraversalTest { @Test fun adversarialNestingWalksAndDumpsBeyondTheCallStackBudget() { // 3072 nested quotes overflowed the recursive walker on the default - // JVM stack; the explicit frame stack must keep walking, dumping, - // and the delta path of an incremental commit working at 4096. + // JVM stack; the explicit frame stack must keep walking and the + // delta path of an incremental commit working at 4096. Walking is + // stack-bound but dumping is heap-bound — the canonical dump's + // per-line prefixes make dump bytes quadratic in depth, beyond the + // Android instrumentation heap at this depth — so full-depth + // verification is structural and dump equality is pinned at a + // depth whose volume every platform affords. val depth = 4096 val source = "> ".repeat(depth) + "leaf\n" val document = Document.parse(source) @@ -51,7 +56,6 @@ class WalkerTraversalTest { // Every node enters exactly once and exits exactly once: the // document, the quote chain, and the innermost paragraph and text. assertEquals(2 * (depth + 3), events) - assertTrue(document.dump().contains("BlockQuote")) val structural = RecordingVisitor() MarkupWalker.walk(document, structural) @@ -62,6 +66,29 @@ class WalkerTraversalTest { session.commit() session.replace(depth * 2, depth * 2 + 4, "seed") val second = session.commit() + var seedSeen = false + var secondEvents = 0 + MarkupWalker.walk(second.document) { event, node, _ -> + secondEvents += 1 + if (event == WalkEvent.ENTERING && node is Text) { + seedSeen = node.literal == "seed" + } + } + assertEquals(2 * (depth + 3), secondEvents) + assertTrue(seedSeen) + } + } + + @Test + fun deepIncrementalCommitDumpsIdenticallyToAOneShotParse() { + // Byte-for-byte dump equality for the deep delta path, at a depth + // whose quadratic dump volume fits every platform's test heap. + val depth = 512 + MarkupSession().use { session -> + session.append("> ".repeat(depth) + "leaf\n") + session.commit() + session.replace(depth * 2, depth * 2 + 4, "seed") + val second = session.commit() assertEquals(Document.parse("> ".repeat(depth) + "seed\n").dump(), second.document.dump()) } } diff --git a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift index ebe8cef..81a8b1d 100644 --- a/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift +++ b/packages/swift-markdown-core/Tests/MarkdownCoreTests/SessionSuites.swift @@ -1,3 +1,4 @@ +import Foundation import MarkdownCore import Testing @@ -299,34 +300,71 @@ private final class ConflationDriver { } @Suite("depth") struct DepthSuite { + /// Primitive results ferried out of the worker threads; access is + /// sequenced by thread completion, never concurrent. + private final class Outcome: @unchecked Sendable { + var failure: String? + var quoteEnters = 0 + var events = 0 + var dumpHasQuote = false + var commitMatchesReference = false + } + @Test("adversarial nesting walks, dumps, and commits beyond the call-stack budget") func adversarialNestingDepth() throws { - // 4096 nested quotes overflowed the recursive walker; the explicit - // frame stack must keep parse, walk, dump, and the delta path of an - // incremental commit working at the same depth. + // 4096 nested quotes overflowed the recursive walker. Two explicit + // stacks make the proof exact: deep value trees deallocate through + // recursive ARC releases, so every deep document lives and dies on + // a 16 MiB-stack thread, while the walk and dump run on a 512 KiB + // stack that recursive traversal at this depth could not survive. let depth = 4096 - let source = String(repeating: "> ", count: depth) + "leaf\n" - let document = try Document.parse(source) - - var quoteEnters = 0 - var events = 0 - MarkupWalker().walk(document) { event, node, _ in - events += 1 - if event == .entering, node is BlockQuote { quoteEnters += 1 } + let outcome = Outcome() + let finished = DispatchSemaphore(value: 0) + let owner = Thread { + defer { finished.signal() } + do { + let source = String(repeating: "> ", count: depth) + "leaf\n" + let document = try Document.parse(source) + + let walked = DispatchSemaphore(value: 0) + let walker = Thread { + defer { walked.signal() } + var quoteEnters = 0 + var events = 0 + MarkupWalker().walk(document) { event, node, _ in + events += 1 + if event == .entering, node is BlockQuote { quoteEnters += 1 } + } + outcome.quoteEnters = quoteEnters + outcome.events = events + outcome.dumpHasQuote = document.dump().contains("BlockQuote") + } + walker.stackSize = 512 * 1024 + walker.start() + walked.wait() + + let session = try MarkupSession() + try session.append(source) + _ = try session.commit() + try session.replace((depth * 2)..<(depth * 2 + 4), with: "seed") + let second = try session.commit() + let reference = try Document.parse(String(repeating: "> ", count: depth) + "seed\n") + outcome.commitMatchesReference = second.document.dump() == reference.dump() + } catch { + outcome.failure = String(describing: error) + } } - #expect(quoteEnters == depth) + owner.stackSize = 16 * 1024 * 1024 + owner.start() + finished.wait() + + #expect(outcome.failure == nil) + #expect(outcome.quoteEnters == depth) // Every node enters exactly once and exits exactly once: the // document, the quote chain, and the innermost paragraph and text. - #expect(events == 2 * (depth + 3)) - #expect(document.dump().contains("BlockQuote")) - - let session = try MarkupSession() - try session.append(source) - _ = try session.commit() - try session.replace((depth * 2)..<(depth * 2 + 4), with: "seed") - let second = try session.commit() - let reference = try Document.parse(String(repeating: "> ", count: depth) + "seed\n") - #expect(second.document.dump() == reference.dump()) + #expect(outcome.events == 2 * (depth + 3)) + #expect(outcome.dumpHasQuote) + #expect(outcome.commitMatchesReference) } }