@orkestrel/markdown 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,18 @@
1
1
  # @orkestrel/markdown
2
2
 
3
- A zero-surprise, types-first markdown parser a hand-written scanner turns
4
- GitHub-Flavored Markdown into a typed AST (a discriminated union keyed by
5
- `element`), and a separate renderer projects that AST to sanitized, XSS-safe
6
- HTML. Total and depth-capped throughout: malformed or pathologically deep
7
- input degrades to literal text instead of throwing. Part of the `@orkestrel`
8
- line.
3
+ A types-first markdown layer over `@orkestrel/html`: parse GitHub-Flavored Markdown into a typed
4
+ AST, project that AST out to sanitized HTML or to canonical markdown source, and project an HTML AST
5
+ back in.
6
+
7
+ - **One AST, several projections.** Nodes are plain readonly data keyed by `element`; parsing,
8
+ querying, rewriting, folding, streaming, and every conversion are operations over it.
9
+ - **Both directions live here.** `markdownToHTML` and `htmlToMarkdown` are inverse projections
10
+ across the boundary, because what an HTML subtree means in markdown is markdown's knowledge, not
11
+ html's.
12
+ - **Sanitized by default, with no opt-out.** `renderHTML` takes one argument and composes
13
+ `@orkestrel/html`'s sanitize floor, so there is no call shape that emits unsafe HTML.
14
+ - **Bounded by design.** Every parse, traversal, and projection is iterative and depth-capped, so
15
+ malformed or hostile input degrades to literal text instead of throwing.
9
16
 
10
17
  ## Install
11
18
 
@@ -15,88 +22,66 @@ npm install @orkestrel/markdown
15
22
 
16
23
  ## Requirements
17
24
 
18
- - Node.js >= 24
19
- - ESM-only (no CommonJS build)
25
+ - Node.js >= 22.12
26
+ - Ships ES and CommonJS builds with its own `.d.ts` types
27
+ - Two runtime dependencies, `@orkestrel/html` and `@orkestrel/contract`
20
28
 
21
29
  ## Usage
22
30
 
23
31
  ```ts
24
- import { createMarkdown, renderHTML } from '@orkestrel/markdown'
32
+ import { createMarkdown, htmlToMarkdown, renderHTML, renderMarkdown } from '@orkestrel/markdown'
33
+ import { parseDocument as parseHTML } from '@orkestrel/html'
25
34
 
26
35
  const markdown = createMarkdown('# Hi\n\nRead the [guide](./guide.md) for more, *thanks*.')
36
+
27
37
  markdown.document
28
38
  // { element: 'document', children: [...] } — the typed, render-agnostic AST
29
39
 
30
40
  renderHTML(markdown.document)
31
- // '<h1>Hi</h1>\n<p>Read the <a href="./guide.md">guide</a> for more, <em>thanks</em>.</p>'
32
- ```
33
-
34
- `createMarkdown(markdown)` (or `new Markdown(markdown)`) runs a two-phase
35
- parse (block phase, then inline phase) and stores the result as a stateful
36
- workspace's `document` — a render-agnostic `MarkdownDocument`. The workspace
37
- also exposes `find` / `filter` / `map` / `reduce` / `fold` / `stream` /
38
- iteration over the AST. `renderHTML(node)` HTML-escapes all text and
39
- attributes and sanitizes link `href`s (an unsafe scheme like `javascript:` or
40
- `data:` is dropped), so even hostile content cannot inject markup or script.
41
- `renderMarkdown(node)` writes canonical markdown source back out — a
42
- `parseDocument(renderMarkdown(doc))` round-trip always deep-equals `doc`. A
43
- fold projects the AST to any shape (a plain string, a DOM tree, a count)
44
- through one total, per-element handler table, with no writer coupling built
45
- in.
46
-
47
- ## Validating untrusted ASTs
48
-
49
- A parsed or deserialized AST crossing a trust boundary (an RPC payload, a
50
- cached document) can be checked without throwing:
41
+ // '<h1>Hi</h1><p>Read the <a href="./guide.md">guide</a> for more, <em>thanks</em>.</p>'
51
42
 
52
- ```ts
53
- import { isMarkdownNode } from '@orkestrel/markdown'
54
-
55
- isMarkdownNode({ element: 'text', value: 'hi' }) // true
56
- isMarkdownNode({ element: 'bogus' }) // false
43
+ // …and back the other way.
44
+ renderMarkdown(htmlToMarkdown(parseHTML('<h1>Release notes</h1><p>Ship <b>fast</b>.</p>')))
45
+ // '# Release notes\n\nShip **fast**.'
57
46
  ```
