@jarenjs/md 0.34.0

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.
Files changed (49) hide show
  1. package/README.md +520 -0
  2. package/dist/types/ast.d.ts +181 -0
  3. package/dist/types/bake.d.ts +61 -0
  4. package/dist/types/compiler.d.ts +141 -0
  5. package/dist/types/component/index.d.ts +101 -0
  6. package/dist/types/directives.d.ts +126 -0
  7. package/dist/types/entities.d.ts +40 -0
  8. package/dist/types/footnotes.d.ts +83 -0
  9. package/dist/types/frontmatter.d.ts +67 -0
  10. package/dist/types/html.d.ts +72 -0
  11. package/dist/types/index.d.ts +30 -0
  12. package/dist/types/loader.d.ts +84 -0
  13. package/dist/types/mdx.d.ts +45 -0
  14. package/dist/types/parser.d.ts +116 -0
  15. package/dist/types/plugins/highlight.d.ts +64 -0
  16. package/dist/types/plugins/index.d.ts +64 -0
  17. package/dist/types/plugins/mermaid.d.ts +12 -0
  18. package/dist/types/scanner.d.ts +240 -0
  19. package/dist/types/to-html.d.ts +104 -0
  20. package/dist/types/to-md.d.ts +23 -0
  21. package/dist/types/to-vnode.d.ts +161 -0
  22. package/dist/types/utils.d.ts +63 -0
  23. package/docs/LOADER.md +92 -0
  24. package/docs/MD-FORMAT.md +502 -0
  25. package/docs/PLUGINS.md +277 -0
  26. package/package.json +80 -0
  27. package/schemas/jaren-md-ast.schema.json +296 -0
  28. package/src/ast.js +346 -0
  29. package/src/bake.js +104 -0
  30. package/src/compiler.js +167 -0
  31. package/src/component/index.js +191 -0
  32. package/src/directives.js +371 -0
  33. package/src/entities.js +107 -0
  34. package/src/footnotes.js +180 -0
  35. package/src/frontmatter.js +947 -0
  36. package/src/html.js +281 -0
  37. package/src/index.js +76 -0
  38. package/src/loader.js +0 -0
  39. package/src/mdx.js +219 -0
  40. package/src/parser.js +1685 -0
  41. package/src/plugins/highlight.js +325 -0
  42. package/src/plugins/index.js +75 -0
  43. package/src/plugins/mermaid.js +14 -0
  44. package/src/scanner.js +832 -0
  45. package/src/to-html.js +425 -0
  46. package/src/to-md.js +396 -0
  47. package/src/to-vnode.js +766 -0
  48. package/src/utils.js +107 -0
  49. package/styles/md.css +238 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @file Line-level scanners for the block parser.
