@brett_lamy/docstream 1.3.0 → 1.4.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.
- package/README.md +25 -5
- package/dist/assets.d.ts +3 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +20 -0
- package/dist/assets.js.map +1 -0
- package/dist/demo/DemoViewer.d.ts +64 -0
- package/dist/demo/DemoViewer.d.ts.map +1 -0
- package/dist/demo/DemoViewer.js +426 -0
- package/dist/demo/DemoViewer.js.map +1 -0
- package/dist/demo/context.d.ts +14 -0
- package/dist/demo/context.d.ts.map +1 -0
- package/dist/demo/context.js +25 -0
- package/dist/demo/context.js.map +1 -0
- package/dist/demo/glob.d.ts +60 -0
- package/dist/demo/glob.d.ts.map +1 -0
- package/dist/demo/glob.js +158 -0
- package/dist/demo/glob.js.map +1 -0
- package/dist/demo/index.d.ts +10 -0
- package/dist/demo/index.d.ts.map +1 -0
- package/dist/demo/index.js +6 -0
- package/dist/demo/index.js.map +1 -0
- package/dist/demo/markdown.d.ts +60 -0
- package/dist/demo/markdown.d.ts.map +1 -0
- package/dist/demo/markdown.js +181 -0
- package/dist/demo/markdown.js.map +1 -0
- package/dist/demo/surface.d.ts +11 -0
- package/dist/demo/surface.d.ts.map +1 -0
- package/dist/demo/surface.js +69 -0
- package/dist/demo/surface.js.map +1 -0
- package/dist/demo/types.d.ts +91 -0
- package/dist/demo/types.d.ts.map +1 -0
- package/dist/demo/types.js +2 -0
- package/dist/demo/types.js.map +1 -0
- package/dist/docs/CollapsibleCode.d.ts +14 -0
- package/dist/docs/CollapsibleCode.d.ts.map +1 -0
- package/dist/docs/CollapsibleCode.js +19 -0
- package/dist/docs/CollapsibleCode.js.map +1 -0
- package/dist/docs/DocsRenderer.d.ts +49 -0
- package/dist/docs/DocsRenderer.d.ts.map +1 -0
- package/dist/docs/DocsRenderer.js +277 -0
- package/dist/docs/DocsRenderer.js.map +1 -0
- package/dist/docs/HighlightedCode.d.ts +6 -0
- package/dist/docs/HighlightedCode.d.ts.map +1 -0
- package/dist/docs/HighlightedCode.js +40 -0
- package/dist/docs/HighlightedCode.js.map +1 -0
- package/dist/docs/Mermaid.d.ts +7 -0
- package/dist/docs/Mermaid.d.ts.map +1 -0
- package/dist/docs/Mermaid.js +39 -0
- package/dist/docs/Mermaid.js.map +1 -0
- package/dist/docs/PageActions.d.ts +33 -0
- package/dist/docs/PageActions.d.ts.map +1 -0
- package/dist/docs/PageActions.js +115 -0
- package/dist/docs/PageActions.js.map +1 -0
- package/dist/docs/controls.d.ts +29 -0
- package/dist/docs/controls.d.ts.map +1 -0
- package/dist/docs/controls.js +46 -0
- package/dist/docs/controls.js.map +1 -0
- package/dist/docs/copy.d.ts +24 -0
- package/dist/docs/copy.d.ts.map +1 -0
- package/dist/docs/copy.js +62 -0
- package/dist/docs/copy.js.map +1 -0
- package/dist/docs/math.d.ts +3 -0
- package/dist/docs/math.d.ts.map +1 -0
- package/dist/docs/math.js +12 -0
- package/dist/docs/math.js.map +1 -0
- package/dist/docs/reference.d.ts +27 -0
- package/dist/docs/reference.d.ts.map +1 -0
- package/dist/docs/reference.js +73 -0
- package/dist/docs/reference.js.map +1 -0
- package/dist/docs/tabs-sync.d.ts +5 -0
- package/dist/docs/tabs-sync.d.ts.map +1 -0
- package/dist/docs/tabs-sync.js +58 -0
- package/dist/docs/tabs-sync.js.map +1 -0
- package/dist/gitbook/ast.d.ts +506 -0
- package/dist/gitbook/ast.d.ts.map +1 -0
- package/dist/gitbook/ast.js +8 -0
- package/dist/gitbook/ast.js.map +1 -0
- package/dist/gitbook/attrs.d.ts +14 -0
- package/dist/gitbook/attrs.d.ts.map +1 -0
- package/dist/gitbook/attrs.js +36 -0
- package/dist/gitbook/attrs.js.map +1 -0
- package/dist/gitbook/flatten.d.ts +14 -0
- package/dist/gitbook/flatten.d.ts.map +1 -0
- package/dist/gitbook/flatten.js +45 -0
- package/dist/gitbook/flatten.js.map +1 -0
- package/dist/gitbook/index.d.ts +10 -0
- package/dist/gitbook/index.d.ts.map +1 -0
- package/dist/gitbook/index.js +8 -0
- package/dist/gitbook/index.js.map +1 -0
- package/dist/gitbook/inline.d.ts +28 -0
- package/dist/gitbook/inline.d.ts.map +1 -0
- package/dist/gitbook/inline.js +956 -0
- package/dist/gitbook/inline.js.map +1 -0
- package/dist/gitbook/outline.d.ts +16 -0
- package/dist/gitbook/outline.d.ts.map +1 -0
- package/dist/gitbook/outline.js +44 -0
- package/dist/gitbook/outline.js.map +1 -0
- package/dist/gitbook/package-managers.d.ts +12 -0
- package/dist/gitbook/package-managers.d.ts.map +1 -0
- package/dist/gitbook/package-managers.js +93 -0
- package/dist/gitbook/package-managers.js.map +1 -0
- package/dist/gitbook/parse.d.ts +44 -0
- package/dist/gitbook/parse.d.ts.map +1 -0
- package/dist/gitbook/parse.js +1113 -0
- package/dist/gitbook/parse.js.map +1 -0
- package/dist/gitbook/serialize.d.ts +23 -0
- package/dist/gitbook/serialize.d.ts.map +1 -0
- package/dist/gitbook/serialize.js +498 -0
- package/dist/gitbook/serialize.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi/OpenApiOperation.d.ts +6 -0
- package/dist/openapi/OpenApiOperation.d.ts.map +1 -0
- package/dist/openapi/OpenApiOperation.js +112 -0
- package/dist/openapi/OpenApiOperation.js.map +1 -0
- package/dist/playground/InlineDemoPreview.d.ts +24 -0
- package/dist/playground/InlineDemoPreview.d.ts.map +1 -0
- package/dist/playground/InlineDemoPreview.js +48 -0
- package/dist/playground/InlineDemoPreview.js.map +1 -0
- package/dist/playground/PlaygroundStreamdown.d.ts +16 -0
- package/dist/playground/PlaygroundStreamdown.d.ts.map +1 -0
- package/dist/playground/PlaygroundStreamdown.js +22 -0
- package/dist/playground/PlaygroundStreamdown.js.map +1 -0
- package/dist/playground/ReactCodePreview.d.ts +64 -0
- package/dist/playground/ReactCodePreview.d.ts.map +1 -0
- package/dist/playground/ReactCodePreview.js +108 -0
- package/dist/playground/ReactCodePreview.js.map +1 -0
- package/dist/playground/filesystem.d.ts +27 -0
- package/dist/playground/filesystem.d.ts.map +1 -0
- package/dist/playground/filesystem.js +65 -0
- package/dist/playground/filesystem.js.map +1 -0
- package/dist/playground/index.d.ts +9 -0
- package/dist/playground/index.d.ts.map +1 -0
- package/dist/playground/index.js +7 -0
- package/dist/playground/index.js.map +1 -0
- package/dist/playground/project.d.ts +57 -0
- package/dist/playground/project.d.ts.map +1 -0
- package/dist/playground/project.js +137 -0
- package/dist/playground/project.js.map +1 -0
- package/dist/replay/NativeReplay.d.ts +9 -0
- package/dist/replay/NativeReplay.d.ts.map +1 -0
- package/dist/replay/NativeReplay.js +173 -0
- package/dist/replay/NativeReplay.js.map +1 -0
- package/dist/replay/ReplayPreview.d.ts +33 -0
- package/dist/replay/ReplayPreview.d.ts.map +1 -0
- package/dist/replay/ReplayPreview.js +91 -0
- package/dist/replay/ReplayPreview.js.map +1 -0
- package/dist/replay/index.d.ts +5 -0
- package/dist/replay/index.d.ts.map +1 -0
- package/dist/replay/index.js +4 -0
- package/dist/replay/index.js.map +1 -0
- package/dist/replay/sanitize.d.ts +4 -0
- package/dist/replay/sanitize.d.ts.map +1 -0
- package/dist/replay/sanitize.js +86 -0
- package/dist/replay/sanitize.js.map +1 -0
- package/dist/replay/url.d.ts +8 -0
- package/dist/replay/url.d.ts.map +1 -0
- package/dist/replay/url.js +41 -0
- package/dist/replay/url.js.map +1 -0
- package/dist/source/SourcePreview.d.ts +13 -0
- package/dist/source/SourcePreview.d.ts.map +1 -0
- package/dist/source/SourcePreview.js +47 -0
- package/dist/source/SourcePreview.js.map +1 -0
- package/dist/source/client.d.ts +7 -0
- package/dist/source/client.d.ts.map +1 -0
- package/dist/source/client.js +52 -0
- package/dist/source/client.js.map +1 -0
- package/dist/source/index.d.ts +6 -0
- package/dist/source/index.d.ts.map +1 -0
- package/dist/source/index.js +3 -0
- package/dist/source/index.js.map +1 -0
- package/dist/source/types.d.ts +33 -0
- package/dist/source/types.d.ts.map +1 -0
- package/dist/source/types.js +2 -0
- package/dist/source/types.js.map +1 -0
- package/dist/streamdown.d.ts +19 -0
- package/dist/streamdown.d.ts.map +1 -0
- package/dist/streamdown.js +15 -0
- package/dist/streamdown.js.map +1 -0
- package/dist/styles.css +2441 -0
- package/dist/video/VideoEmbed.d.ts +17 -0
- package/dist/video/VideoEmbed.d.ts.map +1 -0
- package/dist/video/VideoEmbed.js +12 -0
- package/dist/video/VideoEmbed.js.map +1 -0
- package/dist/video/index.d.ts +3 -0
- package/dist/video/index.d.ts.map +1 -0
- package/dist/video/index.js +2 -0
- package/dist/video/index.js.map +1 -0
- package/dist/vite/index.d.ts +21 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +137 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/viz/VizEmbed.d.ts +18 -0
- package/dist/viz/VizEmbed.d.ts.map +1 -0
- package/dist/viz/VizEmbed.js +12 -0
- package/dist/viz/VizEmbed.js.map +1 -0
- package/dist/viz/index.d.ts +4 -0
- package/dist/viz/index.d.ts.map +1 -0
- package/dist/viz/index.js +3 -0
- package/dist/viz/index.js.map +1 -0
- package/package.json +62 -32
- package/src/docs/Mermaid.tsx +5 -3
- package/src/replay/ReplayPreview.tsx +0 -1
- package/src/styles.css +91 -0
- package/src/replay/styles.css +0 -87
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
import { TAG_ATTRS, parseAttrs } from "./attrs.js";
|
|
2
|
+
import { containerTags, defaultDelimiterRow, definitionLine, delimiterAlign, fenceInfo, figureMarkdown, serializeBlocks } from "./serialize.js";
|
|
3
|
+
import { footnoteDefinitions, normalizeLabel, parseInline, plainText, refDefinitions } from "./inline.js";
|
|
4
|
+
// Minimal HTML-inline → markdown-inline bridge for HTML table cells.
|
|
5
|
+
function htmlToInlineMd(html) {
|
|
6
|
+
return html
|
|
7
|
+
.replace(/<br\s*\/?>/gi, " ")
|
|
8
|
+
.replace(/<strong>(.*?)<\/strong>/gis, "**$1**")
|
|
9
|
+
.replace(/<b>(.*?)<\/b>/gis, "**$1**")
|
|
10
|
+
.replace(/<em>(.*?)<\/em>/gis, "_$1_")
|
|
11
|
+
.replace(/<i>(.*?)<\/i>/gis, "_$1_")
|
|
12
|
+
.replace(/<code>(.*?)<\/code>/gis, "`$1`")
|
|
13
|
+
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gis, "[$2]($1)")
|
|
14
|
+
.replace(/<[^>]+>/g, "")
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
/** Stands in for a pipe written bare inside a table cell's code span, until the cell is parsed. */
|
|
18
|
+
const BARE_PIPE = "\uE000";
|
|
19
|
+
/** A table cell's inlines: pipes a code span wrote bare are remembered on the run (`barePipes`). */
|
|
20
|
+
function parseCell(cell) {
|
|
21
|
+
const nodes = parseInline(cell);
|
|
22
|
+
if (!cell.includes(BARE_PIPE))
|
|
23
|
+
return nodes;
|
|
24
|
+
for (const n of nodes) {
|
|
25
|
+
if (n.type !== "text" || !n.text.includes(BARE_PIPE))
|
|
26
|
+
continue;
|
|
27
|
+
if (n.code) {
|
|
28
|
+
const bare = [];
|
|
29
|
+
for (let k = 0; k < n.text.length; k++)
|
|
30
|
+
if (n.text[k] === BARE_PIPE)
|
|
31
|
+
bare.push(k);
|
|
32
|
+
n.barePipes = bare;
|
|
33
|
+
}
|
|
34
|
+
n.text = n.text.split(BARE_PIPE).join("|");
|
|
35
|
+
}
|
|
36
|
+
return nodes;
|
|
37
|
+
}
|
|
38
|
+
// Split a GFM pipe-table row into cell strings. Splits only on unescaped pipes
|
|
39
|
+
// that are outside inline-code spans, and unescapes "\|" back to a literal "|".
|
|
40
|
+
function splitTableRow(row) {
|
|
41
|
+
const trimmed = row.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
42
|
+
const cells = [];
|
|
43
|
+
let cur = "";
|
|
44
|
+
for (let k = 0; k < trimmed.length; k++) {
|
|
45
|
+
const ch = trimmed[k];
|
|
46
|
+
if (ch === "\\") {
|
|
47
|
+
// `\|` is a literal pipe (in code spans too); any other escape passes through for the inline parser.
|
|
48
|
+
cur += trimmed[k + 1] === "|" ? "|" : trimmed.slice(k, k + 2);
|
|
49
|
+
k++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === "`") {
|
|
53
|
+
// A code span (a backtick run closed by one of the same length) keeps its pipes; a lone run is literal.
|
|
54
|
+
let n = 1;
|
|
55
|
+
while (trimmed[k + n] === "`")
|
|
56
|
+
n++;
|
|
57
|
+
const fence = "`".repeat(n);
|
|
58
|
+
let end = trimmed.indexOf(fence, k + n);
|
|
59
|
+
while (end !== -1 && (trimmed[end + n] === "`" || trimmed[end - 1] === "`")) {
|
|
60
|
+
let e = end;
|
|
61
|
+
while (trimmed[e] === "`")
|
|
62
|
+
e++;
|
|
63
|
+
end = trimmed.indexOf(fence, e);
|
|
64
|
+
}
|
|
65
|
+
const stop = end === -1 ? k + n : end + n;
|
|
66
|
+
// `\|` is a pipe; a bare one is marked so the cell can remember it was written bare.
|
|
67
|
+
cur += trimmed.slice(k, stop).replace(/\\\||\|/g, (m) => (m === "|" ? BARE_PIPE : "|"));
|
|
68
|
+
k = stop - 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === "|") {
|
|
72
|
+
cells.push(cur);
|
|
73
|
+
cur = "";
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
cur += ch;
|
|
77
|
+
}
|
|
78
|
+
cells.push(cur);
|
|
79
|
+
return cells;
|
|
80
|
+
}
|
|
81
|
+
const TEMPLATE_RE = new RegExp(String.raw `^\s*\{%\s*(\S+?)(\s+${TAG_ATTRS})?\s*%\}\s*$`);
|
|
82
|
+
function booleanAttr(attrs, key) {
|
|
83
|
+
if (!(key in attrs))
|
|
84
|
+
return undefined;
|
|
85
|
+
return attrs[key] !== "false";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A fence's line numbers: `lineNumbers` / `showLineNumbers` when written (remembered as
|
|
89
|
+
* explicit so the attribute round-trips even when it equals the default), otherwise on for
|
|
90
|
+
* titled (file) fences and off for untitled ones.
|
|
91
|
+
*/
|
|
92
|
+
function lineNumbersFrom(attrs, titled) {
|
|
93
|
+
const written = booleanAttr(attrs, "lineNumbers") ?? booleanAttr(attrs, "showLineNumbers");
|
|
94
|
+
return written === undefined ? { lineNumbers: titled } : { lineNumbers: written, lineNumbersExplicit: true };
|
|
95
|
+
}
|
|
96
|
+
function positiveNumberAttr(attrs, key) {
|
|
97
|
+
if (!(key in attrs))
|
|
98
|
+
return undefined;
|
|
99
|
+
const value = Number(attrs[key]);
|
|
100
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
|
|
101
|
+
}
|
|
102
|
+
function templateTag(line) {
|
|
103
|
+
const m = line.match(TEMPLATE_RE);
|
|
104
|
+
if (!m)
|
|
105
|
+
return null;
|
|
106
|
+
return { name: m[1], attrs: parseAttrs(m[2]) };
|
|
107
|
+
}
|
|
108
|
+
/** A fence opener (```` ```ts title="x" ```` or `~~~`), or null. Backtick info strings cannot contain backticks. */
|
|
109
|
+
export function fenceOpener(line) {
|
|
110
|
+
const m = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
111
|
+
if (!m)
|
|
112
|
+
return null;
|
|
113
|
+
if (m[1][0] === "`" && m[2].includes("`"))
|
|
114
|
+
return null;
|
|
115
|
+
return { char: m[1][0], length: m[1].length };
|
|
116
|
+
}
|
|
117
|
+
/** True when `line` closes `fence`: the same character, at least as many, nothing else. */
|
|
118
|
+
export function closesFence(line, fence) {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
return trimmed.length >= fence.length && trimmed === fence.char.repeat(trimmed.length);
|
|
121
|
+
}
|
|
122
|
+
/** Tracks whether a line-by-line scan is inside a fenced code block. */
|
|
123
|
+
export function fenceTracker() {
|
|
124
|
+
let open = null;
|
|
125
|
+
return {
|
|
126
|
+
/** Feed the next line; returns true if the line is fence content or a fence delimiter. */
|
|
127
|
+
step(line) {
|
|
128
|
+
if (open) {
|
|
129
|
+
if (closesFence(line, open))
|
|
130
|
+
open = null;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
open = fenceOpener(line);
|
|
134
|
+
return open !== null;
|
|
135
|
+
},
|
|
136
|
+
get inside() {
|
|
137
|
+
return open !== null;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Collects the lines between an opening {% name %} and its matching
|
|
142
|
+
// {% endname %}, honoring nesting of the same tag. Tags inside fenced code
|
|
143
|
+
// (a demo file, a Markdown sample) are content, not structure.
|
|
144
|
+
function collectUntil(lines, start, name) {
|
|
145
|
+
const body = [];
|
|
146
|
+
let depth = 1;
|
|
147
|
+
let i = start;
|
|
148
|
+
const fences = fenceTracker();
|
|
149
|
+
for (; i < lines.length; i++) {
|
|
150
|
+
if (fences.step(lines[i])) {
|
|
151
|
+
body.push(lines[i]);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const tag = templateTag(lines[i]);
|
|
155
|
+
if (tag?.name === name)
|
|
156
|
+
depth++;
|
|
157
|
+
if (tag?.name === `end${name}`) {
|
|
158
|
+
depth--;
|
|
159
|
+
if (depth === 0)
|
|
160
|
+
return { body, next: i + 1, closing: lines[i] };
|
|
161
|
+
}
|
|
162
|
+
body.push(lines[i]);
|
|
163
|
+
}
|
|
164
|
+
return { body, next: i };
|
|
165
|
+
}
|
|
166
|
+
/** Records a container's opening / closing tag lines where they aren't what serialization writes. */
|
|
167
|
+
function tagged(node, opening, closing) {
|
|
168
|
+
const [open, close] = containerTags(node);
|
|
169
|
+
if (opening !== open)
|
|
170
|
+
node.opening = opening;
|
|
171
|
+
if ((closing ?? "") !== close)
|
|
172
|
+
node.closing = closing ?? "";
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
function collectHtmlUntil(lines, start, closeTag) {
|
|
176
|
+
const body = [];
|
|
177
|
+
let i = start;
|
|
178
|
+
for (; i < lines.length; i++) {
|
|
179
|
+
if (lines[i].includes(closeTag)) {
|
|
180
|
+
const before = lines[i].slice(0, lines[i].indexOf(closeTag));
|
|
181
|
+
if (before.trim())
|
|
182
|
+
body.push(before);
|
|
183
|
+
return { body, next: i + 1, ...(before.trim() ? {} : { closing: lines[i] }) };
|
|
184
|
+
}
|
|
185
|
+
body.push(lines[i]);
|
|
186
|
+
}
|
|
187
|
+
return { body, next: i };
|
|
188
|
+
}
|
|
189
|
+
const HINT_STYLES = ["info", "success", "warning", "danger"];
|
|
190
|
+
/** `` alone on its line. */
|
|
191
|
+
const SOLO_IMG_RE = /^!\[([^\]]*)\]\(([^)\s]+)(?:[ \t]+"([^"]*)")?\)$/;
|
|
192
|
+
/** Keeps the figure's source lines, unless they are exactly what serialization writes anyway. */
|
|
193
|
+
function withRaw(figure, raw) {
|
|
194
|
+
if (raw !== figureMarkdown(figure))
|
|
195
|
+
figure.raw = raw;
|
|
196
|
+
return figure;
|
|
197
|
+
}
|
|
198
|
+
/** `[^id]: url "Label"` — a footnote citation definition. */
|
|
199
|
+
const FOOTNOTE_DEF_RE = /^\[\^([^\]\s]+)\]:\s*(\S+)(?:\s+"([^"]*)")?\s*$/;
|
|
200
|
+
/** `[label]: url "title"` — a reference-style link definition (up to three spaces of indent). */
|
|
201
|
+
const REF_DEF_RE = /^ {0,3}\[((?:\\.|[^\]\\])+)\]:[ \t]*(\S+)(?:[ \t]+"((?:\\.|[^"\\])*)")?[ \t]*$/;
|
|
202
|
+
/** The canonical line for a footnote definition. */
|
|
203
|
+
export function footnoteLine(id, url, label) {
|
|
204
|
+
return `[^${id}]: ${url}${label ? ` "${label}"` : ""}`;
|
|
205
|
+
}
|
|
206
|
+
export function parseMarkdown(src) {
|
|
207
|
+
// Windows line endings throughout come back as such; mixed ones stay in the lines as written.
|
|
208
|
+
const crlf = src.includes("\r\n") && !/(^|[^\r])\n/.test(src);
|
|
209
|
+
const lines = src.split(crlf || !src.includes("\r\n") ? /\r?\n/ : "\n");
|
|
210
|
+
refDefinitions.clear();
|
|
211
|
+
footnoteDefinitions.clear();
|
|
212
|
+
const fences = fenceTracker();
|
|
213
|
+
// Canonical footnote definitions (outside fences), by line.
|
|
214
|
+
const canonicalFoot = [];
|
|
215
|
+
lines.forEach((line, k) => {
|
|
216
|
+
// Definition lines inside code fences are content, not definitions.
|
|
217
|
+
if (fences.step(line))
|
|
218
|
+
return;
|
|
219
|
+
// Footnote citation definitions first: [^id]: url "Optional Label"
|
|
220
|
+
const foot = line.match(FOOTNOTE_DEF_RE);
|
|
221
|
+
if (foot) {
|
|
222
|
+
footnoteDefinitions.set(foot[1], { url: foot[2], ...(foot[3] ? { label: foot[3] } : {}) });
|
|
223
|
+
canonicalFoot[k] = line === footnoteLine(foot[1], foot[2], foot[3]);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const def = line.match(REF_DEF_RE);
|
|
227
|
+
// Keys starting with ^ are reserved for footnotes and must never
|
|
228
|
+
// shadow reference-style link definitions. The first definition of a label wins.
|
|
229
|
+
if (def && !def[1].startsWith("^")) {
|
|
230
|
+
const key = normalizeLabel(def[1]);
|
|
231
|
+
if (!refDefinitions.has(key))
|
|
232
|
+
refDefinitions.set(key, def[2]);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// The footnote definitions ending the document are its doc-level `citations`, written back as a group;
|
|
236
|
+
// any others stay where they are, as definition blocks.
|
|
237
|
+
let last = lines.length - 1;
|
|
238
|
+
while (last >= 0 && !lines[last].trim())
|
|
239
|
+
last--;
|
|
240
|
+
let group = last + 1;
|
|
241
|
+
while (group > 0 && canonicalFoot[group - 1])
|
|
242
|
+
group--;
|
|
243
|
+
const trailing = group <= last;
|
|
244
|
+
const { blocks, end } = parseBody(trailing ? lines.slice(0, group) : lines);
|
|
245
|
+
const doc = { type: "doc", children: blocks };
|
|
246
|
+
if (footnoteDefinitions.size) {
|
|
247
|
+
doc.citations = [...footnoteDefinitions.entries()].map(([id, d]) => ({ id, ...d }));
|
|
248
|
+
}
|
|
249
|
+
if (trailing && end !== (blocks.length ? 1 : 0))
|
|
250
|
+
doc.citationsGap = end;
|
|
251
|
+
const ending = crlf ? src.match(/\s*$/)[0].replace(/\r\n/g, "\n") : src.match(/\s*$/)[0];
|
|
252
|
+
if (crlf)
|
|
253
|
+
doc.lineEnding = "\r\n";
|
|
254
|
+
// A missing final newline is added; anything else after the last line is kept.
|
|
255
|
+
if (ending !== "\n" && ending !== "")
|
|
256
|
+
doc.end = ending;
|
|
257
|
+
return doc;
|
|
258
|
+
}
|
|
259
|
+
const DEMO_LAYOUTS = ["auto", "single", "multi"];
|
|
260
|
+
const DEMO_VIEWPORTS = ["desktop", "tablet", "phone"];
|
|
261
|
+
/** `"full:Full,compact:Compact"` → `[{ id: "full", label: "Full" }, …]`. A bare id is its own label. */
|
|
262
|
+
export function parseDemoVariants(raw) {
|
|
263
|
+
return raw
|
|
264
|
+
.split(",")
|
|
265
|
+
.map((part) => part.trim())
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.map((part) => {
|
|
268
|
+
const colon = part.indexOf(":");
|
|
269
|
+
if (colon < 0)
|
|
270
|
+
return { id: part, label: part };
|
|
271
|
+
const id = part.slice(0, colon).trim();
|
|
272
|
+
const label = part.slice(colon + 1).trim();
|
|
273
|
+
return { id, label: label || id };
|
|
274
|
+
})
|
|
275
|
+
.filter((v) => v.id);
|
|
276
|
+
}
|
|
277
|
+
/** `{% command %}npm install x{% endcommand %}` on one line. */
|
|
278
|
+
const COMMAND_ONE_LINE_RE = new RegExp(String.raw `^\s*\{%\s*command(\s${TAG_ATTRS})?\s*%\}(.*?)\{%\s*endcommand\s*%\}\s*$`);
|
|
279
|
+
function commandNode(attrs, raw) {
|
|
280
|
+
const overrides = {};
|
|
281
|
+
for (const pm of ["pnpm", "yarn", "bun"])
|
|
282
|
+
if (attrs[pm])
|
|
283
|
+
overrides[pm] = attrs[pm];
|
|
284
|
+
return {
|
|
285
|
+
type: "command",
|
|
286
|
+
command: raw.replace(/^(?:[ \t]*\n)+/, "").replace(/(?:\n[ \t]*)+$/, "").trim(),
|
|
287
|
+
...(Object.keys(overrides).length ? { overrides } : {}),
|
|
288
|
+
...(attrs.sync && attrs.sync !== "pm" ? { sync: attrs.sync } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const ENDDEMO_RE = /^\s*\{%\s*enddemo\s*%\}\s*$/;
|
|
292
|
+
function fallbackPath(index, language) {
|
|
293
|
+
const ext = language || "txt";
|
|
294
|
+
return index === 0 ? `index.${ext}` : `file-${index + 1}.${ext}`;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The inline files of a `{% demo %}` block starting at `start` (the line after the tag).
|
|
298
|
+
*
|
|
299
|
+
* - `{% enddemo %}` right away: an empty block.
|
|
300
|
+
* - A fence right away: the block form. Fences (and blank lines) are files until
|
|
301
|
+
* `{% enddemo %}`; any other line ends the block too. Reaching the end of input
|
|
302
|
+
* first marks the block `open` (still streaming).
|
|
303
|
+
* - Blank lines, then fences: the block form only if `{% enddemo %}` closes it with
|
|
304
|
+
* nothing but fences in between — otherwise the tag is self-closing and the fences
|
|
305
|
+
* are ordinary code blocks, exactly as before 1.2.
|
|
306
|
+
* - Anything else: no body (self-closing tag).
|
|
307
|
+
*/
|
|
308
|
+
export function demoBody(lines, start, selfClosing = false) {
|
|
309
|
+
let j = start;
|
|
310
|
+
if (j < lines.length && ENDDEMO_RE.test(lines[j]))
|
|
311
|
+
return { files: [], next: j + 1, open: false };
|
|
312
|
+
// `{% demo … /%}` takes no files (a directly following `{% enddemo %}` is tolerated above).
|
|
313
|
+
if (selfClosing)
|
|
314
|
+
return null;
|
|
315
|
+
const immediate = j < lines.length && fenceOpener(lines[j]) !== null;
|
|
316
|
+
if (!immediate) {
|
|
317
|
+
while (j < lines.length && !lines[j].trim())
|
|
318
|
+
j++;
|
|
319
|
+
if (j >= lines.length)
|
|
320
|
+
return null;
|
|
321
|
+
if (ENDDEMO_RE.test(lines[j]))
|
|
322
|
+
return { files: [], next: j + 1, open: false };
|
|
323
|
+
if (!fenceOpener(lines[j]))
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const files = [];
|
|
327
|
+
while (j < lines.length) {
|
|
328
|
+
const line = lines[j];
|
|
329
|
+
if (!line.trim()) {
|
|
330
|
+
j++;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (ENDDEMO_RE.test(line))
|
|
334
|
+
return { files, next: j + 1, open: false };
|
|
335
|
+
const fence = fenceOpener(line);
|
|
336
|
+
if (!fence)
|
|
337
|
+
return immediate ? { files, next: j, open: false } : null;
|
|
338
|
+
const info = line.trim().slice(fence.length).trim();
|
|
339
|
+
// `tsx title="index.tsx"` or just `title="index.tsx"` (no language).
|
|
340
|
+
const first = info.match(/^[^\s`]*/)?.[0] ?? "";
|
|
341
|
+
const language = first.includes("=") ? "" : first;
|
|
342
|
+
const rest = language ? info.slice(language.length) : info;
|
|
343
|
+
const attrs = parseAttrs(rest);
|
|
344
|
+
const content = [];
|
|
345
|
+
j++;
|
|
346
|
+
let closed = false;
|
|
347
|
+
while (j < lines.length) {
|
|
348
|
+
if (closesFence(lines[j], fence)) {
|
|
349
|
+
closed = true;
|
|
350
|
+
j++;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
content.push(lines[j]);
|
|
354
|
+
j++;
|
|
355
|
+
}
|
|
356
|
+
files.push({
|
|
357
|
+
path: (attrs.title ?? "").trim() || fallbackPath(files.length, language || undefined),
|
|
358
|
+
content: content.join("\n"),
|
|
359
|
+
...(language ? { language } : {}),
|
|
360
|
+
});
|
|
361
|
+
if (!closed)
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
return immediate ? { files, next: j, open: true } : null;
|
|
365
|
+
}
|
|
366
|
+
function parseDemoTag(attrs) {
|
|
367
|
+
const variants = attrs.variants ? parseDemoVariants(attrs.variants) : [];
|
|
368
|
+
return {
|
|
369
|
+
type: "demo",
|
|
370
|
+
src: (attrs.src ?? "").trim(),
|
|
371
|
+
...(attrs.title ? { title: attrs.title } : {}),
|
|
372
|
+
...(attrs.description ? { description: attrs.description } : {}),
|
|
373
|
+
...(attrs.height ? { height: attrs.height } : {}),
|
|
374
|
+
...(DEMO_LAYOUTS.includes(attrs.layout) ? { layout: attrs.layout } : {}),
|
|
375
|
+
...(variants.length ? { variants } : {}),
|
|
376
|
+
...(DEMO_VIEWPORTS.includes(attrs.viewport) ? { viewport: attrs.viewport } : {}),
|
|
377
|
+
...(attrs.entry ? { entry: attrs.entry } : {}),
|
|
378
|
+
...(attrs.status ? { status: attrs.status } : {}),
|
|
379
|
+
...(booleanAttr(attrs, "bleed") === undefined ? {} : { bleed: booleanAttr(attrs, "bleed") }),
|
|
380
|
+
...(attrs.className ? { className: attrs.className } : {}),
|
|
381
|
+
...(attrs.surface?.trim() ? { surface: attrs.surface } : {}),
|
|
382
|
+
...(attrs.variantsWidth ? { variantsWidth: attrs.variantsWidth } : {}),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Strips an in-progress inline token (`[^1`, `@bre`, a lone `#`, a trailing
|
|
386
|
+
// `\`) from the end of a partially streamed document so chips don't flash as
|
|
387
|
+
// plain text mid-stream. No-op inside an unterminated code fence.
|
|
388
|
+
export function trimPartialInlineToken(md) {
|
|
389
|
+
const fences = md.match(/^\s*(?:```|~~~)/gm);
|
|
390
|
+
if (fences && fences.length % 2 === 1)
|
|
391
|
+
return md;
|
|
392
|
+
return md
|
|
393
|
+
// A block tag still arriving (`{% demo src="butt`) would flash as a paragraph.
|
|
394
|
+
.replace(/(^|\n)[ \t]*\{(?:%(?:(?!%\})[^\n])*)?$/, "$1")
|
|
395
|
+
// A one-line command still arriving (`{% command %}npm i x{% endcomm`): hold the line back
|
|
396
|
+
// until it closes. (The multi-line form, `{% command %}` alone on its line, renders as it grows.)
|
|
397
|
+
.replace(/(^|\n)[ \t]*\{%\s*command\b[^\n]*?%\}[ \t]*\S[^\n]*$/, (line, lead) => /\{%\s*endcommand\s*%\}\s*$/.test(line) ? line : lead)
|
|
398
|
+
// A fence still arriving (a lone ` or ``) would end a demo block and flash as text.
|
|
399
|
+
.replace(/(^|\n)[ \t]*(?:`{1,2}|~{1,2})$/, "$1")
|
|
400
|
+
.replace(/\[\^[^\]]*$/, "")
|
|
401
|
+
.replace(/(^|[\s([{])[@#][\w.-]*$/, "$1")
|
|
402
|
+
.replace(/\\$/, "");
|
|
403
|
+
}
|
|
404
|
+
export function parseBlocks(lines) {
|
|
405
|
+
return parseBody(lines).blocks;
|
|
406
|
+
}
|
|
407
|
+
/** Records `gap` on a block (or item) when it differs from the default. */
|
|
408
|
+
function spaced(node, gap, fallback) {
|
|
409
|
+
if (gap !== fallback)
|
|
410
|
+
node.gap = gap;
|
|
411
|
+
return node;
|
|
412
|
+
}
|
|
413
|
+
/** Records `gapEnd` on a container when it differs from the default. */
|
|
414
|
+
function ended(node, end, fallback = 0) {
|
|
415
|
+
if (end !== fallback)
|
|
416
|
+
node.gapEnd = end;
|
|
417
|
+
return node;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Blocks of `lines`, with the blank lines before each (`gap`, where not the default: `firstGap` before the
|
|
421
|
+
* first, 1 between siblings) and the number of blank lines after the last (`end`).
|
|
422
|
+
*/
|
|
423
|
+
function parseBody(lines, firstGap = 0) {
|
|
424
|
+
const blocks = [];
|
|
425
|
+
let i = 0;
|
|
426
|
+
// Blank lines since the last block, and before the block starting on the current line.
|
|
427
|
+
let blank = 0;
|
|
428
|
+
let gapHere = 0;
|
|
429
|
+
let paragraphGap = 0;
|
|
430
|
+
// The blank lines themselves, for whitespace-only ones.
|
|
431
|
+
let blankLines = [];
|
|
432
|
+
let gapLines = [];
|
|
433
|
+
let paragraphLines = [];
|
|
434
|
+
const add = (b, gap = gapHere, written = gapLines) => {
|
|
435
|
+
if (written.some((l) => l))
|
|
436
|
+
b.blanks = [...written];
|
|
437
|
+
blocks.push(spaced(b, gap, blocks.length ? 1 : firstGap));
|
|
438
|
+
};
|
|
439
|
+
const container = (node, body, build, first = 0, last = 0) => {
|
|
440
|
+
const parsed = parseBody(body, first);
|
|
441
|
+
build(parsed.blocks);
|
|
442
|
+
add(ended(node, parsed.end, last));
|
|
443
|
+
};
|
|
444
|
+
/** Keeps the last block's source lines as `raw` when they aren't what serialization writes. */
|
|
445
|
+
const keepRaw = (from, to) => {
|
|
446
|
+
const b = blocks[blocks.length - 1];
|
|
447
|
+
const written = lines.slice(from, Math.min(to, lines.length)).join("\n");
|
|
448
|
+
if (written !== serializeBlocks([{ ...b, gap: 0, blanks: undefined }]))
|
|
449
|
+
b.raw = written;
|
|
450
|
+
};
|
|
451
|
+
const paragraph = [];
|
|
452
|
+
let paragraphIndent = "";
|
|
453
|
+
const flushParagraph = () => {
|
|
454
|
+
if (paragraph.length) {
|
|
455
|
+
// Soft line breaks stay newlines and trailing spaces stay, so wrapped paragraphs come back as written.
|
|
456
|
+
const text = paragraph.join("\n");
|
|
457
|
+
if (text.trim()) {
|
|
458
|
+
const node = { type: "paragraph", children: parseInline(text) };
|
|
459
|
+
if (paragraphIndent)
|
|
460
|
+
node.indent = paragraphIndent;
|
|
461
|
+
add(node, paragraphGap, paragraphLines);
|
|
462
|
+
}
|
|
463
|
+
paragraph.length = 0;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
while (i < lines.length) {
|
|
467
|
+
const line = lines[i];
|
|
468
|
+
const trimmed = line.trim();
|
|
469
|
+
if (!trimmed) {
|
|
470
|
+
flushParagraph();
|
|
471
|
+
blank++;
|
|
472
|
+
blankLines.push(line);
|
|
473
|
+
i++;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
gapHere = blank;
|
|
477
|
+
gapLines = blankLines;
|
|
478
|
+
blank = 0;
|
|
479
|
+
blankLines = [];
|
|
480
|
+
const oneLine = line.match(COMMAND_ONE_LINE_RE);
|
|
481
|
+
if (oneLine) {
|
|
482
|
+
flushParagraph();
|
|
483
|
+
add(commandNode(parseAttrs(oneLine[1]), oneLine[2]));
|
|
484
|
+
keepRaw(i, i + 1);
|
|
485
|
+
i++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const tag = templateTag(line);
|
|
489
|
+
if (tag) {
|
|
490
|
+
flushParagraph();
|
|
491
|
+
if (tag.name === "hint") {
|
|
492
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "hint");
|
|
493
|
+
const style = HINT_STYLES.includes(tag.attrs.style)
|
|
494
|
+
? tag.attrs.style
|
|
495
|
+
: "info";
|
|
496
|
+
const hint = { type: "hint", style, children: [] };
|
|
497
|
+
container(tagged(hint, line, closing), body, (children) => (hint.children = children));
|
|
498
|
+
i = next;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (tag.name === "tabs") {
|
|
502
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "tabs");
|
|
503
|
+
const level = Number(tag.attrs.level);
|
|
504
|
+
const tabs = parseItems(body, "tab", (t, lines) => {
|
|
505
|
+
const parsed = parseBody(lines);
|
|
506
|
+
return ended({ type: "tab", title: t.attrs.title ?? "Tab", children: parsed.blocks }, parsed.end);
|
|
507
|
+
});
|
|
508
|
+
const tabsNode = {
|
|
509
|
+
type: "tabs",
|
|
510
|
+
tabs: tabs.items,
|
|
511
|
+
...(tag.attrs.sync ? { sync: tag.attrs.sync } : {}),
|
|
512
|
+
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
513
|
+
...(tag.attrs.title && (level === 3 || level === 4) ? { level: level } : {}),
|
|
514
|
+
};
|
|
515
|
+
add(tagged(ended(tabsNode, tabs.end), line, closing));
|
|
516
|
+
i = next;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (tag.name === "command") {
|
|
520
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "command");
|
|
521
|
+
add(commandNode(tag.attrs, body.join("\n")));
|
|
522
|
+
// (A block still streaming in — no end tag yet — isn't kept as written.)
|
|
523
|
+
if (closing !== undefined)
|
|
524
|
+
keepRaw(i, next);
|
|
525
|
+
i = next;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (tag.name === "stepper") {
|
|
529
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "stepper");
|
|
530
|
+
const steps = parseSteps(body);
|
|
531
|
+
add(tagged(ended({ type: "stepper", steps: steps.items }, steps.end), line, closing));
|
|
532
|
+
i = next;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (tag.name === "columns") {
|
|
536
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "columns");
|
|
537
|
+
const columns = parseItems(body, "column", (_, lines) => {
|
|
538
|
+
const parsed = parseBody(lines);
|
|
539
|
+
return ended({ type: "column", children: parsed.blocks }, parsed.end);
|
|
540
|
+
});
|
|
541
|
+
add(tagged(ended({ type: "columns", columns: columns.items }, columns.end), line, closing));
|
|
542
|
+
i = next;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (tag.name === "code") {
|
|
546
|
+
const { body, next } = collectUntil(lines, i + 1, "code");
|
|
547
|
+
const inner = parseBlocks(body);
|
|
548
|
+
const code = inner.find((b) => b.type === "code");
|
|
549
|
+
if (code && code.type === "code") {
|
|
550
|
+
code.title = tag.attrs.title ?? null;
|
|
551
|
+
Object.assign(code, lineNumbersFrom(tag.attrs, code.title !== null));
|
|
552
|
+
code.live = tag.attrs.live === "true";
|
|
553
|
+
code.entry = tag.attrs.entry ?? null;
|
|
554
|
+
delete code.gap;
|
|
555
|
+
code.raw = lines.slice(i, next).join("\n");
|
|
556
|
+
add(code);
|
|
557
|
+
}
|
|
558
|
+
i = next;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (tag.name === "embed") {
|
|
562
|
+
add({
|
|
563
|
+
type: "embed",
|
|
564
|
+
url: tag.attrs.url ?? "",
|
|
565
|
+
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
566
|
+
...(tag.attrs.poster ? { poster: tag.attrs.poster } : {}),
|
|
567
|
+
...(booleanAttr(tag.attrs, "autoplay") === undefined
|
|
568
|
+
? {}
|
|
569
|
+
: { autoplay: booleanAttr(tag.attrs, "autoplay") }),
|
|
570
|
+
...(booleanAttr(tag.attrs, "loop") === undefined
|
|
571
|
+
? {}
|
|
572
|
+
: { loop: booleanAttr(tag.attrs, "loop") }),
|
|
573
|
+
...(booleanAttr(tag.attrs, "muted") === undefined
|
|
574
|
+
? {}
|
|
575
|
+
: { muted: booleanAttr(tag.attrs, "muted") }),
|
|
576
|
+
...(booleanAttr(tag.attrs, "controls") === undefined
|
|
577
|
+
? {}
|
|
578
|
+
: { controls: booleanAttr(tag.attrs, "controls") }),
|
|
579
|
+
});
|
|
580
|
+
i++;
|
|
581
|
+
// tolerate optional {% endembed %}
|
|
582
|
+
if (i < lines.length && templateTag(lines[i])?.name === "endembed") {
|
|
583
|
+
;
|
|
584
|
+
blocks[blocks.length - 1].raw = lines.slice(i - 1, i + 1).join("\n");
|
|
585
|
+
i++;
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (tag.name === "content-ref") {
|
|
590
|
+
const { body, next } = collectUntil(lines, i + 1, "content-ref");
|
|
591
|
+
const inner = parseInline(body.join(" ").trim());
|
|
592
|
+
add({ type: "content-ref", url: tag.attrs.url ?? "", children: inner });
|
|
593
|
+
keepRaw(i, next);
|
|
594
|
+
i = next;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (tag.name === "source-ref" || tag.name === "component" || tag.name === "story") {
|
|
598
|
+
const kind = tag.name === "story" || tag.attrs.kind === "story" ? "story" : "component";
|
|
599
|
+
add({
|
|
600
|
+
type: "source-ref",
|
|
601
|
+
mount: tag.attrs.mount ?? "source",
|
|
602
|
+
path: tag.attrs.path ?? tag.attrs.src ?? "",
|
|
603
|
+
exportName: tag.attrs.export ?? (kind === "story" ? "Primary" : "default"),
|
|
604
|
+
kind,
|
|
605
|
+
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
606
|
+
});
|
|
607
|
+
const from = i;
|
|
608
|
+
i++;
|
|
609
|
+
if (i < lines.length && templateTag(lines[i])?.name === `end${tag.name}`)
|
|
610
|
+
i++;
|
|
611
|
+
const source = blocks[blocks.length - 1];
|
|
612
|
+
const written = lines.slice(from, i).join("\n");
|
|
613
|
+
if (written !== serializeBlocks([{ ...source, gap: 0 }]))
|
|
614
|
+
source.raw = written;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (tag.name === "demo") {
|
|
618
|
+
const node = parseDemoTag(tag.attrs);
|
|
619
|
+
const demoStart = i;
|
|
620
|
+
i++;
|
|
621
|
+
// `{% demo … /%}` never has a body; otherwise look for inline files / {% enddemo %}.
|
|
622
|
+
const body = demoBody(lines, i, /\/\s*%\}\s*$/.test(line));
|
|
623
|
+
if (body) {
|
|
624
|
+
if (body.files.length)
|
|
625
|
+
node.files = body.files;
|
|
626
|
+
if (body.open)
|
|
627
|
+
node.open = true;
|
|
628
|
+
i = body.next;
|
|
629
|
+
}
|
|
630
|
+
add(node);
|
|
631
|
+
keepRaw(demoStart, i);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (tag.name === "updates") {
|
|
635
|
+
const { body, next, closing } = collectUntil(lines, i + 1, "updates");
|
|
636
|
+
const updates = parseItems(body, "update", (t, lines) => {
|
|
637
|
+
const parsed = parseBody(lines);
|
|
638
|
+
return ended({ type: "update", date: t.attrs.date ?? "", children: parsed.blocks }, parsed.end);
|
|
639
|
+
});
|
|
640
|
+
const updatesNode = { type: "updates", format: tag.attrs.format ?? null, updates: updates.items };
|
|
641
|
+
add(tagged(ended(updatesNode, updates.end), line, closing));
|
|
642
|
+
i = next;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (tag.name === "openapi-operation" || tag.name === "openapi") {
|
|
646
|
+
const { body, next } = collectUntil(lines, i + 1, tag.name);
|
|
647
|
+
const link = body.join(" ").match(/\[([^\]]*)\]\(([^)\s]+)\)/);
|
|
648
|
+
add({
|
|
649
|
+
type: "openapi-operation",
|
|
650
|
+
spec: tag.attrs.spec ?? "",
|
|
651
|
+
path: tag.attrs.path ?? "",
|
|
652
|
+
method: tag.attrs.method ?? "",
|
|
653
|
+
specUrl: link?.[2] ?? "",
|
|
654
|
+
label: link?.[1] ?? "",
|
|
655
|
+
});
|
|
656
|
+
keepRaw(i, next);
|
|
657
|
+
i = next;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (tag.name === "file") {
|
|
661
|
+
// Render file blocks as content-refs for now — same shape, different chrome.
|
|
662
|
+
add({ type: "content-ref", url: tag.attrs.src ?? "", children: parseInline(tag.attrs.caption ?? tag.attrs.src ?? ""), raw: line });
|
|
663
|
+
i++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
// Unknown template tag: kept verbatim (renderers show nothing for it).
|
|
667
|
+
add({ type: "raw", markdown: line });
|
|
668
|
+
i++;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
// <details><summary>…</summary> … </details> (GitBook expandable)
|
|
672
|
+
if (/^<details>/i.test(trimmed)) {
|
|
673
|
+
flushParagraph();
|
|
674
|
+
let summary = "";
|
|
675
|
+
let bodyStart = i + 1;
|
|
676
|
+
const sameLine = trimmed.match(/<summary>(.*?)<\/summary>/i);
|
|
677
|
+
if (sameLine) {
|
|
678
|
+
summary = sameLine[1];
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
let j = i + 1;
|
|
682
|
+
while (j < lines.length && !lines[j].trim())
|
|
683
|
+
j++;
|
|
684
|
+
const m = lines[j]?.trim().match(/^<summary>(.*?)<\/summary>$/i);
|
|
685
|
+
if (m) {
|
|
686
|
+
summary = m[1];
|
|
687
|
+
bodyStart = j + 1;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
const { body, next, closing } = collectHtmlUntil(lines, bodyStart, "</details>");
|
|
691
|
+
const expandable = { type: "expandable", summary, children: [] };
|
|
692
|
+
if (closing !== undefined && closing !== "</details>")
|
|
693
|
+
expandable.closing = closing;
|
|
694
|
+
const opening = lines.slice(i, bodyStart).join("\n");
|
|
695
|
+
if (opening !== `<details>\n\n<summary>${summary}</summary>`)
|
|
696
|
+
expandable.opening = opening;
|
|
697
|
+
container(expandable, body, (children) => (expandable.children = children), 1, 1);
|
|
698
|
+
i = next;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
// <table …>…</table> — GitBook exports complex/cards tables as HTML
|
|
702
|
+
if (/^<table[\s>]/i.test(trimmed)) {
|
|
703
|
+
flushParagraph();
|
|
704
|
+
// Up to the matching </table>: nested tables count, fenced code inside cells doesn't.
|
|
705
|
+
let j = i;
|
|
706
|
+
let depth = 0;
|
|
707
|
+
const tableFences = fenceTracker();
|
|
708
|
+
for (; j < lines.length; j++) {
|
|
709
|
+
if (j > i && tableFences.step(lines[j]))
|
|
710
|
+
continue;
|
|
711
|
+
depth += (lines[j].match(/<table[\s>]/gi) ?? []).length - (lines[j].match(/<\/table>/gi) ?? []).length;
|
|
712
|
+
if (depth <= 0)
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
j = Math.min(j, lines.length - 1);
|
|
716
|
+
const chunk = lines.slice(i, j + 1).join("\n");
|
|
717
|
+
const tableStart = blocks.length;
|
|
718
|
+
const view = chunk.match(/<table[^>]*data-view="([^"]*)"/i)?.[1];
|
|
719
|
+
const rowsHtml = [...chunk.matchAll(/<tr[^>]*>(.*?)<\/tr>/gis)].map((m) => m[1]);
|
|
720
|
+
const parseCells = (rowHtml) => [...rowHtml.matchAll(/<t[dh][^>]*>(.*?)<\/t[dh]>/gis)].map((m) => parseInline(htmlToInlineMd(m[1])));
|
|
721
|
+
const allRows = rowsHtml.map(parseCells);
|
|
722
|
+
const [header, ...rest] = allRows.length ? allRows : [[]];
|
|
723
|
+
add({
|
|
724
|
+
type: "table",
|
|
725
|
+
header: header ?? [],
|
|
726
|
+
rows: rest,
|
|
727
|
+
...(view ? { view } : { html: true }),
|
|
728
|
+
});
|
|
729
|
+
const htmlTable = blocks[tableStart];
|
|
730
|
+
if (chunk !== serializeBlocks([{ ...htmlTable, gap: 0 }]))
|
|
731
|
+
htmlTable.raw = chunk;
|
|
732
|
+
i = j + 1;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
// standalone <img …> line (e.g. GitBook drawings)
|
|
736
|
+
const soloHtmlImg = trimmed.match(/^<img[^>]*src="([^"]*)"[^>]*>$/i);
|
|
737
|
+
if (soloHtmlImg) {
|
|
738
|
+
flushParagraph();
|
|
739
|
+
const alt = trimmed.match(/alt="([^"]*)"/i)?.[1] ?? "";
|
|
740
|
+
add(withRaw({ type: "figure", src: soloHtmlImg[1], alt, caption: "" }, line));
|
|
741
|
+
i++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
// <figure><img …><figcaption>…</figcaption></figure>
|
|
745
|
+
if (/^<figure>/i.test(trimmed)) {
|
|
746
|
+
flushParagraph();
|
|
747
|
+
const chunkLines = [line];
|
|
748
|
+
let j = i;
|
|
749
|
+
while (!chunkLines.join("\n").includes("</figure>") && j + 1 < lines.length) {
|
|
750
|
+
j++;
|
|
751
|
+
chunkLines.push(lines[j]);
|
|
752
|
+
}
|
|
753
|
+
const chunk = chunkLines.join("\n");
|
|
754
|
+
const img = chunk.match(/<img[^>]*src="([^"]*)"[^>]*>/i);
|
|
755
|
+
const alt = chunk.match(/<img[^>]*alt="([^"]*)"[^>]*>/i);
|
|
756
|
+
const cap = chunk.match(/<figcaption>(.*?)<\/figcaption>/is);
|
|
757
|
+
const figure = {
|
|
758
|
+
type: "figure",
|
|
759
|
+
src: img?.[1] ?? "",
|
|
760
|
+
alt: alt?.[1] ?? "",
|
|
761
|
+
caption: (cap?.[1] ?? "").replace(/<\/?p>/g, "").trim(),
|
|
762
|
+
};
|
|
763
|
+
add(withRaw(figure, chunk));
|
|
764
|
+
i = j + 1;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// plain markdown image on its own line
|
|
768
|
+
const soloImg = trimmed.match(SOLO_IMG_RE);
|
|
769
|
+
if (soloImg) {
|
|
770
|
+
flushParagraph();
|
|
771
|
+
const figure = { type: "figure", src: soloImg[2], alt: soloImg[1], caption: "" };
|
|
772
|
+
if (soloImg[3] !== undefined)
|
|
773
|
+
figure.title = soloImg[3];
|
|
774
|
+
add(withRaw(figure, line));
|
|
775
|
+
i++;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
// $$ math $$
|
|
779
|
+
if (trimmed === "$$") {
|
|
780
|
+
flushParagraph();
|
|
781
|
+
const formula = [];
|
|
782
|
+
const mathStart = i;
|
|
783
|
+
i++;
|
|
784
|
+
while (i < lines.length && lines[i].trim() !== "$$") {
|
|
785
|
+
formula.push(lines[i]);
|
|
786
|
+
i++;
|
|
787
|
+
}
|
|
788
|
+
i++;
|
|
789
|
+
add({ type: "math", formula: formula.join("\n") });
|
|
790
|
+
keepRaw(mathStart, i);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
// fenced code (``` or ~~~)
|
|
794
|
+
const fence = trimmed.match(/^(`{3,}|~{3,})([^\s`]*)?(?:\s+(.*?))?\s*$/);
|
|
795
|
+
if (fence) {
|
|
796
|
+
flushParagraph();
|
|
797
|
+
// ```` ```title="x" ```` (attributes, no language): the first word is not a language.
|
|
798
|
+
const bareAttrs = !!fence[2]?.includes("=");
|
|
799
|
+
const attrs = parseAttrs(bareAttrs ? [fence[2], fence[3]].filter(Boolean).join(" ") : fence[3]);
|
|
800
|
+
const code = [];
|
|
801
|
+
i++;
|
|
802
|
+
while (i < lines.length && !lines[i].trim().startsWith(fence[1])) {
|
|
803
|
+
code.push(lines[i]);
|
|
804
|
+
i++;
|
|
805
|
+
}
|
|
806
|
+
const closingLine = i < lines.length ? lines[i] : "";
|
|
807
|
+
i++;
|
|
808
|
+
const layout = {};
|
|
809
|
+
const lead = line.match(/^\s*/)[0];
|
|
810
|
+
if (lead + fence[1] !== "```")
|
|
811
|
+
layout.fence = lead + fence[1];
|
|
812
|
+
if (closingLine !== fence[1])
|
|
813
|
+
layout.closingFence = closingLine;
|
|
814
|
+
const infoRaw = line.trimStart().slice(fence[1].length);
|
|
815
|
+
const codeNode = {
|
|
816
|
+
...layout,
|
|
817
|
+
type: "code",
|
|
818
|
+
language: (!bareAttrs && fence[2]) || null,
|
|
819
|
+
title: attrs.title ?? null,
|
|
820
|
+
...lineNumbersFrom(attrs, attrs.title !== undefined),
|
|
821
|
+
code: code.join("\n"),
|
|
822
|
+
...(booleanAttr(attrs, "live") === undefined ? {} : { live: booleanAttr(attrs, "live") }),
|
|
823
|
+
...(attrs.entry ? { entry: attrs.entry } : {}),
|
|
824
|
+
...(positiveNumberAttr(attrs, "collapsedCodeLines") === undefined
|
|
825
|
+
? {}
|
|
826
|
+
: { collapsedCodeLines: positiveNumberAttr(attrs, "collapsedCodeLines") }),
|
|
827
|
+
...(positiveNumberAttr(attrs, "expandedCodeLines") === undefined
|
|
828
|
+
? {}
|
|
829
|
+
: { expandedCodeLines: positiveNumberAttr(attrs, "expandedCodeLines") }),
|
|
830
|
+
};
|
|
831
|
+
// The info string as written, where it isn't what serialization writes (spacing, attribute order).
|
|
832
|
+
if (infoRaw !== fenceInfo(codeNode))
|
|
833
|
+
codeNode.info = infoRaw;
|
|
834
|
+
add(codeNode);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
// indented code block (4+ spaces, GFM)
|
|
838
|
+
if (paragraph.length === 0 && /^ {4,}\S/.test(line) && !line.trim().match(/^([-*+]|\d+[.)])\s/)) {
|
|
839
|
+
const code = [];
|
|
840
|
+
const from = i;
|
|
841
|
+
while (i < lines.length &&
|
|
842
|
+
(/^ {4,}\S/.test(lines[i]) || (!lines[i].trim() && /^ {4,}\S/.test(lines[i + 1] ?? "")))) {
|
|
843
|
+
code.push(lines[i].slice(4));
|
|
844
|
+
i++;
|
|
845
|
+
}
|
|
846
|
+
const indentedCode = { type: "code", language: null, title: null, lineNumbers: false, code: code.join("\n"), indented: true };
|
|
847
|
+
// Whitespace-only lines inside keep their spaces.
|
|
848
|
+
const written = lines.slice(from, i).join("\n");
|
|
849
|
+
if (written !== code.map((l) => (l ? ` ${l}` : l)).join("\n"))
|
|
850
|
+
indentedCode.raw = written;
|
|
851
|
+
add(indentedCode);
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
// setext headings: a paragraph line followed by ==== (h1) or ---- (h2)
|
|
855
|
+
if (paragraph.length && /^=+$/.test(trimmed)) {
|
|
856
|
+
const text = paragraph.join("\n").trim();
|
|
857
|
+
paragraph.length = 0;
|
|
858
|
+
add({ type: "heading", level: 1, children: parseInline(text), setext: trimmed }, paragraphGap, paragraphLines);
|
|
859
|
+
i++;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (paragraph.length && /^-+$/.test(trimmed)) {
|
|
863
|
+
const text = paragraph.join("\n").trim();
|
|
864
|
+
paragraph.length = 0;
|
|
865
|
+
add({ type: "heading", level: 2, children: parseInline(text), setext: trimmed }, paragraphGap, paragraphLines);
|
|
866
|
+
i++;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
// heading (GitBook exports can contain empty headings like a bare "##")
|
|
870
|
+
// Trailing spaces stay in the text, so they come back.
|
|
871
|
+
const heading = line.trimStart().match(/^(#{1,6})(?:\s+(.*))?$/);
|
|
872
|
+
if (heading) {
|
|
873
|
+
flushParagraph();
|
|
874
|
+
add({
|
|
875
|
+
type: "heading",
|
|
876
|
+
level: heading[1].length,
|
|
877
|
+
children: parseInline(heading[2] ?? ""),
|
|
878
|
+
});
|
|
879
|
+
i++;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
// divider
|
|
883
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
|
884
|
+
flushParagraph();
|
|
885
|
+
add(trimmed === "---" ? { type: "divider" } : { type: "divider", marker: trimmed });
|
|
886
|
+
i++;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
// blockquote
|
|
890
|
+
if (trimmed.startsWith(">")) {
|
|
891
|
+
flushParagraph();
|
|
892
|
+
const quote = [];
|
|
893
|
+
const markers = [];
|
|
894
|
+
while (i < lines.length && lines[i].trim().startsWith(">")) {
|
|
895
|
+
const marker = lines[i].match(/^\s*>[ \t]?/)[0];
|
|
896
|
+
markers.push(marker);
|
|
897
|
+
quote.push(lines[i].slice(marker.length));
|
|
898
|
+
i++;
|
|
899
|
+
}
|
|
900
|
+
const quoteNode = { type: "blockquote", children: [] };
|
|
901
|
+
if (markers.some((m, k) => m !== (quote[k] ? "> " : ">")))
|
|
902
|
+
quoteNode.markers = markers;
|
|
903
|
+
container(quoteNode, quote, (children) => (quoteNode.children = children));
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
// table
|
|
907
|
+
if (trimmed.startsWith("|") && lines[i + 1]?.trim().match(/^\|[\s:|-]+\|$/)) {
|
|
908
|
+
flushParagraph();
|
|
909
|
+
const parseRow = (row) => splitTableRow(row).map((cell) => parseCell(cell.trim()));
|
|
910
|
+
const header = parseRow(lines[i]);
|
|
911
|
+
const delimiterRow = lines[i + 1];
|
|
912
|
+
i += 2;
|
|
913
|
+
const rows = [];
|
|
914
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
|
915
|
+
rows.push(parseRow(lines[i]));
|
|
916
|
+
i++;
|
|
917
|
+
}
|
|
918
|
+
const table = { type: "table", header, rows };
|
|
919
|
+
const rowLines = lines.slice(i - rows.length - 2, i).filter((_, k) => k !== 1);
|
|
920
|
+
const align = delimiterAlign(delimiterRow);
|
|
921
|
+
if (align.some((a) => a !== null))
|
|
922
|
+
table.align = align;
|
|
923
|
+
if (delimiterRow !== defaultDelimiterRow(header.length, table.align))
|
|
924
|
+
table.delimiterRow = delimiterRow;
|
|
925
|
+
const canonicalRows = serializeBlocks([{ ...table, gap: 0 }]).split("\n").filter((_, k) => k !== 1);
|
|
926
|
+
if (rowLines.some((l, k) => l !== canonicalRows[k]))
|
|
927
|
+
table.rawRows = rowLines;
|
|
928
|
+
add(table);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
// list (unordered / ordered / task, with GFM nesting)
|
|
932
|
+
const listMatch = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
|
933
|
+
if (listMatch && listMatch[1].length < 4) {
|
|
934
|
+
flushParagraph();
|
|
935
|
+
const { list, next } = parseList(lines, i);
|
|
936
|
+
add(list);
|
|
937
|
+
i = next;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
// [label]: url "title" — a reference definition, kept where it is; a footnote definition that doesn't
|
|
941
|
+
// end the document too
|
|
942
|
+
const foot = line.match(FOOTNOTE_DEF_RE);
|
|
943
|
+
const def = foot ? null : line.match(REF_DEF_RE);
|
|
944
|
+
if (foot || (def && !def[1].startsWith("^"))) {
|
|
945
|
+
flushParagraph();
|
|
946
|
+
const node = foot
|
|
947
|
+
? { type: "definition", label: `^${foot[1]}`, url: foot[2], ...(foot[3] ? { title: foot[3] } : {}) }
|
|
948
|
+
: { type: "definition", label: def[1], url: def[2], ...(def[3] !== undefined ? { title: def[3] } : {}) };
|
|
949
|
+
if (line !== definitionLine(node))
|
|
950
|
+
node.raw = line;
|
|
951
|
+
add(node);
|
|
952
|
+
i++;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
// Continuation lines keep their indentation and hard-break spaces, so wrapped text comes back as written.
|
|
956
|
+
if (!paragraph.length) {
|
|
957
|
+
paragraphGap = gapHere;
|
|
958
|
+
paragraphLines = gapLines;
|
|
959
|
+
paragraphIndent = line.match(/^\s*/)[0];
|
|
960
|
+
}
|
|
961
|
+
paragraph.push(paragraph.length ? line : line.trimStart());
|
|
962
|
+
i++;
|
|
963
|
+
}
|
|
964
|
+
flushParagraph();
|
|
965
|
+
return { blocks, end: blank };
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* The `{% name %}…{% endname %}` items of a container body (tabs, steps, columns, updates), with the blank
|
|
969
|
+
* lines before each (`gap`: default 0 before the first, 1 between items) and after the last (`end`).
|
|
970
|
+
* Other lines between items are ignored.
|
|
971
|
+
*/
|
|
972
|
+
function parseItems(lines, name, make) {
|
|
973
|
+
const items = [];
|
|
974
|
+
let blank = 0;
|
|
975
|
+
let i = 0;
|
|
976
|
+
while (i < lines.length) {
|
|
977
|
+
if (!lines[i].trim()) {
|
|
978
|
+
blank++;
|
|
979
|
+
i++;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const tag = templateTag(lines[i]);
|
|
983
|
+
if (tag?.name === name) {
|
|
984
|
+
const { body, next, closing } = collectUntil(lines, i + 1, name);
|
|
985
|
+
items.push(spaced(tagged(make(tag, body), lines[i], closing), blank, items.length ? 1 : 0));
|
|
986
|
+
i = next;
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
i++;
|
|
990
|
+
}
|
|
991
|
+
blank = 0;
|
|
992
|
+
}
|
|
993
|
+
return { items, end: blank };
|
|
994
|
+
}
|
|
995
|
+
function parseSteps(lines) {
|
|
996
|
+
return parseItems(lines, "step", (_, body) => {
|
|
997
|
+
// GitBook convention: the step's first heading is its title
|
|
998
|
+
const { blocks: inner, end } = parseBody(body);
|
|
999
|
+
let title = "";
|
|
1000
|
+
const heading = inner[0]?.type === "heading" ? inner[0] : undefined;
|
|
1001
|
+
const step = { type: "step", title, children: inner };
|
|
1002
|
+
if (heading) {
|
|
1003
|
+
step.title = plainText(heading.children);
|
|
1004
|
+
inner.shift();
|
|
1005
|
+
// The title heading as written (level, formatting, blank lines before it), where not `### title`.
|
|
1006
|
+
const written = body.slice(0, (heading.gap ?? 0) + (heading.setext ? 2 : 1)).join("\n");
|
|
1007
|
+
if (written !== `### ${step.title}`)
|
|
1008
|
+
step.heading = written;
|
|
1009
|
+
}
|
|
1010
|
+
return ended(step, end);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
const LIST_ITEM_RE = /^(\s*)([-*+]|\d+[.)])(\s+)(.*)$/;
|
|
1014
|
+
// Indent-aware list parser: deeper-indented marker lines become nested lists
|
|
1015
|
+
// inside the item (handled recursively via parseBlocks on dedented lines).
|
|
1016
|
+
// The markers, numbering, padding, continuation indent and blank lines between items are recorded
|
|
1017
|
+
// where they differ from what serialization writes.
|
|
1018
|
+
function parseList(lines, start) {
|
|
1019
|
+
const first = lines[start].match(LIST_ITEM_RE);
|
|
1020
|
+
const baseIndent = first[1].length;
|
|
1021
|
+
const ordered = /^\d/.test(first[2]);
|
|
1022
|
+
const items = [];
|
|
1023
|
+
const list = { type: "list", ordered, task: false, items };
|
|
1024
|
+
if (first[1])
|
|
1025
|
+
list.indent = first[1];
|
|
1026
|
+
if (ordered) {
|
|
1027
|
+
const n = Number.parseInt(first[2], 10);
|
|
1028
|
+
if (n !== 1)
|
|
1029
|
+
list.start = n;
|
|
1030
|
+
if (first[2].endsWith(")"))
|
|
1031
|
+
list.delimiter = ")";
|
|
1032
|
+
}
|
|
1033
|
+
else if (first[2] !== "-") {
|
|
1034
|
+
list.bullet = first[2];
|
|
1035
|
+
}
|
|
1036
|
+
let i = start;
|
|
1037
|
+
let gap = 0;
|
|
1038
|
+
let blankLine = "";
|
|
1039
|
+
while (i < lines.length) {
|
|
1040
|
+
const m = lines[i].match(LIST_ITEM_RE);
|
|
1041
|
+
if (!m || m[1].length !== baseIndent || /^\d/.test(m[2]) !== ordered)
|
|
1042
|
+
break;
|
|
1043
|
+
let content = m[4];
|
|
1044
|
+
let checked;
|
|
1045
|
+
let checkMark;
|
|
1046
|
+
const taskMatch = content.match(/^\[([ xX])\]\s+(.*)$/);
|
|
1047
|
+
if (taskMatch) {
|
|
1048
|
+
list.task = true;
|
|
1049
|
+
checked = taskMatch[1] !== " ";
|
|
1050
|
+
checkMark = taskMatch[1];
|
|
1051
|
+
content = taskMatch[2];
|
|
1052
|
+
}
|
|
1053
|
+
// Everything indented deeper than the marker belongs to this item.
|
|
1054
|
+
const contIndent = baseIndent + m[2].length + 1;
|
|
1055
|
+
const minIndent = Math.min(contIndent, baseIndent + 2);
|
|
1056
|
+
let bodyIndent;
|
|
1057
|
+
const body = [];
|
|
1058
|
+
i++;
|
|
1059
|
+
while (i < lines.length) {
|
|
1060
|
+
const raw = lines[i];
|
|
1061
|
+
if (!raw.trim()) {
|
|
1062
|
+
// blank line stays in the item only if more indented content follows
|
|
1063
|
+
const lookahead = lines[i + 1];
|
|
1064
|
+
if (lookahead !== undefined && /^\s+/.test(lookahead) && lookahead.search(/\S/) >= minIndent) {
|
|
1065
|
+
// Whitespace-only lines stay as written (serialization doesn't indent them).
|
|
1066
|
+
body.push(raw);
|
|
1067
|
+
i++;
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1072
|
+
const indent = raw.search(/\S/);
|
|
1073
|
+
if (indent >= minIndent) {
|
|
1074
|
+
// The first continuation line sets the item's indent (four or more past the content is code).
|
|
1075
|
+
bodyIndent ??= indent >= contIndent + 4 ? contIndent : indent;
|
|
1076
|
+
body.push(raw.slice(Math.min(indent, bodyIndent)));
|
|
1077
|
+
i++;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
const para = { type: "paragraph", children: parseInline(content) };
|
|
1083
|
+
const children = body.length ? [para, ...parseBlocks(body)] : [para];
|
|
1084
|
+
const item = checked === undefined ? { type: "listItem", children } : { type: "listItem", children, checked };
|
|
1085
|
+
const expected = ordered ? `${(list.start ?? 1) + items.length}${list.delimiter ?? "."}` : (list.bullet ?? "-");
|
|
1086
|
+
if (m[2] !== expected)
|
|
1087
|
+
item.marker = m[2];
|
|
1088
|
+
if (m[3].length !== 1)
|
|
1089
|
+
item.pad = m[3].length;
|
|
1090
|
+
if (bodyIndent !== undefined && bodyIndent - baseIndent !== 2)
|
|
1091
|
+
item.indent = bodyIndent - baseIndent;
|
|
1092
|
+
if (checkMark === "X")
|
|
1093
|
+
item.checkMark = "X";
|
|
1094
|
+
if (blankLine)
|
|
1095
|
+
item.blanks = [blankLine];
|
|
1096
|
+
items.push(spaced(item, gap, 0));
|
|
1097
|
+
gap = 0;
|
|
1098
|
+
blankLine = "";
|
|
1099
|
+
// skip a single blank line between sibling items
|
|
1100
|
+
if (i < lines.length && !lines[i].trim()) {
|
|
1101
|
+
const after = lines[i + 1]?.match(/^(\s*)([-*+]|\d+[.)])\s+/);
|
|
1102
|
+
if (after && after[1].length === baseIndent && /^\d/.test(after[2]) === ordered) {
|
|
1103
|
+
gap = 1;
|
|
1104
|
+
blankLine = lines[i];
|
|
1105
|
+
i++;
|
|
1106
|
+
}
|
|
1107
|
+
else
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return { list, next: i };
|
|
1112
|
+
}
|
|
1113
|
+
//# sourceMappingURL=parse.js.map
|