58
47
 
59
- `isMarkdownNode`, `isMarkdownDocument`, `isBlockNode`, and `isInlineNode` are
60
- total guards safe to call on cyclic or adversarial input, even deeply
61
- nested structures.
48
+ `createMarkdown` (or `new Markdown`) runs a two-phase parse — block phase, then inline phase — and
49
+ holds the result as a stateful workspace exposing `find` / `filter` / `map` / `reduce` / `fold` /
50
+ `stream` / iteration over the AST. `renderHTML` projects that AST onto html's AST, sanitizes it
51
+ against a floor no option can lower, and serializes it. `renderMarkdown` writes canonical markdown
52
+ source instead. `htmlToMarkdown` folds an HTML AST back down to a `MarkdownDocument`, re-sanitizing
53
+ every destination as it goes. Guards (`isMarkdownNode`, `isMarkdownDocument`, `isBlockNode`,
54
+ `isInlineNode`) validate an AST that arrives from outside — an RPC payload, a cached document —
55
+ without ever throwing, and the non-recursive leaf nodes each compile to a `@orkestrel/contract`
56
+ bundle of guard, parser, JSON Schema, and seeded generator.
62
57
 
63
- ## Contract-backed leaf shapes
58
+ ## Laws
64
59
 
65
- The non-recursive leaf nodes (`TextNode`, `CodeSpanNode`, `CodeBlockNode`,
66
- `ThematicBreakNode`) each have a compiled contract — a guard, parser, JSON
67
- Schema, and seeded generator from one shape declaration, built on
68
- `@orkestrel/contract`:
60
+ - **Markdown fixpoint** — `parseDocument(renderMarkdown(document))` deep-equals a parser-produced
61
+ `document`, and `renderMarkdown` is idempotent.
62
+ - **Projection anchor** `parseDocument(renderMarkdown(htmlToMarkdown(x)))` deep-equals
63
+ `htmlToMarkdown(x)`: whatever the projection emits, markdown can write it and read it back
64
+ unchanged.
69
65
 
70
- ```ts
71
- import { createTextContract } from '@orkestrel/markdown'
72
-
73
- const text = createTextContract()
74
- text.schema // the compiled JSON Schema
75
- text.generate() // a seeded, schema-valid TextNode
76
- ```
66
+ Both round-trip laws hold within the depth budget — markdown's `MAX_DEPTH`, plus html's own cap on
67
+ the inbound trip; beyond it only totality is promised.
77
68
 
78
- ## Safety notes
69
+ - **Sanitized output** — `renderHTML` refuses `javascript:`, `data:`, `vbscript:`, `file:`, and
70
+ protocol-relative destinations, removes unsafe subtrees whole, and strips every handler and
71
+ styling attribute, whatever the input AST claims.
79
72
 
80
- - `renderHTML`'s `href`s are restricted to a safe scheme allowlist (`http`,
81
- `https`, `mailto`, `tel`, or scheme-less/relative/anchor links) anything
82
- else is dropped.
83
- - All of `renderHTML`'s rendered text and attributes are HTML-escaped.
84
- `renderMarkdown` is not an HTML boundary — it writes markdown source, not
85
- markup — so no HTML-escaping applies there.
86
- - Parsing and rendering are depth-capped (`MAX_DEPTH`); past that depth the
87
- parser/writer degrades to literal text instead of recursing further or
88
- throwing.
73
+ HTML is richer than markdown, so what survives the inbound trip is the projected AST, not the input
74
+ bytes: comments and doctypes vanish, an element with no markdown meaning unwraps to its children,
75
+ and block content inside a table cell flattens to one line.
89
76
 
90
77
  ## Guide
91
78
 
92
- For the full surface — the AST shape, the two-phase parse, GFM tables, and
93
- the contract-backed leaf shapes — see
94
- [`guides/src/markdown.md`](guides/src/markdown.md).
79
+ For the full surface — the AST shape, the two-phase parse, the sanitization policy, the projection
80
+ seam, and the contract-backed leaf shapes — see [`guides/src/markdown.md`](guides/src/markdown.md).
95
81
 
96
82
  ## Package
97
83
 
98
- Published as a single typed entry point per the `exports` field in
99
- `package.json`.
84
+ Published as a single typed entry point per the `exports` field in `package.json`.
100
85
 
101
86
  ## License
102
87