3
+ *
4
+ * Pure, allocation-conscious functions that classify one (detabbed)
5
+ * line at a char-code level: does a construct start here, and where
6
+ * does its content begin? The parser in parser.js owns all state; this
7
+ * module owns none. Every regular expression is compiled once at
8
+ * module load — nothing in here builds a pattern per call.
9
+ */
10
+ /**
11
+ * Thematic break: three or more `*`, `-` or `_` (same character),
12
+ * interleaved with spaces, nothing else on the line.
13
+ * @param {string} line
14
+ * @param {number} start first non-space offset
15
+ * @returns {boolean}
16
+ */
17
+ export declare function scanThematicBreak(line: string, start: number): boolean;
18
+ /**
19
+ * ATX heading: `#{1,6}` followed by space or end of line. Returns the
20
+ * depth and the heading text (closing `#` run stripped), or null.
21
+ * @param {string} line
22
+ * @param {number} start first non-space offset
23
+ * @returns {{ depth: number, text: string } | null}
24
+ */
25
+ export declare function scanAtxHeading(line: string, start: number): {
26
+ depth: number;
27
+ text: string;
28
+ } | null;
29
+ /**
30
+ * Code fence opener: three or more backticks or tildes. A backtick
31
+ * fence's info string may not contain a backtick.
32
+ * @param {string} line
33
+ * @param {number} start first non-space offset
34
+ * @returns {{ marker: number, length: number, info: string } | null}
35
+ */
36
+ export declare function scanFenceOpen(line: string, start: number): {
37
+ marker: number;
38
+ length: number;
39
+ info: string;
40
+ } | null;
41
+ /**
42
+ * Does this line close a fence opened with `marker` × `length`?
43
+ * @param {string} line
44
+ * @param {number} marker
45
+ * @param {number} length
46
+ * @returns {boolean}
47
+ */
48
+ export declare function scanFenceClose(line: string, marker: number, length: number): boolean;
49
+ /**
50
+ * Split a fence info string into `lang` (first word) and `meta` (the
51
+ * rest), resolving backslash escapes and character references in both.
52
+ * @param {string} info
53
+ * @returns {{ lang: string|null, meta: string|null }}
54
+ */
55
+ export declare function splitFenceInfo(info: string): {
56
+ lang: string | null;
57
+ meta: string | null;
58
+ };
59
+ /**
60
+ * Blockquote marker at `offset`: `>` with an optional following space.
61
+ * Returns the content offset, or -1.
62
+ * @param {string} line
63
+ * @param {number} offset first non-space offset
64
+ * @returns {number}
65
+ */
66
+ export declare function scanBlockquote(line: string, offset: number): number;
67
+ /**
68
+ * List marker: `-`/`+`/`*` bullet or `1.`/`1)` ordered (start ≤ 9
69
+ * digits), followed by a space or line end. Returns the marker
70
+ * geometry the parser turns into a list container, or null.
71
+ * @param {string} line
72
+ * @param {number} start first non-space offset
73
+ * @returns {{ ordered: boolean, bullet: string, start: number,
74
+ * delimiter: string, contentOffset: number } | null}
75
+ */
76
+ export declare function scanListMarker(line: string, start: number): {
77
+ ordered: boolean;
78
+ bullet: string;
79
+ start: number;
80
+ delimiter: string;
81
+ contentOffset: number;
82
+ } | null;
83
+ /**
84
+ * Setext underline under an open paragraph: `=` run (depth 1) or `-`
85
+ * run (depth 2), possibly space-padded. Returns 0 when neither.
86
+ * @param {string} line
87
+ * @param {number} start first non-space offset
88
+ * @returns {number}
89
+ */
90
+ export declare function scanSetextUnderline(line: string, start: number): number;
91
+ /**
92
+ * GFM table delimiter row: cells of `---`, `:--`, `--:`, `:-:` split
93
+ * by pipes. Returns the alignment array, or null.
94
+ * @param {string} line
95
+ * @returns {(string|null)[] | null}
96
+ */
97
+ export declare function scanTableDelimiter(line: string): (string | null)[] | null;
98
+ /**
99
+ * Split a table row into raw cell strings on unescaped `|`, honoring
100
+ * `\|` and pipes inside backtick code spans. Leading and trailing
101
+ * empty cells from outer pipes are dropped. Returns null when the line
102
+ * contains no pipe at all.
103
+ * @param {string} line
104
+ * @returns {string[] | null}
105
+ */
106
+ export declare function splitTableRow(line: string): string[] | null;
107
+ /**
108
+ * Classify an HTML block opener at `start` (CommonMark types 1–7);
109
+ * 0 means no HTML block starts here. Type 7 is only valid when no
110
+ * paragraph is open — the caller passes `paragraphOpen`.
111
+ * @param {string} line
112
+ * @param {number} start first non-space offset
113
+ * @param {boolean} paragraphOpen
114
+ * @returns {number}
115
+ */
116
+ export declare function scanHtmlBlockStart(line: string, start: number, paragraphOpen: boolean): number;
117
+ /**
118
+ * Does this line end an HTML block of `kind`? Types 6/7 end on the
119
+ * following blank line (the parser checks that); types 1–5 end on a
120
+ * content condition, which may sit on the opening line itself.
121
+ * @param {number} kind
122
+ * @param {string} line
123
+ * @returns {boolean}
124
+ */
125
+ export declare function scanHtmlBlockEnd(kind: number, line: string): boolean;
126
+ /**
127
+ * Is this char code a space or tab?
128
+ * @param {number} c
129
+ * @returns {boolean}
130
+ */
131
+ export declare function isSpaceCode(c: number): boolean;
132
+ /**
133
+ * Link reference definition at the start of a closed paragraph's text:
134
+ * `[label]: destination "title"` (title optional, may be single-,
135
+ * double- or paren-quoted; destination may be `<>`-wrapped). Returns
136
+ * the definition and the offset after it, or null.
137
+ * @param {string} text the paragraph's raw text
138
+ * @param {number} pos
139
+ * @returns {{ label: string, url: string, title: string|null, end: number } | null}
140
+ */
141
+ export declare function scanLinkDefinition(text: string, pos: number): {
142
+ label: string;
143
+ url: string;
144
+ title: string | null;
145
+ end: number;
146
+ } | null;
147
+ /**
148
+ * Scan a link destination at `pos`: `<...>` wrapped or a run of
149
+ * non-space characters with balanced parens.
150
+ * @param {string} text
151
+ * @param {number} pos
152
+ * @returns {{ url: string, end: number } | null}
153
+ */
154
+ export declare function scanLinkDestination(text: string, pos: number): {
155
+ url: string;
156
+ end: number;
157
+ } | null;
158
+ /**
159
+ * Scan a link title at `pos`: `"..."`, `'...'` or `(...)`.
160
+ * @param {string} text
161
+ * @param {number} pos
162
+ * @returns {{ title: string, end: number } | null}
163
+ */
164
+ export declare function scanLinkTitle(text: string, pos: number): {
165
+ title: string;
166
+ end: number;
167
+ } | null;
168
+ /** ASCII punctuation membership (emphasis flanking, backslash escapes). */
169
+ export declare const ASCII_PUNCT: Uint8Array<ArrayBuffer>;
170
+ /**
171
+ * Is this code point a whitespace character in the spec's sense?
172
+ * @param {number} point
173
+ * @returns {boolean}
174
+ */
175
+ export declare function isUnicodeWhitespace(point: number): boolean;
176
+ /**
177
+ * Is this code point a punctuation character in the spec's sense (P* or S*)?
178
+ * @param {number} point
179
+ * @returns {boolean}
180
+ */
181
+ export declare function isUnicodePunctuation(point: number): boolean;
182
+ /**
183
+ * The whole code point ending at `pos`, so a run preceded by an astral
184
+ * symbol classifies on the symbol and not on a surrogate half.
185
+ * @param {string} src @param {number} pos
186
+ * @returns {number}
187
+ */
188
+ export declare function codePointBefore(src: string, pos: number): number;
189
+ /**
190
+ * Normalize a link label for matching: trim, collapse internal
191
+ * whitespace runs to one space, and case fold.
192
+ *
193
+ * The fold is lower→upper→lower, not `toLowerCase()`: the spec asks for
194
+ * Unicode case folding, under which `ẞ` matches `SS`, while lower-casing
195
+ * alone maps `ẞ` to `ß` and never meets `ss`. The round trip routes both
196
+ * spellings through the same expansion (`ẞ`→`ß`→`SS`→`ss`, and `fi`→`fi`),
197
+ * which is as close to the full fold as a zero-dependency package gets
198
+ * without shipping the table.
199
+ * @param {string} label
200
+ * @returns {string}
201
+ */
202
+ export declare function normalizeLabel(label: string): string;
203
+ /**
204
+ * Footnote definition opener: `[^label]:` and the spaces after it.
205
+ * @param {string} line
206
+ * @param {number} start first non-space offset
207
+ * @returns {{ label: string, contentOffset: number } | null}
208
+ */
209
+ export declare function scanFootnoteDefinition(line: string, start: number): {
210
+ label: string;
211
+ contentOffset: number;
212
+ } | null;
213
+ /**
214
+ * Footnote reference: `[^label]` in inline text.
215
+ * @param {string} text
216
+ * @param {number} start offset of the `[`
217
+ * @returns {{ label: string, end: number } | null}
218
+ */
219
+ export declare function scanFootnoteReference(text: string, start: number): {
220
+ label: string;
221
+ end: number;
222
+ } | null;
223
+ /**
224
+ * Find every extended autolink in one text value (GFM §Autolinks): bare
225
+ * `www.…`, `http://…`, `https://…`, `ftp://…` and email addresses.
226
+ * Returns the matches in order, or `null` when there are none — the
227
+ * common answer, and the one that costs nothing.
228
+ *
229
+ * This works on a TEXT VALUE and not on the source, which is what makes
230
+ * the entity rule meaningful: `&copy;` has already become `©` by the
231
+ * time we look, so the only `&…;` left to exclude is one that was never
232
+ * an entity in the first place.
233
+ * @param {string} value
234
+ * @returns {{ start: number, end: number, url: string }[] | null}
235
+ */
236
+ export declare function scanAutolinkLiterals(value: string): {
237
+ start: number;
238
+ end: number;
239
+ url: string;
240
+ }[] | null;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @file AST → HTML string, directly.
3
+ *
4
+ * The package's second emitter, and deliberately not a wrapper around
5
+ * the first (ARCHITECTURE.md "Two emitters"). `mdToVnode` builds a
6
+ * PATCHABLE TREE — keyed, memoized, reference-stable — whose safety is
7
+ * structural: a vnode has no slot for unescaped author markup, which is
8
+ * why a lone `</div>` cannot survive it. `toHtml` builds BYTES, and a
9
+ * string can hold half an element, so the raw-HTML corner of CommonMark
10
+ * is reachable here and only here.
11
+ *
12
+ * What the two share is factored, not copied: the escapers are
13
+ * `@jarenjs/view`'s (the same ones its SSR renderer uses, which is what
14
+ * makes the two outputs byte-identical for markup a vnode can express),
15
+ * the URL policy is `@jarenjs/view/helpers`, heading slugs come from
16
+ * `@jarenjs/core/string` through `utils.js`, and plugin dispatch is the
17
+ * parser's own table.
18
+ *
19
+ * Escaping is the default and `html: 'raw'` is per-call: the default
20
+ * cannot emit unescaped author content, and no jaren surface passes
21
+ * `'raw'`.
22
+ */
23
+ export type MdNode = import('./ast.js').MdNode;
24
+ export type MdHtmlOptions = {
25
+ /**
26
+ * raw-HTML policy (default
27
+ * `'escape'`): show the markup as literal text, drop it (the vnode
28
+ * path's default), or pass it through verbatim. **`'raw'` emits author
29
+ * content as live markup and is for trusted input only** — a document
30
+ * from a user, a fetch or a model must never be rendered with it.
31
+ */
32
+ html?: 'escape' | 'skip' | 'raw';
33
+ /**
34
+ * link/image URL
35
+ * filter, replacing the default deny-list; return the URL to emit or
36
+ * `null` to drop the attribute. It applies in EVERY `html` mode,
37
+ * `'raw'` included: `'raw'` is a statement about HTML blocks, not a
38
+ * blanket trust, so a markdown `[x](javascript:…)` is still filtered.
39
+ */
40
+ sanitizeUrl?: (url: string) => (string | null);
41
+ /**
42
+ * plugin set (must match the parse set for
43
+ * claimed nodes); a plugin contributes `toHtml(node, ctx)` here the
44
+ * way it contributes `render` to the vnode path.
45
+ */
46
+ plugins?: any[];
47
+ /**
48
+ * GitHub-compatible `id` per heading
49
+ * (default `false`; see MD-FORMAT.md §4.5 for why it is opt-in).
50
+ */
51
+ headingIds?: boolean;
52
+ /**
53
+ * prepended to every heading id and
54
+ * anchor href (default `''`); set it for markdown you did not author.
55
+ */
56
+ slugPrefix?: string;
57
+ /**
58
+ * append a `#` permalink to each
59
+ * heading (default `false`). Requires `headingIds`.
60
+ */
61
+ headingAnchors?: boolean;
62
+ /**
63
+ * the accessible name of the
64
+ * appended footnotes section (default `'Footnotes'`); a localized page
65
+ * sets it, since it is the one string the emitter writes that a reader
66
+ * can hear.
67
+ */
68
+ footnotesLabel?: string;
69
+ /**
70
+ * wrapping element, written as it appears in
71
+ * the start tag (`'article class="md"'`). Default `null`: bare
72
+ * fragment HTML, which is what a consumer concatenating into a
73
+ * template wants.
74
+ */
75
+ wrap?: string;
76
+ };
77
+ export type HtmlCtx = {
78
+ tables: any;
79
+ html: 'escape' | 'skip' | 'raw';
80
+ sanitizeUrl: (url: string) => (string | null);
81
+ headingIds: boolean;
82
+ slugPrefix: string;
83
+ headingAnchors: boolean;
84
+ slugs: Map<string, number>;
85
+ options: MdHtmlOptions;
86
+ footnotes: import('./footnotes.js').Footnotes | null;
87
+ footnotePrefix: string;
88
+ footnotesLabel: string;
89
+ };
90
+ /**
91
+ * Render an MdDocument (or a CompiledMd, an AST array, a single node)
92
+ * to an HTML string.
93
+ *
94
+ * @example
95
+ * toHtml(parseMarkdown('# Hi')); // '<h1>Hi</h1>'
96
+ * toHtml(parseMarkdown('<b>x</b>')); // '&lt;b&gt;x&lt;/b&gt;'
97
+ * toHtml(doc, { html: 'raw' }); // trusted input only
98
+ * toHtml(doc, { wrap: 'article class="md"' }); // wrapped
99
+ *
100
+ * @param {any} docOrAst MdDocument, CompiledMd, MdNode[] or MdNode
101
+ * @param {MdHtmlOptions} [options]
102
+ * @returns {string}
103
+ */
104
+ export declare function toHtml(docOrAst: any, options?: MdHtmlOptions): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @file Canonical Markdown printer: AST → text (MD-FORMAT.md §5).
3
+ *
4
+ * The printer is the round-trip half of the package: canonical output
5
+ * re-parses to a deep-equal AST. Canonical choices: ATX headings, `-`
6
+ * bullets, `1.`/`2.` ordered markers renumbered from `start`, backtick
7
+ * fences, `*`/`**` emphasis, inline links, backslash hard breaks and
8
+ * piped tables. Dispatch is one prebuilt table per node class — no
9
+ * per-node type chains.
10
+ */
11
+ export type MdNode = import('./ast.js').MdNode;
12
+ /**
13
+ * Print an MdDocument (or a bare AST array / single node) to canonical
14
+ * Markdown. Frontmatter re-emits as a `---json` block by default —
15
+ * exact, syntax-neutral round-trips (MD-FORMAT.md §5).
16
+ *
17
+ * @param {any} docOrAst
18
+ * @param {{ frontmatter?: boolean }} [options]
19
+ * @returns {string}
20
+ */
21
+ export declare function toMarkdown(docOrAst: any, options?: {
22
+ frontmatter?: boolean;
23
+ }): string;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file AST → @jarenjs/view vnodes, and the hydrating renderer.
3
+ *
4
+ * The emitter is a prebuilt dispatch table keyed on node type; plugin
5
+ * `render` entries shadow the core entries (that is how the highlight
6
+ * plugin takes over `code` nodes). Two identity guarantees feed the
7
+ * view patcher's O(1) fast paths (VIEW-FORMAT.md §5.1):
8
+ *
9
+ * - per-node memo: the same AST node reference emits the same vnode
10
+ * reference, so unchanged subtrees of a JSLT-transformed document
11
+ * patch in O(1);
12
+ * - content-hash keys: block vnodes are keyed by a hash of their
13
+ * content, so moved blocks reorder instead of rebuilding.
14
+ *
15
+ * Raw HTML nodes are dropped by default; `options.html: 'text'` shows
16
+ * them literally, and `'vnode'` PARSES them through an allow-list
17
+ * (`parseHtmlFragment`, or an injected `parseHtml`). The vnode format
18
+ * has no unescaped output in any of the three, which is what makes
19
+ * dropping the safe default for untrusted Markdown. Link and image URLs are
20
+ * filtered on the same principle: a destination whose scheme can execute
21
+ * (`javascript:`, `vbscript:`) or stand in for a document
22
+ * (`data:text/html`, `file:`) loses its attribute rather than reaching
23
+ * the page. The AST keeps the URL verbatim, so `toMarkdown` still
24
+ * round-trips what the author wrote — only the vnode is filtered.
25
+ */
26
+ export type MdNode = import('./ast.js').MdNode;
27
+ export type MdDocument = import('./ast.js').MdDocument;
28
+ export type MdVnodeOptions = {
29
+ /**
30
+ * plugin set (must match the parse set for claimed nodes)
31
+ */
32
+ plugins?: any[];
33
+ /**
34
+ * raw HTML handling (default
35
+ * 'skip'): drop it, show it as literal text, or parse it to vnodes
36
+ */
37
+ html?: 'skip' | 'text' | 'vnode';
38
+ /**
39
+ * the parser `html: 'vnode'`
40
+ * uses (default `parseHtmlFragment` from `@jarenjs/md/html`) — the
41
+ * injection point for a host's own sanitizer. It returns a LIST of
42
+ * vnodes (empty or null when nothing survived); an array is always
43
+ * read as a list, because a vnode is an array too and the two would
44
+ * otherwise be indistinguishable.
45
+ */
46
+ parseHtml?: (html: string) => any;
47
+ /**
48
+ * link/image URL
49
+ * filter, replacing the default deny-list; return the URL to emit, or
50
+ * `null` to drop the attribute. Supply one only to widen the policy for
51
+ * trusted content (a custom scheme, say) — it is the whole guard.
52
+ */
53
+ sanitizeUrl?: (url: string) => (string | null);
54
+ /**
55
+ * give every heading a GitHub-compatible
56
+ * `id` so `[see below](#the-section)` lands (default `false`). The
57
+ * default is OFF ON PURPOSE and must stay that way: CommonMark
58
+ * specifies `<h1>Foo</h1>`, so an id emitted by default would fail
59
+ * every heading example in the conformance corpus and make the
60
+ * package's published score a lie. A host that wants anchors asks for
61
+ * them; the spec path stays honest.
62
+ */
63
+ headingIds?: boolean;
64
+ /**
65
+ * prepended to every heading id and
66
+ * anchor href (default `''`). A host rendering markdown it did not
67
+ * author into a page it owns sets this — GitHub's own answer is
68
+ * `user-content-` — so an author cannot mint an id that collides with
69
+ * the host's own DOM.
70
+ */
71
+ slugPrefix?: string;
72
+ /**
73
+ * append a `#` link to each heading
74
+ * so a reader can copy a link to the section (default `false`).
75
+ * Requires `headingIds`; without ids there is nothing to link to.
76
+ */
77
+ headingAnchors?: boolean;
78
+ /**
79
+ * the accessible name of the
80
+ * appended footnotes section (default `'Footnotes'`) — the one string
81
+ * this emitter writes that a reader can hear.
82
+ */
83
+ footnotesLabel?: string;
84
+ /**
85
+ * give each top-level block a content-hash
86
+ * `key` (default `true`).
87
+ *
88
+ * Keys are what let the view patcher REORDER blocks instead of
89
+ * rebuilding them, so any caller whose output will be patched needs
90
+ * them — and computing one means hashing the block's whole subtree,
91
+ * which is around a quarter of this emitter's cost. A caller that
92
+ * renders once and throws the tree away (SSR, a string, a snapshot)
93
+ * pays that for nothing and should pass `false`.
94
+ *
95
+ * It is deliberately NOT inferred. A renderer cannot know whether its
96
+ * output will be patched, and guessing wrong silently turns O(1)
97
+ * reconciliation into a rebuild — a correctness-shaped failure with no
98
+ * error message. The default is the safe answer; opting out is a
99
+ * statement about the caller, which is why every caller in this
100
+ * repository that passes `false` says why.
101
+ */
102
+ keyed?: boolean;
103
+ };
104
+ export type RenderCtx = {
105
+ tables: any;
106
+ html: 'skip' | 'text' | 'vnode';
107
+ options: MdVnodeOptions;
108
+ parseHtml: (html: string) => any;
109
+ sanitizeUrl: (url: string) => (string | null);
110
+ headingIds: boolean;
111
+ slugPrefix: string;
112
+ headingAnchors: boolean;
113
+ slugs: Map<string, number>;
114
+ footnotes: import('./footnotes.js').Footnotes | null;
115
+ footnotePrefix: string;
116
+ footnotesLabel: string;
117
+ hash: (str: string) => string;
118
+ counts: Map<string, number>;
119
+ };
120
+ /**
121
+ * Emit a whole document (or AST array) as one `article.md` vnode with
122
+ * content-hash-keyed block children.
123
+ *
124
+ * @example
125
+ * mdToVnode(parseMarkdown('# Hi'))
126
+ * // ['article', { class: 'md' }, [['h1', { key: '…' }, 'Hi']]]
127
+ *
128
+ * @param {any} docOrCompiled MdDocument, CompiledMd or MdNode[]
129
+ * @param {MdVnodeOptions} [options]
130
+ * @returns {any}
131
+ */
132
+ export declare function mdToVnode(docOrCompiled: any, options?: MdVnodeOptions): any;
133
+ /**
134
+ * Create a renderer over `@jarenjs/view`'s DOM patcher that also runs
135
+ * plugin `hydrate` hooks after mount (PLUGINS.md §5). Returns a
136
+ * `render(docOrCompiled)` function.
137
+ *
138
+ * @param {{
139
+ * container: any,
140
+ * plugins?: any[],
141
+ * html?: 'skip'|'text',
142
+ * headingIds?: boolean,
143
+ * slugPrefix?: string,
144
+ * headingAnchors?: boolean,
145
+ * document?: any,
146
+ * onEvent?: (binding: any, event: any) => void,
147
+ * onHydrateError?: (err: any) => void,
148
+ * }} options
149
+ * @returns {(docOrCompiled: any) => void}
150
+ */
151
+ export declare function createMdRenderer(options: {
152
+ container: any;
153
+ plugins?: any[];
154
+ html?: 'skip' | 'text';
155
+ headingIds?: boolean;
156
+ slugPrefix?: string;
157
+ headingAnchors?: boolean;
158
+ document?: any;
159
+ onEvent?: (binding: any, event: any) => void;
160
+ onHydrateError?: (err: any) => void;
161
+ }): (docOrCompiled: any) => void;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @file Small shared helpers for the md package.
3
+ *
4
+ * The content hash is the package's identity primitive — block vnode
5
+ * keys, the document `meta.hash`, and the mermaid SVG cache are all keyed
6
+ * by it — so md re-exports the suite's single `hashContent` from
7
+ * `@jarenjs/core` rather than carrying its own copy; equal content hits
8
+ * O(1) fast paths everywhere downstream. `fnv1a` is that same mixing
9
+ * step, exposed for the two callers that fold a hash incrementally (the
10
+ * streaming parser's chunks, the structural block-key walk) and so must
11
+ * seed it themselves from `FNV1A_OFFSET_BASIS`. Heading slugs come from
12
+ * the same place for the same reason: `slugify` is a pure text→fragment
13
+ * transform with no Markdown knowledge, so the suite keeps exactly one
14
+ * of it. The remaining helpers are md's own allocation-light scanner
15
+ * utilities.
16
+ */
17
+ export { hashContent, fnv1a, FNV1A_OFFSET_BASIS, slugify } from '@jarenjs/core/string';
18
+ /**
19
+ * The `id` for one heading, unique within one emission.
20
+ *
21
+ * Both emitters mint ids, so the rule lives here once: slug the text,
22
+ * substitute `section` when nothing slug-worthy survives, number
23
+ * repeats the way GitHub numbers them (`setup`, `setup-1`, `setup-2`)
24
+ * and prefix the result. The COUNTER belongs to the caller — one map per
25
+ * emission, never shared with another numbering (a block key's hash and
26
+ * a slug share a namespace only by accident, and a collision there would
27
+ * shift an unrelated heading's number).
28
+ *
29
+ * @param {string} text the heading's plain text (`textOf`)
30
+ * @param {Map<string, number>} seen the emission's slug counter
31
+ * @param {string} prefix prepended to the result
32
+ * @returns {string}
33
+ */
34
+ export declare function headingId(text: string, seen: Map<string, number>, prefix: string): string;
35
+ /**
36
+ * The accessible name for a heading's permalink affordance — `#` alone
37
+ * names nothing, so the link says which section it points at.
38
+ * @param {string} text the heading's plain text (`textOf`)
39
+ * @returns {string}
40
+ */
41
+ export declare function permalinkLabel(text: string): string;
42
+ /**
43
+ * Count leading space characters (U+0020 only; the scanner expands no
44
+ * tabs here — callers pass detabbed text).
45
+ * @param {string} line
46
+ * @returns {number}
47
+ */
48
+ export declare function countIndent(line: string): number;
49
+ /**
50
+ * Is the line blank (empty or whitespace-only)?
51
+ * @param {string} line
52
+ * @returns {boolean}
53
+ */
54
+ export declare function isBlankLine(line: string): boolean;
55
+ /**
56
+ * Replace tabs with spaces to the next 4-column tab stop, counting
57
+ * columns from `startColumn`. Lines without tabs return the same
58
+ * string reference (the common case allocates nothing).
59
+ * @param {string} line
60
+ * @param {number} [startColumn]
61
+ * @returns {string}
62
+ */
63
+ export declare function expandTabs(line: string, startColumn?: number): string;
package/docs/LOADER.md ADDED
@@ -0,0 +1,92 @@
1
+ # The Jaren Markdown Loader
2
+
3
+ **Version 0.1 — Specification**
4
+
5
+ `loadMarkdown` turns any URL into a compiled Markdown document, with
6
+ caching, cancellation and streaming as first-class citizens.
7
+
8
+ ## 1. API
9
+
10
+ ```js
11
+ const compiled = await loadMarkdown(url, {
12
+ plugins: [], // compiled into the parser, §PLUGINS.md
13
+ base: undefined, // base URL for relative url arguments
14
+ signal: undefined, // AbortSignal — rejects with the abort reason
15
+ cache: defaultMdCache, // a cache instance, or false to bypass
16
+ fetch: globalThis.fetch,// injectable transport (tests, custom auth)
17
+ retainSource: true, // false drops the source string after parsing
18
+ toml: undefined, // injectable TOML frontmatter parser
19
+ });
20
+ ```
21
+
22
+ Accepted URL schemes are whatever the platform `fetch` accepts:
23
+ `http(s):`, `data:`, `blob:`, and relative paths resolved against
24
+ `options.base` (or as-is when already absolute). The resolved compiled
25
+ document is the same closure bundle `compileMarkdown` returns, with
26
+ `doc.meta.sourceUrl` set to the normalized URL.
27
+
28
+ ## 2. Caching
29
+
30
+ The shared in-memory cache is keyed by **normalized URL**. Each entry
31
+ remembers the response's `ETag` and `Last-Modified`; a cache hit
32
+ resolves immediately with the cached compiled document, and a
33
+ revalidating fetch (`If-None-Match`/`If-Modified-Since`) only replaces
34
+ the entry when the origin answers with new content (`200` with a
35
+ different validator). Entries also key on the compile-relevant options
36
+ (plugin names), so the same URL compiled with different plugin sets
37
+ does not alias.
38
+
39
+ - `createMdCache(limit = 64)` — an LRU cache instance; the default
40
+ shared instance is exported as `defaultMdCache`.
41
+ - `cache: false` — bypass entirely.
42
+ - `cache.delete(url)` / `cache.clear()` — manual invalidation.
43
+
44
+ Concurrent `loadMarkdown` calls for the same key share one in-flight
45
+ fetch (the promise itself is cached), so a burst of loads costs one
46
+ request.
47
+
48
+ ## 3. Cancellation
49
+
50
+ `options.signal` is passed through to `fetch` and checked between
51
+ parse steps; aborting rejects the promise with the signal's reason and
52
+ leaves the cache untouched (an aborted in-flight entry is evicted so
53
+ the next call retries).
54
+
55
+ ## 4. Streaming
56
+
57
+ When the response body is a `ReadableStream`, the loader parses
58
+ **block-by-block as chunks arrive** instead of buffering the full body:
59
+
60
+ ```js
61
+ for await (const block of streamMarkdown(url, options)) {
62
+ render(block); // top-level AST block nodes, in order
63
+ }
64
+ ```
65
+
66
+ `streamMarkdown(urlOrStream, options)` accepts a URL, a `Response`, a
67
+ `ReadableStream`, or any async iterable of string/Uint8Array chunks,
68
+ and yields completed top-level block nodes as soon as their end is
69
+ certain (a construct boundary that no later chunk can reopen: a blank
70
+ line at container depth zero outside an open fence). The generator's
71
+ **return value** is the finished `MdDocument`; `loadMarkdown` itself is
72
+ implemented over the same incremental core.
73
+
74
+ The incremental core is exported for non-URL sources:
75
+
76
+ ```js
77
+ const inc = createIncrementalParser(options);
78
+ inc.feed(chunk); // → MdNode[] — blocks completed by this chunk
79
+ inc.end(); // → MdDocument — flushes the tail
80
+ ```
81
+
82
+ Frontmatter is resolved as soon as its closing fence arrives, so
83
+ `inc.frontmatter` (and the first yielded value's document) is available
84
+ before the body finishes downloading.
85
+
86
+ ## 5. In an app document (non-normative)
87
+
88
+ `loadMarkdown` fits `@jarenjs/app`'s effect registry as an ordinary
89
+ async effect: an action dispatches `{ effect: 'md.load', with: url }`,
90
+ the effect resolves the compiled document and dispatches a completion
91
+ action whose payload is the plain `MdDocument` — from there the app's
92
+ JSLT view stylesheet takes over (see the README's end-to-end example).