Skip to content

Latest commit

 

History

364 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Markdown Engine

@osionos/markdown-engine — a compact Markdown engine built around a canonical src/ implementation.

What it provides

  • Markdown parsing into a typed AST
  • Semantic HTML rendering
  • Incremental reparsing from line-range patches
  • Diagnostics for malformed or out-of-bounds input

Boundary contract

The engine never imports a host application. It declares its own block vocabulary in blockContract.ts (MarkEngineBlock / MarkEngineBlockType) and its own table-layout rules in tableConfig.ts. A host adapts to the engine, not the reverse — which is what makes it reusable across apps.

MarkEngineBlock carries a [key: string]: unknown index signature, so a host with a richer block model stays structurally compatible without a cast. Hosts are expected to add one compile-time guard asserting MarkEngineBlockType extends <their BlockType>; then adding a block type here that the host does not know fails that single file instead of thirty call sites.

Consuming apps enforce the boundary with two gates on their side:

  • a tsc project that clears paths, so any @/… import is a hard resolution error;
  • a lexical check for host-package imports, ../../../ escapes, and react outside the React entry point (a real package still resolves, so tsc alone cannot catch it).

Entry points

The root entry point is React-free and DOM-free. That is load-bearing, not cosmetic: it must stay loadable by node --test --experimental-strip-types.

Subpath Contents Needs
. AST types, parse/parseInline, renderHtml, parseInlineMarkdown, render modes, worker clients, URL sugar —
./blocks detectBlockType, BLOCK_SHORTCUTS, parseMarkdownToBlocks, the block contract —
./inline inline formatting, text edits, autoformat, inline document, colour tokens —
./dom contentEditable selection/DOM helpers lib.dom
./react MarkdownView, renderReact, renderInlineToReact react (peer)
./tables table normalization and config —
./terminal ANSI renderer, callout icons —

Consumed as source

There is no build step for consumers: exports points at TypeScript source, resolved through the host's bundler/compiler alias rather than node_modules. npm run build:engine exists only for this repo's own tests, coverage and benchmarks.

tsconfig.json deliberately covers only the pure core (lib: ["ES2022"], no DOM) — it is the standing proof that the parser/renderer half needs neither a DOM nor React. The ./dom and ./react entry points are type-checked by the consuming app instead.

Public API

The current entry point for the rich inline/editor helpers is index.ts.

Typical usage:

import {
  compileMarkdownToHtml,
  incrementalParse,
  parseMarkdown,
  renderHtml,
} from ".";

const parsed = parseMarkdown("# Title\n\nA *fast* engine.", {
  documentVersion: 1,
});

const html = renderHtml(parsed.ast);
const compiled = compileMarkdownToHtml("# Title\n\nA *fast* engine.");

const next = incrementalParse("# Title\n\nA *fast* engine.", parsed, {
  fromLine: 2,
  toLine: 2,
  text: "A *very fast* engine.",
});

Inline editor helpers

For contentEditable integrations, markengine also exposes:

  • parseInlineMarkdown(source) to render canonical inline HTML
  • applyInlineFormatting(source, selection, command) to mutate inline content on the AST
  • readInlineEditorDomState(root) to convert editor DOM back into canonical source
  • getInlineEditorSelectionSnapshot(root) / getInlineEditorSelectionOffsets(root) to read selection state
  • setInlineEditorSelectionOffsets(root, offsets) to restore the browser selection
  • normalizeInlineLinkHref(href) to normalize user-entered inline link targets

Inline formatting architecture

The inline editor pipeline is intentionally split into small modules:

This split keeps the editor responsibilities separate:

  • DOM reading is independent from AST mutation.
  • AST mutation is independent from rendering.
  • Shared inline styling semantics live in one place instead of being repeated across renderers.

Performance notes

  • inlineFormatting.ts compares AST selections structurally instead of serializing them with JSON.stringify, avoiding extra allocations on repeated formatting operations.
  • Shared inline style helpers reduce duplicated per-render style construction logic across renderer implementations.

Worker responsiveness

Large documents can move parse and render work off the main thread through a single MarkEngineWorker. The worker is intentionally not a pool: parsing one Markdown document is sequential, and editor integrations usually have one active document parse at a time.

import { createBrowserMarkEngineWorkerClient } from ".";

const engine = createBrowserMarkEngineWorkerClient(
  new URL("./src/browser-worker.js", import.meta.url),
  { syncThresholdBytes: 8 * 1024 },
);

const parsed = await engine.parse(markdownSource);
const html = await engine.renderHtml(parsed.ast, {}, {
  sourceByteLength: markdownSource.length,
});

Node integrations can use createNodeMarkEngineWorkerClient() from the same public API. Sources above the threshold are encoded into transferable ArrayBuffers before posting to the worker. Documents below the threshold run synchronously because the worker round trip costs more than the parse.

This improves responsiveness, not throughput. A 1MB parse still costs CPU time, and end-to-end worker latency can include transfer plus structured-clone overhead; the win is that the main thread can keep handling input and paint. Run npm run bench:worker-blocking to compare main-thread timer delay for a 1MB parse on the main thread versus in MarkEngineWorker.

Architecture notes

For a deeper architecture overview, see docs/Markdown_engine.md.

How to add a new block type

  1. Add the typed AST shape in src/types.ts. Extend BlockKind, add a node interface with a precise span, and include that interface in the BlockNode union.
  2. Teach src/block-parser.ts to recognize the block before the paragraph fallback. Keep detection local and deterministic: parse only from cursor, return the parsed node, nextLine, and diagnostics, then let parseMarkdown add it to the block index.
  3. Preserve incremental parsing by making the new node span cover every source line it owns. src/incremental.ts relies on those spans to choose the smallest safe reparse window.
  4. Render the new node in src/renderer.ts and, when source-view output needs special markup, in src/source-renderer.ts.
  5. Add tests that cover detection, rendering, malformed input, and incremental edits around the new block. The CI gate requires at least 95% branch coverage for src/block-parser.ts and src/inline-parser.ts, so include both positive and fallback cases.
  6. Run npm run check, npm run coverage:parsers, and npm run bench:ci before opening a PR. The quality gate fails on explicit any, unused internal exports, circular imports, missing public API examples, and parser coverage drops.

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages