@localix-ai/flowdown 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/browser/file-actions.js +80 -0
- package/dist/browser/file-actions.js.map +1 -0
- package/dist/core/block-facts.js +72 -0
- package/dist/core/block-facts.js.map +1 -0
- package/dist/core/blocks.d.ts +25 -0
- package/dist/core/blocks.js +463 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/bounded-cache.js +42 -0
- package/dist/core/bounded-cache.js.map +1 -0
- package/dist/core/dollars.d.ts +12 -0
- package/dist/core/dollars.js +88 -0
- package/dist/core/dollars.js.map +1 -0
- package/dist/core/fence-scan.js +53 -0
- package/dist/core/fence-scan.js.map +1 -0
- package/dist/core/file-tree-parse.d.ts +26 -0
- package/dist/core/file-tree-parse.js +51 -0
- package/dist/core/file-tree-parse.js.map +1 -0
- package/dist/core/forms-html.js +63 -0
- package/dist/core/forms-html.js.map +1 -0
- package/dist/core/forms.d.ts +28 -0
- package/dist/core/forms.js +23 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/headings.d.ts +11 -0
- package/dist/core/headings.js +74 -0
- package/dist/core/headings.js.map +1 -0
- package/dist/core/heal.d.ts +11 -0
- package/dist/core/heal.js +220 -0
- package/dist/core/heal.js.map +1 -0
- package/dist/core/load-once.js +23 -0
- package/dist/core/load-once.js.map +1 -0
- package/dist/core/math-delimiters.js +33 -0
- package/dist/core/math-delimiters.js.map +1 -0
- package/dist/core/math-syntax.js +56 -0
- package/dist/core/math-syntax.js.map +1 -0
- package/dist/core/prose.d.ts +10 -0
- package/dist/core/prose.js +77 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/remark-answer-blocks.d.ts +5 -0
- package/dist/core/remark-answer-blocks.js +172 -0
- package/dist/core/remark-answer-blocks.js.map +1 -0
- package/dist/core/remark-answer-containers.js +299 -0
- package/dist/core/remark-answer-containers.js.map +1 -0
- package/dist/core/sanitize-schema.d.ts +18 -0
- package/dist/core/sanitize-schema.js +62 -0
- package/dist/core/sanitize-schema.js.map +1 -0
- package/dist/core/syntax.d.ts +8 -0
- package/dist/core/syntax.js +32 -0
- package/dist/core/syntax.js.map +1 -0
- package/dist/dialect.d.ts +92 -0
- package/dist/dialect.js +176 -0
- package/dist/dialect.js.map +1 -0
- package/dist/document/fence.d.ts +25 -0
- package/dist/document/fence.js +101 -0
- package/dist/document/fence.js.map +1 -0
- package/dist/document/index.d.ts +46 -0
- package/dist/document/index.js +66 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/table.d.ts +11 -0
- package/dist/document/table.js +35 -0
- package/dist/document/table.js.map +1 -0
- package/dist/dom/index.d.ts +21 -0
- package/dist/dom/index.js +123 -0
- package/dist/dom/index.js.map +1 -0
- package/dist/dom/patch.js +104 -0
- package/dist/dom/patch.js.map +1 -0
- package/dist/element/index.d.ts +29 -0
- package/dist/element/index.js +64 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine/blocks.js +774 -0
- package/dist/engine/blocks.js.map +1 -0
- package/dist/engine/entities.js +83 -0
- package/dist/engine/entities.js.map +1 -0
- package/dist/engine/entity-core.js +9 -0
- package/dist/engine/entity-core.js.map +1 -0
- package/dist/engine/entity-table.js +9 -0
- package/dist/engine/entity-table.js.map +1 -0
- package/dist/engine/html.d.ts +7 -0
- package/dist/engine/html.js +207 -0
- package/dist/engine/html.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +74 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/inlines.js +852 -0
- package/dist/engine/inlines.js.map +1 -0
- package/dist/engine/types.d.ts +109 -0
- package/dist/engine/url.d.ts +11 -0
- package/dist/engine/url.js +16 -0
- package/dist/engine/url.js.map +1 -0
- package/dist/engine/visit.d.ts +10 -0
- package/dist/engine/visit.js +29 -0
- package/dist/engine/visit.js.map +1 -0
- package/dist/highlight/highlight.d.ts +4 -0
- package/dist/highlight/highlight.js +144 -0
- package/dist/highlight/highlight.js.map +1 -0
- package/dist/highlight/index.d.ts +4 -0
- package/dist/highlight/index.js +18 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/html/index.d.ts +4 -0
- package/dist/html/index.js +544 -0
- package/dist/html/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/math/index.d.ts +17 -0
- package/dist/math/index.js +20 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/typeset.d.ts +12 -0
- package/dist/math/typeset.js +74 -0
- package/dist/math/typeset.js.map +1 -0
- package/dist/mermaid/diagram.d.ts +8 -0
- package/dist/mermaid/diagram.js +114 -0
- package/dist/mermaid/diagram.js.map +1 -0
- package/dist/mermaid/drawing.js +139 -0
- package/dist/mermaid/drawing.js.map +1 -0
- package/dist/mermaid/icons.js +16 -0
- package/dist/mermaid/icons.js.map +1 -0
- package/dist/mermaid/index.d.ts +13 -0
- package/dist/mermaid/index.js +3 -0
- package/dist/mermaid/loader.d.ts +5 -0
- package/dist/mermaid/loader.js +18 -0
- package/dist/mermaid/loader.js.map +1 -0
- package/dist/mermaid/look.d.ts +16 -0
- package/dist/mermaid/look.js +167 -0
- package/dist/mermaid/look.js.map +1 -0
- package/dist/mermaid/normalize-svg.js +44 -0
- package/dist/mermaid/normalize-svg.js.map +1 -0
- package/dist/mermaid/renderer.d.ts +22 -0
- package/dist/mermaid/renderer.js +132 -0
- package/dist/mermaid/renderer.js.map +1 -0
- package/dist/mermaid/viewer.js +309 -0
- package/dist/mermaid/viewer.js.map +1 -0
- package/dist/prompt/index.d.ts +19 -0
- package/dist/prompt/index.js +54 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/react/callout.d.ts +9 -0
- package/dist/react/callout.js +12 -0
- package/dist/react/callout.js.map +1 -0
- package/dist/react/code-block.d.ts +18 -0
- package/dist/react/code-block.js +81 -0
- package/dist/react/code-block.js.map +1 -0
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +17 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/draw.d.ts +2 -0
- package/dist/react/draw.js +16 -0
- package/dist/react/draw.js.map +1 -0
- package/dist/react/file-tree.d.ts +16 -0
- package/dist/react/file-tree.js +69 -0
- package/dist/react/file-tree.js.map +1 -0
- package/dist/react/growing-code.js +36 -0
- package/dist/react/growing-code.js.map +1 -0
- package/dist/react/helpers.d.ts +6 -0
- package/dist/react/helpers.js +17 -0
- package/dist/react/helpers.js.map +1 -0
- package/dist/react/icons.js +23 -0
- package/dist/react/icons.js.map +1 -0
- package/dist/react/labels.js +9 -0
- package/dist/react/labels.js.map +1 -0
- package/dist/react/markdown.d.ts +78 -0
- package/dist/react/markdown.js +204 -0
- package/dist/react/markdown.js.map +1 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/render.js +41 -0
- package/dist/react/render.js.map +1 -0
- package/dist/react/table-rows.js +66 -0
- package/dist/react/table-rows.js.map +1 -0
- package/dist/react/table.js +36 -0
- package/dist/react/table.js.map +1 -0
- package/dist/react/tabs.d.ts +25 -0
- package/dist/react/tabs.js +58 -0
- package/dist/react/tabs.js.map +1 -0
- package/dist/react/use-smooth-text.d.ts +6 -0
- package/dist/react/use-smooth-text.js +156 -0
- package/dist/react/use-smooth-text.js.map +1 -0
- package/dist/react/use-stage.d.ts +9 -0
- package/dist/react/use-stage.js +57 -0
- package/dist/react/use-stage.js.map +1 -0
- package/dist/remark.d.ts +20 -0
- package/dist/remark.js +10 -0
- package/dist/render/factory.d.ts +15 -0
- package/dist/render/labels.d.ts +41 -0
- package/dist/render/labels.js +43 -0
- package/dist/render/labels.js.map +1 -0
- package/dist/render/tree.js +312 -0
- package/dist/render/tree.js.map +1 -0
- package/dist/render/view.d.ts +55 -0
- package/dist/render/view.js +302 -0
- package/dist/render/view.js.map +1 -0
- package/dist/render/views.d.ts +16 -0
- package/dist/render/views.js +261 -0
- package/dist/render/views.js.map +1 -0
- package/dist/render/vnode.d.ts +21 -0
- package/dist/render/vnode.js +194 -0
- package/dist/render/vnode.js.map +1 -0
- package/dist/static/index.d.ts +28 -0
- package/dist/static/index.js +27 -0
- package/dist/static/index.js.map +1 -0
- package/dist/stream.d.ts +12 -0
- package/dist/stream.js +3 -0
- package/dist/styles.css +1351 -0
- package/dist/tui/code.d.ts +5 -0
- package/dist/tui/code.js +240 -0
- package/dist/tui/code.js.map +1 -0
- package/dist/tui/index.d.ts +54 -0
- package/dist/tui/index.js +99 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/math.js +242 -0
- package/dist/tui/math.js.map +1 -0
- package/dist/tui/printable.d.ts +12 -0
- package/dist/tui/printable.js +31 -0
- package/dist/tui/printable.js.map +1 -0
- package/dist/tui/render.d.ts +12 -0
- package/dist/tui/render.js +524 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/style.d.ts +35 -0
- package/dist/tui/style.js +77 -0
- package/dist/tui/style.js.map +1 -0
- package/dist/tui/table.js +73 -0
- package/dist/tui/table.js.map +1 -0
- package/dist/tui/width.d.ts +2 -0
- package/dist/tui/width.js +133 -0
- package/dist/tui/width.js.map +1 -0
- package/dist/tui/wrap.d.ts +8 -0
- package/dist/tui/wrap.js +190 -0
- package/dist/tui/wrap.js.map +1 -0
- package/package.json +127 -4
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { visit as visit$1 } from "../engine/visit.js";
|
|
2
|
+
import { ATTRIBUTION_PREFIX, fenceTitle, parseCalloutMarker } from "../dialect.js";
|
|
3
|
+
import { applyContainer, isShapedContainer, meaningful } from "./remark-answer-containers.js";
|
|
4
|
+
import { lowered, marked } from "./forms.js";
|
|
5
|
+
import { lowerToHtml } from "./forms-html.js";
|
|
6
|
+
// src/core/remark-answer-blocks.ts
|
|
7
|
+
/** The engine's tree walker, read with mdast's types: the engine's tree is mdast's. */
|
|
8
|
+
const visit = visit$1;
|
|
9
|
+
/** Strip a `[!KIND]` marker off the quote's first paragraph; true when it was a callout. */
|
|
10
|
+
function applyCallout(quote) {
|
|
11
|
+
const first = quote.children[0];
|
|
12
|
+
if (first?.type !== "paragraph") return false;
|
|
13
|
+
const lead = first.children[0];
|
|
14
|
+
if (lead?.type !== "text") return false;
|
|
15
|
+
const marker = parseCalloutMarker(lead.value);
|
|
16
|
+
if (!marker) return false;
|
|
17
|
+
const titleIsWholeLine = lead.value.includes("\n") || first.children.length === 1;
|
|
18
|
+
const title = titleIsWholeLine ? marker.title : "";
|
|
19
|
+
lead.value = titleIsWholeLine ? lead.value.slice(marker.consumed) : lead.value.slice(lead.value.indexOf("]") + 1).trimStart();
|
|
20
|
+
if (lead.value === "") first.children.shift();
|
|
21
|
+
if (first.children.length === 0) quote.children.shift();
|
|
22
|
+
quote.data = marked({
|
|
23
|
+
form: "callout",
|
|
24
|
+
kind: marker.kind,
|
|
25
|
+
...title ? { title } : {}
|
|
26
|
+
}, quote.data);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Where the attribution starts inside a paragraph: its first node (`> — Author` as its own
|
|
31
|
+
* paragraph) or a line inside it (`> quote\n> — Author`, the common single-paragraph form).
|
|
32
|
+
*/
|
|
33
|
+
function attributionSplit(paragraph) {
|
|
34
|
+
for (let index = paragraph.children.length - 1; index >= 0; index -= 1) {
|
|
35
|
+
const node = paragraph.children[index];
|
|
36
|
+
if (node?.type !== "text") continue;
|
|
37
|
+
const lines = node.value.split("\n");
|
|
38
|
+
for (let line = lines.length - 1; line >= 0; line -= 1) {
|
|
39
|
+
if (!ATTRIBUTION_PREFIX.test(lines[line])) continue;
|
|
40
|
+
const atParagraphStart = index === 0 && line === 0;
|
|
41
|
+
const afterBreak = line === 0 && paragraph.children[index - 1]?.type === "break";
|
|
42
|
+
if (line === 0 && !atParagraphStart && !afterBreak) continue;
|
|
43
|
+
const offset = lines.slice(0, line).reduce((sum, text) => sum + text.length + 1, 0);
|
|
44
|
+
return {
|
|
45
|
+
index,
|
|
46
|
+
offset
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** Move a quote's closing `— Author` line into a footer paragraph of its own. */
|
|
53
|
+
function applyAttribution(quote) {
|
|
54
|
+
const last = quote.children.at(-1);
|
|
55
|
+
if (last?.type !== "paragraph") return;
|
|
56
|
+
const split = attributionSplit(last);
|
|
57
|
+
if (!split) return;
|
|
58
|
+
if (quote.children.length === 1 && split.index === 0 && split.offset === 0) return;
|
|
59
|
+
const node = last.children[split.index];
|
|
60
|
+
const lead = last.children.slice(0, split.index);
|
|
61
|
+
if (lead.at(-1)?.type === "break") lead.pop();
|
|
62
|
+
const before = node.value.slice(0, split.offset).replace(/\n$/, "");
|
|
63
|
+
if (before) lead.push({
|
|
64
|
+
type: "text",
|
|
65
|
+
value: before
|
|
66
|
+
});
|
|
67
|
+
const sourceText = node.value.slice(split.offset).replace(ATTRIBUTION_PREFIX, "");
|
|
68
|
+
const source = [...sourceText ? [{
|
|
69
|
+
type: "text",
|
|
70
|
+
value: sourceText
|
|
71
|
+
}] : [], ...last.children.slice(split.index + 1)];
|
|
72
|
+
const footer = {
|
|
73
|
+
type: "paragraph",
|
|
74
|
+
data: marked({ form: "quote-source" }),
|
|
75
|
+
children: source
|
|
76
|
+
};
|
|
77
|
+
if (lead.length === 0) quote.children.splice(-1, 1, footer);
|
|
78
|
+
else {
|
|
79
|
+
last.children = lead;
|
|
80
|
+
quote.children.push(footer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The caption a line right under an image carries: that line is one emphasis and nothing else. */
|
|
84
|
+
function emphasisCaption(next) {
|
|
85
|
+
if (next?.type !== "paragraph") return null;
|
|
86
|
+
const content = meaningful(next.children);
|
|
87
|
+
return content.length === 1 && content[0].type === "emphasis" ? content[0].children : null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* An image alone in its paragraph is a figure: centred and bounded, rather than an inline
|
|
91
|
+
* image stretched across the reply. Its caption is the image title, or — the way models and
|
|
92
|
+
* people most often write one — an `*emphasis-only*` line directly under it, which is then
|
|
93
|
+
* consumed into the figure instead of trailing it as a stray italic paragraph.
|
|
94
|
+
*/
|
|
95
|
+
function applyFigure(paragraph, index, parent) {
|
|
96
|
+
const content = meaningful(paragraph.children);
|
|
97
|
+
const image = content[0];
|
|
98
|
+
if (image?.type !== "image") return;
|
|
99
|
+
const sameParagraph = content.length === 2 && content[1].type === "emphasis" && paragraph.children.some((child) => child.type === "text" && child.value.includes("\n"));
|
|
100
|
+
if (content.length !== 1 && !sameParagraph) return;
|
|
101
|
+
const title = image.title?.trim();
|
|
102
|
+
let underneath = null;
|
|
103
|
+
if (sameParagraph) underneath = content[1].children;
|
|
104
|
+
else if (!title) {
|
|
105
|
+
underneath = emphasisCaption(parent.children[index + 1]);
|
|
106
|
+
if (underneath) parent.children.splice(index + 1, 1);
|
|
107
|
+
}
|
|
108
|
+
const caption = title ? [{
|
|
109
|
+
type: "text",
|
|
110
|
+
value: title
|
|
111
|
+
}] : underneath;
|
|
112
|
+
paragraph.data = marked({ form: "figure" }, paragraph.data);
|
|
113
|
+
paragraph.children = [image, ...caption ? [{
|
|
114
|
+
type: "emphasis",
|
|
115
|
+
data: marked({ form: "caption" }),
|
|
116
|
+
children: caption
|
|
117
|
+
}] : []];
|
|
118
|
+
}
|
|
119
|
+
const SECTION_NUMBER = /^(\d+(?:\.\d+)*\.?)[ \t]+/;
|
|
120
|
+
/**
|
|
121
|
+
* A heading the author numbered (`## 1.2 Water temperature`) gets its number as a quieter span,
|
|
122
|
+
* so the title reads first and the number as the outline position it is. Only a number the text
|
|
123
|
+
* already carries is styled; nothing is numbered that the author did not number.
|
|
124
|
+
*/
|
|
125
|
+
function applySectionNumber(heading) {
|
|
126
|
+
const lead = heading.children[0];
|
|
127
|
+
if (lead?.type !== "text") return;
|
|
128
|
+
const match = SECTION_NUMBER.exec(lead.value);
|
|
129
|
+
if (!match || match[0].length === lead.value.length) return;
|
|
130
|
+
lead.value = lead.value.slice(match[0].length);
|
|
131
|
+
heading.children.unshift({
|
|
132
|
+
type: "emphasis",
|
|
133
|
+
data: marked({ form: "section-number" }),
|
|
134
|
+
children: [{
|
|
135
|
+
type: "text",
|
|
136
|
+
value: match[1]
|
|
137
|
+
}]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The dialect's pass over a tree. `lower` says what each form it marks is in the target's terms;
|
|
142
|
+
* without one the nodes carry their marks alone.
|
|
143
|
+
*/
|
|
144
|
+
function dialectPass(lower) {
|
|
145
|
+
const read = (tree) => visit(tree, (node, index, parent) => {
|
|
146
|
+
if (node.type === "blockquote") {
|
|
147
|
+
if (isShapedContainer(node)) return void 0;
|
|
148
|
+
if (parent && index !== void 0 && applyContainer(node, index, parent)) return index;
|
|
149
|
+
if (!applyCallout(node)) applyAttribution(node);
|
|
150
|
+
} else if (node.type === "paragraph" && parent && index !== void 0) applyFigure(node, index, parent);
|
|
151
|
+
else if (node.type === "heading") applySectionNumber(node);
|
|
152
|
+
else if (node.type === "code" && node.meta) {
|
|
153
|
+
const title = fenceTitle(node.meta);
|
|
154
|
+
if (title) node.data = marked({
|
|
155
|
+
form: "code",
|
|
156
|
+
title
|
|
157
|
+
}, node.data);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return (tree) => lowered(lower, () => read(tree));
|
|
161
|
+
}
|
|
162
|
+
/** The dialect as a remark plugin: its forms as the elements and classes `styles.css` draws. */
|
|
163
|
+
const remarkAnswerBlocks = () => dialectPass(lowerToHtml);
|
|
164
|
+
const transform = remarkAnswerBlocks();
|
|
165
|
+
/** The dialect as an extension of the engine: what `<Markdown>` reads every document with. */
|
|
166
|
+
const flowdownDialect = {
|
|
167
|
+
name: "flowdown",
|
|
168
|
+
transform: (root) => transform(root)
|
|
169
|
+
};
|
|
170
|
+
export { dialectPass, flowdownDialect, remarkAnswerBlocks };
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=remark-answer-blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remark-answer-blocks.js","names":["walk"],"sources":["../../src/core/remark-answer-blocks.ts"],"sourcesContent":["// remark plugin: the answer dialect's block constructs (`./dialect`).\n//\n// Plain GFM gives an agent one way to set text apart — a grey quote — so a warning, a tip and\n// a cited sentence all looked the same. This pass keeps the source plain Markdown (it still\n// reads correctly as text, on GitHub, or pasted anywhere) and only re-labels nodes the\n// renderer already emits:\n// `> [!WARNING]` quote → blockquote carrying `data-callout` (+ `data-callout-title`)\n// `> — Author` closing line → the quote's <footer> attribution\n// an image alone in a paragraph → a centred <figure>, captioned by its title\n// (``) or by an emphasis-only line right under it (`*Caption*`)\n// `## 1.2 Title` → the section number set apart, quieter than the title\n// `> [!STEPS]`, `[!TABS]`, `[!STATS]`, `[!PROS]`/`[!CONS]`, `[!DETAILS]`, `[!SOURCES]` →\n// container forms, shaped by `remarkAnswerContainers.ts` (a timeline, tabs, a row of figures,\n// the case for and against, a folded section, the references)\n// ` ```ts title=\"src/app.ts\" ` → the fence's file name on its <code> (`data-title`)\n// Everything stays in mdast `data.hName`/`hProperties`, so no raw HTML is produced and the\n// sanitize pipeline has nothing new to trust.\nimport { visit as walk } from '../engine/visit'\nimport type {\n Blockquote,\n Heading,\n Image,\n Nodes,\n Paragraph,\n Parents,\n PhrasingContent,\n Root,\n Text,\n} from 'mdast'\nimport { ATTRIBUTION_PREFIX, fenceTitle, parseCalloutMarker } from '../dialect'\nimport { applyContainer, isShapedContainer, meaningful } from './remark-answer-containers'\nimport type { Extension, Tree } from '../engine/types'\nimport { lowered, marked, type Lowering } from './forms'\nimport { lowerToHtml } from './forms-html'\n\n/** The engine's tree walker, read with mdast's types: the engine's tree is mdast's. */\nconst visit = walk as unknown as (\n tree: Root,\n visitor: (\n node: Nodes,\n index: number | undefined,\n parent: Parents | undefined,\n ) => number | undefined,\n) => void\n\n/** Strip a `[!KIND]` marker off the quote's first paragraph; true when it was a callout. */\nfunction applyCallout(quote: Blockquote): boolean {\n const first = quote.children[0]\n if (first?.type !== 'paragraph') return false\n const lead = first.children[0]\n if (lead?.type !== 'text') return false\n const marker = parseCalloutMarker(lead.value)\n if (!marker) return false\n\n // The title is the marker line's own text. When that line carries formatting (`[!TIP] **x**`)\n // the lead text node ends before the line does, so the line is body content instead. The line\n // ended inside the lead when the lead has a line break, or is all there is.\n const titleIsWholeLine = lead.value.includes('\\n') || first.children.length === 1\n const title = titleIsWholeLine ? marker.title : ''\n lead.value = titleIsWholeLine\n ? lead.value.slice(marker.consumed)\n : lead.value.slice(lead.value.indexOf(']') + 1).trimStart()\n if (lead.value === '') first.children.shift()\n if (first.children.length === 0) quote.children.shift()\n\n quote.data = marked(\n { form: 'callout', kind: marker.kind, ...(title ? { title } : {}) },\n quote.data,\n )\n return true\n}\n\n/**\n * Where the attribution starts inside a paragraph: its first node (`> — Author` as its own\n * paragraph) or a line inside it (`> quote\\n> — Author`, the common single-paragraph form).\n */\nfunction attributionSplit(paragraph: Paragraph): { index: number; offset: number } | null {\n for (let index = paragraph.children.length - 1; index >= 0; index -= 1) {\n const node = paragraph.children[index]\n if (node?.type !== 'text') continue\n const lines = node.value.split('\\n')\n for (let line = lines.length - 1; line >= 0; line -= 1) {\n if (!ATTRIBUTION_PREFIX.test(lines[line]!)) continue\n const atParagraphStart = index === 0 && line === 0\n const afterBreak = line === 0 && paragraph.children[index - 1]?.type === 'break'\n if (line === 0 && !atParagraphStart && !afterBreak) continue\n const offset = lines.slice(0, line).reduce((sum, text) => sum + text.length + 1, 0)\n return { index, offset }\n }\n }\n return null\n}\n\n/** Move a quote's closing `— Author` line into a footer paragraph of its own. */\nfunction applyAttribution(quote: Blockquote): void {\n const last = quote.children.at(-1)\n if (last?.type !== 'paragraph') return\n const split = attributionSplit(last)\n if (!split) return\n // A quote that is nothing but its source line has no quotation to attribute.\n if (quote.children.length === 1 && split.index === 0 && split.offset === 0) return\n\n const node = last.children[split.index] as Text\n const lead: PhrasingContent[] = last.children.slice(0, split.index)\n if (lead.at(-1)?.type === 'break') lead.pop()\n const before = node.value.slice(0, split.offset).replace(/\\n$/, '')\n if (before) lead.push({ type: 'text', value: before })\n const sourceText = node.value.slice(split.offset).replace(ATTRIBUTION_PREFIX, '')\n const source: PhrasingContent[] = [\n ...(sourceText ? [{ type: 'text', value: sourceText } as Text] : []),\n ...last.children.slice(split.index + 1),\n ]\n const footer: Paragraph = {\n type: 'paragraph',\n data: marked({ form: 'quote-source' }),\n children: source,\n }\n if (lead.length === 0) quote.children.splice(-1, 1, footer)\n else {\n last.children = lead\n quote.children.push(footer)\n }\n}\n\n/** The caption a line right under an image carries: that line is one emphasis and nothing else. */\nfunction emphasisCaption(next: Parents['children'][number] | undefined): PhrasingContent[] | null {\n if (next?.type !== 'paragraph') return null\n const content = meaningful(next.children)\n return content.length === 1 && content[0]!.type === 'emphasis' ? content[0].children : null\n}\n\n/**\n * An image alone in its paragraph is a figure: centred and bounded, rather than an inline\n * image stretched across the reply. Its caption is the image title, or — the way models and\n * people most often write one — an `*emphasis-only*` line directly under it, which is then\n * consumed into the figure instead of trailing it as a stray italic paragraph.\n */\nfunction applyFigure(paragraph: Paragraph, index: number, parent: Parents): void {\n const content = meaningful(paragraph.children)\n const image = content[0]\n if (image?.type !== 'image') return\n // `` then `*Caption*` on the very next line is ONE paragraph: image, a line\n // break, emphasis. The break must be there — ` *c*` on one line is prose.\n const sameParagraph =\n content.length === 2 &&\n content[1]!.type === 'emphasis' &&\n paragraph.children.some((child) => child.type === 'text' && child.value.includes('\\n'))\n if (content.length !== 1 && !sameParagraph) return\n const title = (image as Image).title?.trim()\n let underneath: PhrasingContent[] | null = null\n if (sameParagraph)\n underneath = (content[1] as Extract<PhrasingContent, { type: 'emphasis' }>).children\n else if (!title) {\n underneath = emphasisCaption(parent.children[index + 1])\n if (underneath) parent.children.splice(index + 1, 1)\n }\n const caption: PhrasingContent[] | null = title ? [{ type: 'text', value: title }] : underneath\n // A class of its own: code blocks and diagrams are <figure>s with a <figcaption> toolbar too.\n paragraph.data = marked({ form: 'figure' }, paragraph.data)\n paragraph.children = [\n image,\n // `emphasis` is only a phrasing carrier here: hName turns the element into the caption.\n ...(caption\n ? [\n {\n type: 'emphasis',\n data: marked({ form: 'caption' }),\n children: caption,\n } as PhrasingContent,\n ]\n : []),\n ]\n}\n\nconst SECTION_NUMBER = /^(\\d+(?:\\.\\d+)*\\.?)[ \\t]+/\n\n/**\n * A heading the author numbered (`## 1.2 Water temperature`) gets its number as a quieter span,\n * so the title reads first and the number as the outline position it is. Only a number the text\n * already carries is styled; nothing is numbered that the author did not number.\n */\nfunction applySectionNumber(heading: Heading): void {\n const lead = heading.children[0]\n if (lead?.type !== 'text') return\n const match = SECTION_NUMBER.exec(lead.value)\n if (!match || match[0].length === lead.value.length) return\n lead.value = lead.value.slice(match[0].length)\n heading.children.unshift({\n type: 'emphasis',\n data: marked({ form: 'section-number' }),\n children: [{ type: 'text', value: match[1]! }],\n })\n}\n\n/**\n * The dialect's pass over a tree. `lower` says what each form it marks is in the target's terms;\n * without one the nodes carry their marks alone.\n */\nexport function dialectPass(lower: Lowering | null) {\n const read = (tree: Tree): void =>\n visit(tree as Root, (node, index, parent): number | undefined => {\n if (node.type === 'blockquote') {\n // A container this pass already shaped (a tab, a pros/cons aside) is not a quote.\n if (isShapedContainer(node)) return undefined\n // Replaced in place: revisit the index so what replaced the quote is walked too.\n if (parent && index !== undefined && applyContainer(node, index, parent)) return index\n if (!applyCallout(node)) applyAttribution(node)\n } else if (node.type === 'paragraph' && parent && index !== undefined) {\n applyFigure(node, index, parent)\n } else if (node.type === 'heading') {\n applySectionNumber(node)\n } else if (node.type === 'code' && node.meta) {\n // The file a fence names rides on its <code>: a node's `meta` does not survive a sanitizer.\n const title = fenceTitle(node.meta)\n if (title) node.data = marked({ form: 'code', title }, node.data)\n }\n return undefined\n })\n return (tree: Tree): void => lowered(lower, () => read(tree))\n}\n\n/** The dialect as a remark plugin: its forms as the elements and classes `styles.css` draws. */\nexport const remarkAnswerBlocks = () => dialectPass(lowerToHtml)\n\nconst transform = remarkAnswerBlocks()\nconst mark = dialectPass(null)\n\n/** The dialect as an extension of the engine: what `<Markdown>` reads every document with. */\nexport const flowdownDialect: Extension = {\n name: 'flowdown',\n transform: (root) => transform(root),\n}\n\n/**\n * The dialect with its forms marked and nothing said about HTML: what a target that is not HTML\n * (a terminal) reads a document with.\n */\nexport const flowdownForms: Extension = {\n name: 'flowdown',\n transform: (root) => mark(root),\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { parseContainerMarker } from "../dialect.js";
|
|
2
|
+
import { formOf, marked } from "./forms.js";
|
|
3
|
+
// src/core/remark-answer-containers.ts
|
|
4
|
+
/** A phrasing run without its whitespace-only text nodes. */
|
|
5
|
+
const meaningful = (children) => children.filter((child) => !(child.type === "text" && child.value.trim() === ""));
|
|
6
|
+
/** Separator text between a lead and what follows: ` — `, ` · `, `: ` or just a line break. */
|
|
7
|
+
const LEAD_SEPARATOR = /^[ \t]*(?:[—–·:|-][ \t]*)?(?:\n[ \t]*)?/;
|
|
8
|
+
/** Drop leading separator text (and a hard break) off a phrasing run. */
|
|
9
|
+
function trimLead(nodes) {
|
|
10
|
+
const rest = [...nodes];
|
|
11
|
+
while (rest[0]) {
|
|
12
|
+
const lead = rest[0];
|
|
13
|
+
if (lead.type === "break") rest.shift();
|
|
14
|
+
else if (lead.type === "text") {
|
|
15
|
+
const value = lead.value.replace(LEAD_SEPARATOR, "");
|
|
16
|
+
if (value) {
|
|
17
|
+
rest[0] = {
|
|
18
|
+
...lead,
|
|
19
|
+
value
|
|
20
|
+
};
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
rest.shift();
|
|
24
|
+
} else break;
|
|
25
|
+
}
|
|
26
|
+
return rest;
|
|
27
|
+
}
|
|
28
|
+
/** A paragraph rendered as a `div`: in a TIGHT list remark-rehype unwraps a `p` into bare text. */
|
|
29
|
+
const block = (form, children) => ({
|
|
30
|
+
type: "paragraph",
|
|
31
|
+
data: marked({ form }),
|
|
32
|
+
children
|
|
33
|
+
});
|
|
34
|
+
/** The container's title line, when the author wrote one after the marker. */
|
|
35
|
+
const titleLine = (form, title) => title ? [{
|
|
36
|
+
type: "paragraph",
|
|
37
|
+
data: marked({ form }),
|
|
38
|
+
children: [{
|
|
39
|
+
type: "text",
|
|
40
|
+
value: title
|
|
41
|
+
}]
|
|
42
|
+
}] : [];
|
|
43
|
+
/**
|
|
44
|
+
* Split an item's first paragraph at its bold lead: the lead (and an emphasis right after it,
|
|
45
|
+
* the "when") into a head, the rest into a body. Null when the item does not open with bold.
|
|
46
|
+
*/
|
|
47
|
+
function splitLead(item) {
|
|
48
|
+
const first = item.children[0];
|
|
49
|
+
if (first?.type !== "paragraph") return null;
|
|
50
|
+
const [lead, ...after] = trimLead(first.children);
|
|
51
|
+
if (lead?.type !== "strong") return null;
|
|
52
|
+
let rest = trimLead(after);
|
|
53
|
+
let meta;
|
|
54
|
+
if (rest[0]?.type === "emphasis") {
|
|
55
|
+
meta = rest[0];
|
|
56
|
+
rest = trimLead(rest.slice(1));
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
lead,
|
|
60
|
+
...meta ? { meta } : {},
|
|
61
|
+
rest
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** One step: head (title + when) and body. An item without a bold title keeps its dot only. */
|
|
65
|
+
function shapeStep(item) {
|
|
66
|
+
const split = splitLead(item);
|
|
67
|
+
if (!split) return;
|
|
68
|
+
split.lead.data = marked({ form: "step-title" });
|
|
69
|
+
if (split.meta) split.meta.data = marked({ form: "step-meta" });
|
|
70
|
+
const head = block("step-head", split.meta ? [split.lead, split.meta] : [split.lead]);
|
|
71
|
+
const body = meaningful(split.rest).length > 0 ? [block("step-body", split.rest)] : [];
|
|
72
|
+
item.children.splice(0, 1, head, ...body);
|
|
73
|
+
}
|
|
74
|
+
/** One figure: its bold value and its label. An item without a bold value stays a plain line. */
|
|
75
|
+
function shapeStat(item) {
|
|
76
|
+
const split = splitLead(item);
|
|
77
|
+
if (!split) return;
|
|
78
|
+
split.lead.data = marked({ form: "stat-value" });
|
|
79
|
+
const label = {
|
|
80
|
+
type: "emphasis",
|
|
81
|
+
data: marked({ form: "stat-label" }),
|
|
82
|
+
children: split.meta ? [split.meta, ...split.rest] : split.rest
|
|
83
|
+
};
|
|
84
|
+
item.children.splice(0, 1, block("stat", [split.lead, label]));
|
|
85
|
+
}
|
|
86
|
+
/** The site a reference points at, as a reader names it: `docs.example.com`. '' for a relative link. */
|
|
87
|
+
function hostOf(url) {
|
|
88
|
+
return (/^[a-z][a-z0-9+.-]*:\/\/(?:[^/?#@]*@)?([^/?#:]+)/i.exec(url)?.[1] ?? "").replace(/^www\./, "");
|
|
89
|
+
}
|
|
90
|
+
const span = (form, children) => ({
|
|
91
|
+
type: "emphasis",
|
|
92
|
+
data: marked({ form }),
|
|
93
|
+
children
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* One reference: its link, the site quiet beside the title, then any note the author added. An
|
|
97
|
+
* item that does not open with a link stays a plain line. The link itself keeps its own element
|
|
98
|
+
* (and whatever the page's `a` does with it); only what is inside it is arranged.
|
|
99
|
+
*/
|
|
100
|
+
function shapeSource(item) {
|
|
101
|
+
const first = item.children[0];
|
|
102
|
+
if (first?.type !== "paragraph") return;
|
|
103
|
+
const [link, ...after] = trimLead(first.children);
|
|
104
|
+
if (link?.type !== "link") return;
|
|
105
|
+
const host = hostOf(link.url);
|
|
106
|
+
const bare = link.children.length === 1 && labelText(link.children) === link.url;
|
|
107
|
+
link.children = [span("source-title", bare && host ? [{
|
|
108
|
+
type: "text",
|
|
109
|
+
value: host
|
|
110
|
+
}] : link.children), ...host && !bare ? [span("source-host", [{
|
|
111
|
+
type: "text",
|
|
112
|
+
value: host
|
|
113
|
+
}])] : []];
|
|
114
|
+
const note = trimLead(after);
|
|
115
|
+
item.children.splice(0, 1, block("source", [link, ...meaningful(note).length > 0 ? [span("source-note", note)] : []]));
|
|
116
|
+
}
|
|
117
|
+
/** `[!TABS]`: each heading opens a tab, holding everything up to the next heading of its depth. */
|
|
118
|
+
function tabsOf(content) {
|
|
119
|
+
const first = content[0];
|
|
120
|
+
if (first?.type !== "heading") return null;
|
|
121
|
+
const depth = first.depth;
|
|
122
|
+
const tabs = [];
|
|
123
|
+
for (const node of content) if (node.type === "heading" && node.depth === depth) {
|
|
124
|
+
const label = labelText(node.children).trim();
|
|
125
|
+
tabs.push({
|
|
126
|
+
type: "blockquote",
|
|
127
|
+
data: marked({
|
|
128
|
+
form: "tab",
|
|
129
|
+
label: label || `${tabs.length + 1}`
|
|
130
|
+
}),
|
|
131
|
+
children: []
|
|
132
|
+
});
|
|
133
|
+
} else tabs.at(-1).children.push(node);
|
|
134
|
+
return tabs.length > 0 ? tabs : null;
|
|
135
|
+
}
|
|
136
|
+
/** A bold/link/emphasis heading still has a name; formatting must not reduce it to "1". */
|
|
137
|
+
function labelText(nodes) {
|
|
138
|
+
return nodes.map((node) => {
|
|
139
|
+
if ("children" in node) return labelText(node.children);
|
|
140
|
+
if ("value" in node) return node.value;
|
|
141
|
+
if (node.type === "image" || node.type === "imageReference") return node.alt ?? "";
|
|
142
|
+
return "";
|
|
143
|
+
}).join("");
|
|
144
|
+
}
|
|
145
|
+
function verdict(kind, title, list) {
|
|
146
|
+
if (list.type === "list") list.data = marked({ form: "verdict-list" }, list.data);
|
|
147
|
+
return {
|
|
148
|
+
type: "blockquote",
|
|
149
|
+
data: marked({
|
|
150
|
+
form: "verdict",
|
|
151
|
+
kind
|
|
152
|
+
}),
|
|
153
|
+
children: [...titleLine("verdict-title", title), list]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const isVerdict = (node, kind) => {
|
|
157
|
+
const form = node?.type === "blockquote" ? formOf(node) : void 0;
|
|
158
|
+
return form?.form === "verdict" && form.kind === kind;
|
|
159
|
+
};
|
|
160
|
+
/** Each form's replacement nodes, or null when the content does not fit it (it stays a quote). */
|
|
161
|
+
function shape(kind, title, content) {
|
|
162
|
+
const list = content[0];
|
|
163
|
+
switch (kind) {
|
|
164
|
+
case "steps":
|
|
165
|
+
if (list?.type !== "list") return null;
|
|
166
|
+
list.data = marked({ form: "steps" }, list.data);
|
|
167
|
+
for (const item of list.children) shapeStep(item);
|
|
168
|
+
return [
|
|
169
|
+
...titleLine("steps-title", title),
|
|
170
|
+
list,
|
|
171
|
+
...content.slice(1)
|
|
172
|
+
];
|
|
173
|
+
case "stats":
|
|
174
|
+
if (list?.type !== "list") return null;
|
|
175
|
+
list.data = marked({ form: "stats" }, list.data);
|
|
176
|
+
for (const item of list.children) shapeStat(item);
|
|
177
|
+
return [
|
|
178
|
+
...titleLine("stats-title", title),
|
|
179
|
+
list,
|
|
180
|
+
...content.slice(1)
|
|
181
|
+
];
|
|
182
|
+
case "tabs": {
|
|
183
|
+
const tabs = tabsOf(content);
|
|
184
|
+
if (!tabs) return null;
|
|
185
|
+
return [...titleLine("tabs-title", title), {
|
|
186
|
+
type: "blockquote",
|
|
187
|
+
data: marked({ form: "tabs" }),
|
|
188
|
+
children: tabs
|
|
189
|
+
}];
|
|
190
|
+
}
|
|
191
|
+
case "pros":
|
|
192
|
+
case "cons":
|
|
193
|
+
if (list?.type !== "list") return null;
|
|
194
|
+
return [verdict(kind, title, list), ...content.slice(1)];
|
|
195
|
+
case "sources":
|
|
196
|
+
if (list?.type !== "list") return null;
|
|
197
|
+
list.data = marked({ form: "sources" }, list.data);
|
|
198
|
+
for (const item of list.children) shapeSource(item);
|
|
199
|
+
return [
|
|
200
|
+
...titleLine("sources-title", title),
|
|
201
|
+
list,
|
|
202
|
+
...content.slice(1)
|
|
203
|
+
];
|
|
204
|
+
case "details": return [{
|
|
205
|
+
type: "blockquote",
|
|
206
|
+
data: marked({ form: "details" }),
|
|
207
|
+
children: [...title ? [{
|
|
208
|
+
type: "paragraph",
|
|
209
|
+
data: marked({ form: "summary" }),
|
|
210
|
+
children: [{
|
|
211
|
+
type: "text",
|
|
212
|
+
value: title
|
|
213
|
+
}]
|
|
214
|
+
}] : [], ...content]
|
|
215
|
+
}];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The blocks a marker-only quote governs, read by grammar, not guessed: a list form takes the ONE
|
|
220
|
+
* list right after it; tabs take the run of sections that starts with a heading right after it,
|
|
221
|
+
* up to a shallower heading, a `---`, or the end of the reply. Anything else governs nothing.
|
|
222
|
+
*/
|
|
223
|
+
function following(kind, siblings) {
|
|
224
|
+
const first = siblings[0];
|
|
225
|
+
if (kind === "details") return [];
|
|
226
|
+
if (kind !== "tabs") return first?.type === "list" ? [first] : [];
|
|
227
|
+
if (first?.type !== "heading") return [];
|
|
228
|
+
const run = [];
|
|
229
|
+
for (const node of siblings) {
|
|
230
|
+
if (node.type === "thematicBreak") break;
|
|
231
|
+
if (node.type === "heading" && node.depth < first.depth) break;
|
|
232
|
+
run.push(node);
|
|
233
|
+
}
|
|
234
|
+
return run;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A container quote, shaped in place. True when it was one: the caller revisits the index, so
|
|
238
|
+
* the nodes that replaced the quote are walked (and their figures and callouts shaped) too.
|
|
239
|
+
*/
|
|
240
|
+
function applyContainer(quote, index, parent) {
|
|
241
|
+
const first = quote.children[0];
|
|
242
|
+
const lead = first?.type === "paragraph" ? first.children[0] : void 0;
|
|
243
|
+
if (lead?.type !== "text" || first?.type !== "paragraph") return false;
|
|
244
|
+
const marker = parseContainerMarker(lead.value);
|
|
245
|
+
if (!marker) return false;
|
|
246
|
+
if (marker.kind === "details") {
|
|
247
|
+
if (marker.consumed < lead.value.length || first.children.length === 1) lead.value = lead.value.slice(marker.consumed);
|
|
248
|
+
else {
|
|
249
|
+
marker.title = "";
|
|
250
|
+
lead.value = lead.value.slice(lead.value.indexOf("]") + 1).trimStart();
|
|
251
|
+
}
|
|
252
|
+
if (lead.value === "") first.children.shift();
|
|
253
|
+
if (first.children[0]?.type === "break") first.children.shift();
|
|
254
|
+
if (first.children.length > 0) quote.children.unshift({
|
|
255
|
+
type: "paragraph",
|
|
256
|
+
children: []
|
|
257
|
+
});
|
|
258
|
+
} else if (first.children.length !== 1 || lead.value.slice(marker.consumed).trim()) return false;
|
|
259
|
+
const inside = quote.children.slice(1);
|
|
260
|
+
const after = inside.length === 0 ? following(marker.kind, parent.children.slice(index + 1)) : [];
|
|
261
|
+
const nodes = shape(marker.kind, marker.title, inside.length > 0 ? inside : after);
|
|
262
|
+
if (!nodes) return false;
|
|
263
|
+
const end = (after.at(-1) ?? quote).position?.end;
|
|
264
|
+
const position = quote.position && end ? {
|
|
265
|
+
start: quote.position.start,
|
|
266
|
+
end
|
|
267
|
+
} : void 0;
|
|
268
|
+
const form = nodes.length === 1 ? nodes[0] : {
|
|
269
|
+
type: "blockquote",
|
|
270
|
+
data: marked({ form: "form" }),
|
|
271
|
+
children: nodes
|
|
272
|
+
};
|
|
273
|
+
if (position) form.position = position;
|
|
274
|
+
parent.children.splice(index, 1 + after.length, form);
|
|
275
|
+
const other = marker.kind === "pros" ? "cons" : "pros";
|
|
276
|
+
const before = parent.children[index - 1];
|
|
277
|
+
if ((marker.kind === "pros" || marker.kind === "cons") && isVerdict(before, other)) {
|
|
278
|
+
const start = before?.position?.start;
|
|
279
|
+
const pair = {
|
|
280
|
+
type: "blockquote",
|
|
281
|
+
data: marked({ form: "verdicts" }),
|
|
282
|
+
children: [before, form],
|
|
283
|
+
...start && position ? { position: {
|
|
284
|
+
start,
|
|
285
|
+
end: position.end
|
|
286
|
+
} } : {}
|
|
287
|
+
};
|
|
288
|
+
parent.children.splice(index - 1, 2, pair);
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
/** A node this module already shaped: the block visitor must not read it again. */
|
|
293
|
+
const isShapedContainer = (quote) => {
|
|
294
|
+
const form = formOf(quote);
|
|
295
|
+
return form !== void 0 && form.form !== "callout";
|
|
296
|
+
};
|
|
297
|
+
export { applyContainer, isShapedContainer, meaningful };
|
|
298
|
+
|
|
299
|
+
//# sourceMappingURL=remark-answer-containers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remark-answer-containers.js","names":[],"sources":["../../src/core/remark-answer-containers.ts"],"sourcesContent":["// remark: the answer dialect's CONTAINER forms (`./dialect`) — a quote\n// opened by `[!STEPS]`, `[!TABS]`, `[!STATS]`, `[!PROS]` or `[!CONS]` over ordinary Markdown.\n//\n// steps the quote gives way to its list, classed as a timeline; each item split into a\n// head (bold title + italic \"when\") and its text\n// tabs `#### Label` sections become one `answer-tab` each inside an `answer-tabs`, which\n// the renderer draws as tabs (`AnswerVariants.tsx`)\n// stats the list becomes a row of figures: `**value**` and its label\n// pros / cons an aside with a list of points; a `pros` right before a `cons` (or the other way\n// round) is paired into one two-column block\n// details the quote becomes a `<details>`, its title the `<summary>`\n// sources the list becomes the reply's references: each link with its site beside it\n//\n// Everything stays in mdast `data.hName`/`hProperties` — no raw HTML — and a container the\n// renderer already shaped carries an `hName`, which is how the block visitor knows to leave it\n// alone instead of reading it again as a callout or a quote.\nimport type {\n Blockquote,\n BlockContent,\n DefinitionContent,\n Link,\n ListItem,\n Paragraph,\n Parents,\n PhrasingContent,\n} from 'mdast'\nimport { parseContainerMarker, type ContainerKind } from '../dialect'\nimport { formOf, marked, type FormName } from './forms'\n\n/** A phrasing run without its whitespace-only text nodes. */\nexport const meaningful = (children: PhrasingContent[]) =>\n children.filter((child) => !(child.type === 'text' && child.value.trim() === ''))\n\n/** Separator text between a lead and what follows: ` — `, ` · `, `: ` or just a line break. */\nconst LEAD_SEPARATOR = /^[ \\t]*(?:[—–·:|-][ \\t]*)?(?:\\n[ \\t]*)?/\n\n/** Drop leading separator text (and a hard break) off a phrasing run. */\nfunction trimLead(nodes: PhrasingContent[]): PhrasingContent[] {\n const rest = [...nodes]\n while (rest[0]) {\n const lead = rest[0]\n if (lead.type === 'break') rest.shift()\n else if (lead.type === 'text') {\n const value = lead.value.replace(LEAD_SEPARATOR, '')\n if (value) {\n rest[0] = { ...lead, value }\n break\n }\n rest.shift()\n } else break\n }\n return rest\n}\n\n/** A paragraph rendered as a `div`: in a TIGHT list remark-rehype unwraps a `p` into bare text. */\nconst block = (form: FormName, children: PhrasingContent[]): Paragraph => ({\n type: 'paragraph',\n data: marked({ form }),\n children,\n})\n\n/** The container's title line, when the author wrote one after the marker. */\nconst titleLine = (form: FormName, title: string): Paragraph[] =>\n title\n ? [\n {\n type: 'paragraph',\n data: marked({ form }),\n children: [{ type: 'text', value: title }],\n },\n ]\n : []\n\n/**\n * Split an item's first paragraph at its bold lead: the lead (and an emphasis right after it,\n * the \"when\") into a head, the rest into a body. Null when the item does not open with bold.\n */\nfunction splitLead(\n item: ListItem,\n): { lead: PhrasingContent; meta?: PhrasingContent; rest: PhrasingContent[] } | null {\n const first = item.children[0]\n if (first?.type !== 'paragraph') return null\n const [lead, ...after] = trimLead(first.children)\n if (lead?.type !== 'strong') return null\n let rest = trimLead(after)\n let meta: PhrasingContent | undefined\n if (rest[0]?.type === 'emphasis') {\n meta = rest[0]\n rest = trimLead(rest.slice(1))\n }\n return { lead, ...(meta ? { meta } : {}), rest }\n}\n\n/** One step: head (title + when) and body. An item without a bold title keeps its dot only. */\nfunction shapeStep(item: ListItem): void {\n const split = splitLead(item)\n if (!split) return\n split.lead.data = marked({ form: 'step-title' })\n if (split.meta) split.meta.data = marked({ form: 'step-meta' })\n const head = block('step-head', split.meta ? [split.lead, split.meta] : [split.lead])\n const body = meaningful(split.rest).length > 0 ? [block('step-body', split.rest)] : []\n item.children.splice(0, 1, head, ...body)\n}\n\n/** One figure: its bold value and its label. An item without a bold value stays a plain line. */\nfunction shapeStat(item: ListItem): void {\n const split = splitLead(item)\n if (!split) return\n split.lead.data = marked({ form: 'stat-value' })\n const label: PhrasingContent = {\n // `emphasis` only as a phrasing carrier: hName makes it the label's span.\n type: 'emphasis',\n data: marked({ form: 'stat-label' }),\n children: split.meta ? [split.meta, ...split.rest] : split.rest,\n }\n item.children.splice(0, 1, block('stat', [split.lead, label]))\n}\n\n/** The site a reference points at, as a reader names it: `docs.example.com`. '' for a relative link. */\nfunction hostOf(url: string): string {\n const host = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^/?#@]*@)?([^/?#:]+)/i.exec(url)?.[1] ?? ''\n return host.replace(/^www\\./, '')\n}\n\nconst span = (form: FormName, children: PhrasingContent[]): PhrasingContent => ({\n // `emphasis` only as a phrasing carrier: the form says what it is.\n type: 'emphasis',\n data: marked({ form }),\n children,\n})\n\n/**\n * One reference: its link, the site quiet beside the title, then any note the author added. An\n * item that does not open with a link stays a plain line. The link itself keeps its own element\n * (and whatever the page's `a` does with it); only what is inside it is arranged.\n */\nfunction shapeSource(item: ListItem): void {\n const first = item.children[0]\n if (first?.type !== 'paragraph') return\n const [link, ...after] = trimLead(first.children)\n if (link?.type !== 'link') return\n const host = hostOf(link.url)\n // A bare URL is its own text: the site names it, and the address is not printed twice.\n const bare = link.children.length === 1 && labelText(link.children) === link.url\n ;(link as Link).children = [\n span('source-title', bare && host ? [{ type: 'text', value: host }] : link.children),\n ...(host && !bare ? [span('source-host', [{ type: 'text', value: host }])] : []),\n ]\n const note = trimLead(after)\n item.children.splice(\n 0,\n 1,\n block('source', [link, ...(meaningful(note).length > 0 ? [span('source-note', note)] : [])]),\n )\n}\n\n/** The quote's content after its marker paragraph. */\ntype QuoteContent = Blockquote['children']\n\n/** `[!TABS]`: each heading opens a tab, holding everything up to the next heading of its depth. */\nfunction tabsOf(content: QuoteContent): Blockquote[] | null {\n const first = content[0]\n if (first?.type !== 'heading') return null\n const depth = first.depth\n const tabs: Blockquote[] = []\n for (const node of content) {\n if (node.type === 'heading' && node.depth === depth) {\n const label = labelText(node.children).trim()\n tabs.push({\n type: 'blockquote',\n data: marked({ form: 'tab', label: label || `${tabs.length + 1}` }),\n children: [],\n })\n } else tabs.at(-1)!.children.push(node as BlockContent | DefinitionContent)\n }\n return tabs.length > 0 ? tabs : null\n}\n\n/** A bold/link/emphasis heading still has a name; formatting must not reduce it to \"1\". */\nfunction labelText(nodes: PhrasingContent[]): string {\n return nodes\n .map((node): string => {\n if ('children' in node) return labelText(node.children)\n if ('value' in node) return node.value\n if (node.type === 'image' || node.type === 'imageReference') return node.alt ?? ''\n return ''\n })\n .join('')\n}\n\nfunction verdict(kind: 'pros' | 'cons', title: string, list: QuoteContent[number]): Blockquote {\n if (list.type === 'list') list.data = marked({ form: 'verdict-list' }, list.data)\n return {\n type: 'blockquote',\n data: marked({ form: 'verdict', kind }),\n children: [...titleLine('verdict-title', title), list as BlockContent],\n }\n}\n\nconst isVerdict = (node: Parents['children'][number] | undefined, kind: 'pros' | 'cons') => {\n const form = node?.type === 'blockquote' ? formOf(node) : undefined\n return form?.form === 'verdict' && form.kind === kind\n}\n\n/** Each form's replacement nodes, or null when the content does not fit it (it stays a quote). */\nfunction shape(kind: ContainerKind, title: string, content: QuoteContent): QuoteContent | null {\n const list = content[0]\n switch (kind) {\n case 'steps':\n if (list?.type !== 'list') return null\n list.data = marked({ form: 'steps' }, list.data)\n for (const item of list.children) shapeStep(item)\n return [...titleLine('steps-title', title), list, ...content.slice(1)]\n case 'stats':\n if (list?.type !== 'list') return null\n list.data = marked({ form: 'stats' }, list.data)\n for (const item of list.children) shapeStat(item)\n return [...titleLine('stats-title', title), list, ...content.slice(1)]\n case 'tabs': {\n const tabs = tabsOf(content)\n if (!tabs) return null\n return [\n ...titleLine('tabs-title', title),\n {\n type: 'blockquote',\n data: marked({ form: 'tabs' }),\n children: tabs,\n },\n ]\n }\n case 'pros':\n case 'cons':\n if (list?.type !== 'list') return null\n return [verdict(kind, title, list), ...content.slice(1)]\n case 'sources':\n if (list?.type !== 'list') return null\n list.data = marked({ form: 'sources' }, list.data)\n for (const item of list.children) shapeSource(item)\n return [...titleLine('sources-title', title), list, ...content.slice(1)]\n case 'details':\n // Without a title there is no summary, and the browser prints its own, in its language.\n return [\n {\n type: 'blockquote',\n data: marked({ form: 'details' }),\n children: [\n ...(title\n ? [\n {\n type: 'paragraph',\n data: marked({ form: 'summary' }),\n children: [{ type: 'text', value: title }],\n } satisfies Paragraph,\n ]\n : []),\n ...content,\n ],\n },\n ]\n }\n}\n\n/**\n * The blocks a marker-only quote governs, read by grammar, not guessed: a list form takes the ONE\n * list right after it; tabs take the run of sections that starts with a heading right after it,\n * up to a shallower heading, a `---`, or the end of the reply. Anything else governs nothing.\n */\nfunction following(kind: ContainerKind, siblings: Parents['children']): QuoteContent {\n const first = siblings[0]\n // What folds is what the author quoted: nothing after the quote says where it would end.\n if (kind === 'details') return []\n if (kind !== 'tabs') return first?.type === 'list' ? [first] : []\n if (first?.type !== 'heading') return []\n const run: QuoteContent = []\n for (const node of siblings) {\n if (node.type === 'thematicBreak') break\n if (node.type === 'heading' && node.depth < first.depth) break\n run.push(node as QuoteContent[number])\n }\n return run\n}\n\n/**\n * A container quote, shaped in place. True when it was one: the caller revisits the index, so\n * the nodes that replaced the quote are walked (and their figures and callouts shaped) too.\n */\nexport function applyContainer(quote: Blockquote, index: number, parent: Parents): boolean {\n const first = quote.children[0]\n const lead = first?.type === 'paragraph' ? first.children[0] : undefined\n if (lead?.type !== 'text' || first?.type !== 'paragraph') return false\n const marker = parseContainerMarker(lead.value)\n if (!marker) return false\n if (marker.kind === 'details') {\n // Its content is prose as often as a list, and prose right under the marker is the marker's\n // own paragraph: the marker line comes off it, as a callout's does. A title line that carries\n // formatting ends its text node early; it is then content, not a cut-off title.\n const titled = marker.consumed < lead.value.length || first.children.length === 1\n if (titled) lead.value = lead.value.slice(marker.consumed)\n else {\n marker.title = ''\n lead.value = lead.value.slice(lead.value.indexOf(']') + 1).trimStart()\n }\n if (lead.value === '') first.children.shift()\n if (first.children[0]?.type === 'break') first.children.shift()\n if (first.children.length > 0) quote.children.unshift({ type: 'paragraph', children: [] })\n } else if (first.children.length !== 1 || lead.value.slice(marker.consumed).trim()) return false\n // A quote holding ONLY the marker takes the blocks right after it. That is how models write\n // these: `> [!TABS]` alone, then the headings unquoted — and Markdown ends a quote at a heading\n // or a list, so read literally the marker governed nothing and its content rendered as prose.\n const inside = quote.children.slice(1)\n const after = inside.length === 0 ? following(marker.kind, parent.children.slice(index + 1)) : []\n const nodes = shape(marker.kind, marker.title, inside.length > 0 ? inside : after)\n if (!nodes) return false\n // ONE node in the quote's place, spanning the form's whole source, marker included. A block's\n // actions copy their markdown by its position (`rehypeContentActions`), and the shaped list\n // kept only its own: `> [!STEPS]\\n> 1. …\\n> 2. …` was copied from `1.` on, so the board got\n // the first step bare, the rest still quoted and no marker — a broken list, not a timeline.\n const end = (after.at(-1) ?? quote).position?.end\n const position = quote.position && end ? { start: quote.position.start, end } : undefined\n const form: QuoteContent[number] =\n nodes.length === 1\n ? nodes[0]!\n : { type: 'blockquote', data: marked({ form: 'form' }), children: nodes }\n if (position) form.position = position\n parent.children.splice(index, 1 + after.length, form as Parents['children'][number])\n // A `pros` and a `cons` written one after the other are one decision: set them side by side.\n const other = marker.kind === 'pros' ? 'cons' : 'pros'\n const before = parent.children[index - 1]\n if ((marker.kind === 'pros' || marker.kind === 'cons') && isVerdict(before, other)) {\n const start = before?.position?.start\n const pair: Blockquote = {\n type: 'blockquote',\n data: marked({ form: 'verdicts' }),\n children: [before as Blockquote, form as Blockquote],\n ...(start && position ? { position: { start, end: position.end } } : {}),\n }\n parent.children.splice(index - 1, 2, pair as Parents['children'][number])\n }\n return true\n}\n\n/** A node this module already shaped: the block visitor must not read it again. */\nexport const isShapedContainer = (quote: Blockquote) => {\n const form = formOf(quote)\n return form !== undefined && form.form !== 'callout'\n}\n"],"mappings":";;;;AA8BA,MAAa,cAAc,aACzB,SAAS,QAAQ,UAAU,EAAE,MAAM,SAAS,UAAU,MAAM,MAAM,KAAK,MAAM,GAAG;;AAGlF,MAAM,iBAAiB;;AAGvB,SAAS,SAAS,OAA6C;CAC7D,MAAM,OAAO,CAAC,GAAG,KAAK;CACtB,OAAO,KAAK,IAAI;EACd,MAAM,OAAO,KAAK;EAClB,IAAI,KAAK,SAAS,SAAS,KAAK,MAAM;OACjC,IAAI,KAAK,SAAS,QAAQ;GAC7B,MAAM,QAAQ,KAAK,MAAM,QAAQ,gBAAgB,EAAE;GACnD,IAAI,OAAO;IACT,KAAK,KAAK;KAAE,GAAG;KAAM;IAAM;IAC3B;GACF;GACA,KAAK,MAAM;EACb,OAAO;CACT;CACA,OAAO;AACT;;AAGA,MAAM,SAAS,MAAgB,cAA4C;CACzE,MAAM;CACN,MAAM,OAAO,EAAE,KAAK,CAAC;CACrB;AACF;;AAGA,MAAM,aAAa,MAAgB,UACjC,QACI,CACE;CACE,MAAM;CACN,MAAM,OAAO,EAAE,KAAK,CAAC;CACrB,UAAU,CAAC;EAAE,MAAM;EAAQ,OAAO;CAAM,CAAC;AAC3C,CACF,IACA,CAAC;;;;;AAMP,SAAS,UACP,MACmF;CACnF,MAAM,QAAQ,KAAK,SAAS;CAC5B,IAAI,OAAO,SAAS,aAAa,OAAO;CACxC,MAAM,CAAC,MAAM,GAAG,SAAS,SAAS,MAAM,QAAQ;CAChD,IAAI,MAAM,SAAS,UAAU,OAAO;CACpC,IAAI,OAAO,SAAS,KAAK;CACzB,IAAI;CACJ,IAAI,KAAK,EAAE,EAAE,SAAS,YAAY;EAChC,OAAO,KAAK;EACZ,OAAO,SAAS,KAAK,MAAM,CAAC,CAAC;CAC/B;CACA,OAAO;EAAE;EAAM,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EAAI;CAAK;AACjD;;AAGA,SAAS,UAAU,MAAsB;CACvC,MAAM,QAAQ,UAAU,IAAI;CAC5B,IAAI,CAAC,OAAO;CACZ,MAAM,KAAK,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;CAC/C,IAAI,MAAM,MAAM,MAAM,KAAK,OAAO,OAAO,EAAE,MAAM,YAAY,CAAC;CAC9D,MAAM,OAAO,MAAM,aAAa,MAAM,OAAO,CAAC,MAAM,MAAM,MAAM,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC;CACpF,MAAM,OAAO,WAAW,MAAM,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,aAAa,MAAM,IAAI,CAAC,IAAI,CAAC;CACrF,KAAK,SAAS,OAAO,GAAG,GAAG,MAAM,GAAG,IAAI;AAC1C;;AAGA,SAAS,UAAU,MAAsB;CACvC,MAAM,QAAQ,UAAU,IAAI;CAC5B,IAAI,CAAC,OAAO;CACZ,MAAM,KAAK,OAAO,OAAO,EAAE,MAAM,aAAa,CAAC;CAC/C,MAAM,QAAyB;EAE7B,MAAM;EACN,MAAM,OAAO,EAAE,MAAM,aAAa,CAAC;EACnC,UAAU,MAAM,OAAO,CAAC,MAAM,MAAM,GAAG,MAAM,IAAI,IAAI,MAAM;CAC7D;CACA,KAAK,SAAS,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAC/D;;AAGA,SAAS,OAAO,KAAqB;CAEnC,QADa,mDAAmD,KAAK,GAAG,CAAC,GAAG,MAAM,GAAA,CACtE,QAAQ,UAAU,EAAE;AAClC;AAEA,MAAM,QAAQ,MAAgB,cAAkD;CAE9E,MAAM;CACN,MAAM,OAAO,EAAE,KAAK,CAAC;CACrB;AACF;;;;;;AAOA,SAAS,YAAY,MAAsB;CACzC,MAAM,QAAQ,KAAK,SAAS;CAC5B,IAAI,OAAO,SAAS,aAAa;CACjC,MAAM,CAAC,MAAM,GAAG,SAAS,SAAS,MAAM,QAAQ;CAChD,IAAI,MAAM,SAAS,QAAQ;CAC3B,MAAM,OAAO,OAAO,KAAK,GAAG;CAE5B,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,UAAU,KAAK,QAAQ,MAAM,KAAK;CAC5E,KAAe,WAAW,CACzB,KAAK,gBAAgB,QAAQ,OAAO,CAAC;EAAE,MAAM;EAAQ,OAAO;CAAK,CAAC,IAAI,KAAK,QAAQ,GACnF,GAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,eAAe,CAAC;EAAE,MAAM;EAAQ,OAAO;CAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAChF;CACA,MAAM,OAAO,SAAS,KAAK;CAC3B,KAAK,SAAS,OACZ,GACA,GACA,MAAM,UAAU,CAAC,MAAM,GAAI,WAAW,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,KAAK,eAAe,IAAI,CAAC,IAAI,CAAC,CAAE,CAAC,CAC7F;AACF;;AAMA,SAAS,OAAO,SAA4C;CAC1D,MAAM,QAAQ,QAAQ;CACtB,IAAI,OAAO,SAAS,WAAW,OAAO;CACtC,MAAM,QAAQ,MAAM;CACpB,MAAM,OAAqB,CAAC;CAC5B,KAAK,MAAM,QAAQ,SACjB,IAAI,KAAK,SAAS,aAAa,KAAK,UAAU,OAAO;EACnD,MAAM,QAAQ,UAAU,KAAK,QAAQ,CAAC,CAAC,KAAK;EAC5C,KAAK,KAAK;GACR,MAAM;GACN,MAAM,OAAO;IAAE,MAAM;IAAO,OAAO,SAAS,GAAG,KAAK,SAAS;GAAI,CAAC;GAClE,UAAU,CAAC;EACb,CAAC;CACH,OAAO,KAAK,GAAG,EAAE,CAAC,CAAE,SAAS,KAAK,IAAwC;CAE5E,OAAO,KAAK,SAAS,IAAI,OAAO;AAClC;;AAGA,SAAS,UAAU,OAAkC;CACnD,OAAO,MACJ,KAAK,SAAiB;EACrB,IAAI,cAAc,MAAM,OAAO,UAAU,KAAK,QAAQ;EACtD,IAAI,WAAW,MAAM,OAAO,KAAK;EACjC,IAAI,KAAK,SAAS,WAAW,KAAK,SAAS,kBAAkB,OAAO,KAAK,OAAO;EAChF,OAAO;CACT,CAAC,CAAC,CACD,KAAK,EAAE;AACZ;AAEA,SAAS,QAAQ,MAAuB,OAAe,MAAwC;CAC7F,IAAI,KAAK,SAAS,QAAQ,KAAK,OAAO,OAAO,EAAE,MAAM,eAAe,GAAG,KAAK,IAAI;CAChF,OAAO;EACL,MAAM;EACN,MAAM,OAAO;GAAE,MAAM;GAAW;EAAK,CAAC;EACtC,UAAU,CAAC,GAAG,UAAU,iBAAiB,KAAK,GAAG,IAAoB;CACvE;AACF;AAEA,MAAM,aAAa,MAA+C,SAA0B;CAC1F,MAAM,OAAO,MAAM,SAAS,eAAe,OAAO,IAAI,IAAI,KAAA;CAC1D,OAAO,MAAM,SAAS,aAAa,KAAK,SAAS;AACnD;;AAGA,SAAS,MAAM,MAAqB,OAAe,SAA4C;CAC7F,MAAM,OAAO,QAAQ;CACrB,QAAQ,MAAR;EACE,KAAK;GACH,IAAI,MAAM,SAAS,QAAQ,OAAO;GAClC,KAAK,OAAO,OAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI;GAC/C,KAAK,MAAM,QAAQ,KAAK,UAAU,UAAU,IAAI;GAChD,OAAO;IAAC,GAAG,UAAU,eAAe,KAAK;IAAG;IAAM,GAAG,QAAQ,MAAM,CAAC;GAAC;EACvE,KAAK;GACH,IAAI,MAAM,SAAS,QAAQ,OAAO;GAClC,KAAK,OAAO,OAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI;GAC/C,KAAK,MAAM,QAAQ,KAAK,UAAU,UAAU,IAAI;GAChD,OAAO;IAAC,GAAG,UAAU,eAAe,KAAK;IAAG;IAAM,GAAG,QAAQ,MAAM,CAAC;GAAC;EACvE,KAAK,QAAQ;GACX,MAAM,OAAO,OAAO,OAAO;GAC3B,IAAI,CAAC,MAAM,OAAO;GAClB,OAAO,CACL,GAAG,UAAU,cAAc,KAAK,GAChC;IACE,MAAM;IACN,MAAM,OAAO,EAAE,MAAM,OAAO,CAAC;IAC7B,UAAU;GACZ,CACF;EACF;EACA,KAAK;EACL,KAAK;GACH,IAAI,MAAM,SAAS,QAAQ,OAAO;GAClC,OAAO,CAAC,QAAQ,MAAM,OAAO,IAAI,GAAG,GAAG,QAAQ,MAAM,CAAC,CAAC;EACzD,KAAK;GACH,IAAI,MAAM,SAAS,QAAQ,OAAO;GAClC,KAAK,OAAO,OAAO,EAAE,MAAM,UAAU,GAAG,KAAK,IAAI;GACjD,KAAK,MAAM,QAAQ,KAAK,UAAU,YAAY,IAAI;GAClD,OAAO;IAAC,GAAG,UAAU,iBAAiB,KAAK;IAAG;IAAM,GAAG,QAAQ,MAAM,CAAC;GAAC;EACzE,KAAK,WAEH,OAAO,CACL;GACE,MAAM;GACN,MAAM,OAAO,EAAE,MAAM,UAAU,CAAC;GAChC,UAAU,CACR,GAAI,QACA,CACE;IACE,MAAM;IACN,MAAM,OAAO,EAAE,MAAM,UAAU,CAAC;IAChC,UAAU,CAAC;KAAE,MAAM;KAAQ,OAAO;IAAM,CAAC;GAC3C,CACF,IACA,CAAC,GACL,GAAG,OACL;EACF,CACF;CACJ;AACF;;;;;;AAOA,SAAS,UAAU,MAAqB,UAA6C;CACnF,MAAM,QAAQ,SAAS;CAEvB,IAAI,SAAS,WAAW,OAAO,CAAC;CAChC,IAAI,SAAS,QAAQ,OAAO,OAAO,SAAS,SAAS,CAAC,KAAK,IAAI,CAAC;CAChE,IAAI,OAAO,SAAS,WAAW,OAAO,CAAC;CACvC,MAAM,MAAoB,CAAC;CAC3B,KAAK,MAAM,QAAQ,UAAU;EAC3B,IAAI,KAAK,SAAS,iBAAiB;EACnC,IAAI,KAAK,SAAS,aAAa,KAAK,QAAQ,MAAM,OAAO;EACzD,IAAI,KAAK,IAA4B;CACvC;CACA,OAAO;AACT;;;;;AAMA,SAAgB,eAAe,OAAmB,OAAe,QAA0B;CACzF,MAAM,QAAQ,MAAM,SAAS;CAC7B,MAAM,OAAO,OAAO,SAAS,cAAc,MAAM,SAAS,KAAK,KAAA;CAC/D,IAAI,MAAM,SAAS,UAAU,OAAO,SAAS,aAAa,OAAO;CACjE,MAAM,SAAS,qBAAqB,KAAK,KAAK;CAC9C,IAAI,CAAC,QAAQ,OAAO;CACpB,IAAI,OAAO,SAAS,WAAW;EAK7B,IADe,OAAO,WAAW,KAAK,MAAM,UAAU,MAAM,SAAS,WAAW,GACpE,KAAK,QAAQ,KAAK,MAAM,MAAM,OAAO,QAAQ;OACpD;GACH,OAAO,QAAQ;GACf,KAAK,QAAQ,KAAK,MAAM,MAAM,KAAK,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU;EACvE;EACA,IAAI,KAAK,UAAU,IAAI,MAAM,SAAS,MAAM;EAC5C,IAAI,MAAM,SAAS,EAAE,EAAE,SAAS,SAAS,MAAM,SAAS,MAAM;EAC9D,IAAI,MAAM,SAAS,SAAS,GAAG,MAAM,SAAS,QAAQ;GAAE,MAAM;GAAa,UAAU,CAAC;EAAE,CAAC;CAC3F,OAAO,IAAI,MAAM,SAAS,WAAW,KAAK,KAAK,MAAM,MAAM,OAAO,QAAQ,CAAC,CAAC,KAAK,GAAG,OAAO;CAI3F,MAAM,SAAS,MAAM,SAAS,MAAM,CAAC;CACrC,MAAM,QAAQ,OAAO,WAAW,IAAI,UAAU,OAAO,MAAM,OAAO,SAAS,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC;CAChG,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,SAAS,IAAI,SAAS,KAAK;CACjF,IAAI,CAAC,OAAO,OAAO;CAKnB,MAAM,OAAO,MAAM,GAAG,EAAE,KAAK,MAAA,CAAO,UAAU;CAC9C,MAAM,WAAW,MAAM,YAAY,MAAM;EAAE,OAAO,MAAM,SAAS;EAAO;CAAI,IAAI,KAAA;CAChF,MAAM,OACJ,MAAM,WAAW,IACb,MAAM,KACN;EAAE,MAAM;EAAc,MAAM,OAAO,EAAE,MAAM,OAAO,CAAC;EAAG,UAAU;CAAM;CAC5E,IAAI,UAAU,KAAK,WAAW;CAC9B,OAAO,SAAS,OAAO,OAAO,IAAI,MAAM,QAAQ,IAAmC;CAEnF,MAAM,QAAQ,OAAO,SAAS,SAAS,SAAS;CAChD,MAAM,SAAS,OAAO,SAAS,QAAQ;CACvC,KAAK,OAAO,SAAS,UAAU,OAAO,SAAS,WAAW,UAAU,QAAQ,KAAK,GAAG;EAClF,MAAM,QAAQ,QAAQ,UAAU;EAChC,MAAM,OAAmB;GACvB,MAAM;GACN,MAAM,OAAO,EAAE,MAAM,WAAW,CAAC;GACjC,UAAU,CAAC,QAAsB,IAAkB;GACnD,GAAI,SAAS,WAAW,EAAE,UAAU;IAAE;IAAO,KAAK,SAAS;GAAI,EAAE,IAAI,CAAC;EACxE;EACA,OAAO,SAAS,OAAO,QAAQ,GAAG,GAAG,IAAmC;CAC1E;CACA,OAAO;AACT;;AAGA,MAAa,qBAAqB,UAAsB;CACtD,MAAM,OAAO,OAAO,KAAK;CACzB,OAAO,SAAS,KAAA,KAAa,KAAK,SAAS;AAC7C"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dialect's names added to a sanitizer's schema, for a unified pipeline that runs
|
|
3
|
+
* `rehype-sanitize` after the dialect's plugin: without them the sanitizer takes every callout,
|
|
4
|
+
* tab set and step list for content it does not know and strips it.
|
|
5
|
+
*
|
|
6
|
+
* import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
|
|
7
|
+
* .use(rehypeSanitize, sanitizeSchemaFor(defaultSchema))
|
|
8
|
+
*
|
|
9
|
+
* Only the dialect's own names pass: `fd-` classes, the forms' data attributes and the two tab
|
|
10
|
+
* elements, with the presentational attributes a README relies on (alignment, image sizes).
|
|
11
|
+
*/
|
|
12
|
+
type Rule = string | [string, ...Array<string | number | boolean | RegExp | null | undefined>];
|
|
13
|
+
interface Schema {
|
|
14
|
+
tagNames?: readonly string[] | null;
|
|
15
|
+
attributes?: Readonly<Record<string, readonly Rule[]>> | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function sanitizeSchemaFor<S extends Schema>(schema: S): S;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/core/sanitize-schema.ts
|
|
2
|
+
const DIALECT_CLASS = /^fd-/;
|
|
3
|
+
function sanitizeSchemaFor(schema) {
|
|
4
|
+
const defaults = schema.attributes ?? {};
|
|
5
|
+
const lists = {};
|
|
6
|
+
for (const tag of [
|
|
7
|
+
"ol",
|
|
8
|
+
"ul",
|
|
9
|
+
"li"
|
|
10
|
+
]) lists[tag] = [...(defaults[tag] ?? []).filter((rule) => !(Array.isArray(rule) && rule[0] === "className")), [
|
|
11
|
+
"className",
|
|
12
|
+
"contains-task-list",
|
|
13
|
+
"task-list-item",
|
|
14
|
+
DIALECT_CLASS
|
|
15
|
+
]];
|
|
16
|
+
return {
|
|
17
|
+
...schema,
|
|
18
|
+
tagNames: [...schema.tagNames ?? [], ..."div span picture source aside figure figcaption footer answer-tabs answer-tab".split(" ")],
|
|
19
|
+
attributes: {
|
|
20
|
+
...defaults,
|
|
21
|
+
"*": [
|
|
22
|
+
...defaults["*"] ?? [],
|
|
23
|
+
"align",
|
|
24
|
+
"width",
|
|
25
|
+
"height",
|
|
26
|
+
["className", DIALECT_CLASS],
|
|
27
|
+
"dataCallout",
|
|
28
|
+
"dataCalloutTitle",
|
|
29
|
+
"dataVerdict",
|
|
30
|
+
"dataLabel",
|
|
31
|
+
"dataTitle"
|
|
32
|
+
],
|
|
33
|
+
a: [
|
|
34
|
+
...defaults.a ?? [],
|
|
35
|
+
"target",
|
|
36
|
+
"rel"
|
|
37
|
+
],
|
|
38
|
+
code: [[
|
|
39
|
+
"className",
|
|
40
|
+
/^language-./,
|
|
41
|
+
"math-inline",
|
|
42
|
+
"math-display"
|
|
43
|
+
]],
|
|
44
|
+
img: [
|
|
45
|
+
...defaults.img ?? [],
|
|
46
|
+
"width",
|
|
47
|
+
"height",
|
|
48
|
+
"align"
|
|
49
|
+
],
|
|
50
|
+
source: [
|
|
51
|
+
"srcSet",
|
|
52
|
+
"srcset",
|
|
53
|
+
"media",
|
|
54
|
+
"type"
|
|
55
|
+
],
|
|
56
|
+
...lists
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export { sanitizeSchemaFor };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=sanitize-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-schema.js","names":[],"sources":["../../src/core/sanitize-schema.ts"],"sourcesContent":["/**\n * The dialect's names added to a sanitizer's schema, for a unified pipeline that runs\n * `rehype-sanitize` after the dialect's plugin: without them the sanitizer takes every callout,\n * tab set and step list for content it does not know and strips it.\n *\n * import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'\n * .use(rehypeSanitize, sanitizeSchemaFor(defaultSchema))\n *\n * Only the dialect's own names pass: `fd-` classes, the forms' data attributes and the two tab\n * elements, with the presentational attributes a README relies on (alignment, image sizes).\n */\ntype Rule = string | [string, ...Array<string | number | boolean | RegExp | null | undefined>]\n\ninterface Schema {\n tagNames?: readonly string[] | null\n attributes?: Readonly<Record<string, readonly Rule[]>> | null\n}\n\nconst DIALECT_CLASS = /^fd-/\n\nexport function sanitizeSchemaFor<S extends Schema>(schema: S): S {\n const defaults = schema.attributes ?? {}\n const lists: Record<string, Rule[]> = {}\n for (const tag of ['ol', 'ul', 'li']) {\n // GitHub's schema names `className` for these three itself (the task-list classes), which\n // overrides the wildcard rule, so the dialect's classes are added to theirs.\n lists[tag] = [\n ...(defaults[tag] ?? []).filter((rule) => !(Array.isArray(rule) && rule[0] === 'className')),\n ['className', 'contains-task-list', 'task-list-item', DIALECT_CLASS],\n ]\n }\n return {\n ...schema,\n tagNames: [\n ...(schema.tagNames ?? []),\n ...'div span picture source aside figure figcaption footer answer-tabs answer-tab'.split(' '),\n ],\n attributes: {\n ...defaults,\n '*': [\n ...(defaults['*'] ?? []),\n 'align',\n 'width',\n 'height',\n ['className', DIALECT_CLASS],\n 'dataCallout',\n 'dataCalloutTitle',\n 'dataVerdict',\n 'dataLabel',\n 'dataTitle',\n ],\n a: [...(defaults.a ?? []), 'target', 'rel'],\n code: [['className', /^language-./, 'math-inline', 'math-display']],\n img: [...(defaults.img ?? []), 'width', 'height', 'align'],\n source: ['srcSet', 'srcset', 'media', 'type'],\n ...lists,\n },\n }\n}\n"],"mappings":";AAkBA,MAAM,gBAAgB;AAEtB,SAAgB,kBAAoC,QAAc;CAChE,MAAM,WAAW,OAAO,cAAc,CAAC;CACvC,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,OAAO;EAAC;EAAM;EAAM;CAAI,GAGjC,MAAM,OAAO,CACX,IAAI,SAAS,QAAQ,CAAC,EAAA,CAAG,QAAQ,SAAS,EAAE,MAAM,QAAQ,IAAI,KAAK,KAAK,OAAO,YAAY,GAC3F;EAAC;EAAa;EAAsB;EAAkB;CAAa,CACrE;CAEF,OAAO;EACL,GAAG;EACH,UAAU,CACR,GAAI,OAAO,YAAY,CAAC,GACxB,GAAG,gFAAgF,MAAM,GAAG,CAC9F;EACA,YAAY;GACV,GAAG;GACH,KAAK;IACH,GAAI,SAAS,QAAQ,CAAC;IACtB;IACA;IACA;IACA,CAAC,aAAa,aAAa;IAC3B;IACA;IACA;IACA;IACA;GACF;GACA,GAAG;IAAC,GAAI,SAAS,KAAK,CAAC;IAAI;IAAU;GAAK;GAC1C,MAAM,CAAC;IAAC;IAAa;IAAe;IAAe;GAAc,CAAC;GAClE,KAAK;IAAC,GAAI,SAAS,OAAO,CAAC;IAAI;IAAS;IAAU;GAAO;GACzD,QAAQ;IAAC;IAAU;IAAU;IAAS;GAAM;GAC5C,GAAG;EACL;CACF;AACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the prose holds a formula, so KaTeX is worth loading: a `$$…$$` pair, or a single-dollar
|
|
3
|
+
* pair by the parser's rule (`math-delimiters.ts`). Prices, shell variables and code do not count.
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasMathSyntax(markdown: string): boolean;
|
|
6
|
+
/** Raw HTML only matters in prose. Tags shown inside fenced/indented/inline code
|
|
7
|
+
* stay literal, so they must not enable the full rehype-raw + sanitize pipeline. */
|
|
8
|
+
export declare function hasHtmlSyntax(markdown: string): boolean;
|