@dogsbay/format-dogsbay-md 0.2.0-beta.127 → 0.2.0-beta.129
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/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +3 -1
- package/dist/container-rule.d.ts +5 -9
- package/dist/container-rule.js +14 -26
- package/dist/diagnostics.d.ts +2 -5
- package/dist/diagnostics.js +128 -12
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/parse-attrs.d.ts +9 -0
- package/dist/parse-attrs.js +28 -0
- package/dist/plugin-block-leaf.js +3 -15
- package/dist/plugin-containers.d.ts +13 -0
- package/dist/plugin-containers.js +17 -4
- package/dist/plugin-inline-directives.js +3 -15
- package/dist/renderers/diagrams.d.ts +4 -0
- package/dist/renderers/diagrams.js +23 -1
- package/dist/renderers/pele.d.ts +13 -0
- package/dist/renderers/pele.js +42 -0
- package/dist/serialize.js +3 -2
- package/package.json +7 -6
package/dist/browser.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export { dogsbayMdToTree, parseInline, containerOpenBefore, type ParseResult, type ParseOptions } from "./parse.js";
|
|
15
15
|
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
16
|
-
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
|
16
|
+
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
17
17
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
18
18
|
export type { SerializeOptions } from "./types.js";
|
|
19
19
|
export { inlineToDogsbayMd } from "./inline.js";
|
package/dist/browser.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export { dogsbayMdToTree, parseInline, containerOpenBefore } from "./parse.js";
|
|
15
15
|
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
16
16
|
// The `:::name` blocks the parser knows (an editor completes names from it)
|
|
17
|
-
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
|
17
|
+
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
18
18
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
19
19
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
20
20
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
|
package/dist/capabilities.js
CHANGED
|
@@ -30,7 +30,7 @@ export const DOGSBAY_MD_CAPABILITIES = [
|
|
|
30
30
|
rendersAs: "a ```mermaid fence with the diagram source",
|
|
31
31
|
note: "The source round-trips (it re-parses as a code fence, which the build turns back into a diagram); a pre-rendered SVG is not kept.",
|
|
32
32
|
},
|
|
33
|
-
{ feature: "youtube", support: "full", rendersAs: "
|
|
33
|
+
{ feature: "youtube", support: "full", rendersAs: "`::youtube{#id title=\"…\"}`" },
|
|
34
34
|
// ── Components ───────────────────────────────────────────
|
|
35
35
|
{ feature: "callout", support: "full", rendersAs: "GitHub alert `> [!NOTE]`; `:::type[Title]{…}` for a custom title or a non-GitHub type" },
|
|
36
36
|
{ feature: "details", support: "full", rendersAs: "`:::details{title=\"…\"}`" },
|
package/dist/cli.js
CHANGED
|
@@ -131,11 +131,13 @@ async function importDogsbayMd(source, opts) {
|
|
|
131
131
|
// (mermaid-cli) isn't installed the code block is left as-is and
|
|
132
132
|
// a one-line warning is printed. See plans/mermaid-dogsbay-md.md.
|
|
133
133
|
const diagramsOn = opts.diagrams !== false;
|
|
134
|
+
// `content.diagrams: pele` draws them with Pele; `true` (the default) with mmdc
|
|
135
|
+
const diagramEngine = opts.diagrams === "pele" ? "pele" : "mermaid-cli";
|
|
134
136
|
for (const { filePath, slug, sourcePath } of files) {
|
|
135
137
|
const content = readFileSync(filePath, "utf-8");
|
|
136
138
|
const { tree, frontmatter } = dogsbayMdToTree(content);
|
|
137
139
|
if (diagramsOn) {
|
|
138
|
-
await renderDiagrams(tree);
|
|
140
|
+
await renderDiagrams(tree, { engine: diagramEngine });
|
|
139
141
|
}
|
|
140
142
|
const title = String(frontmatter.title
|
|
141
143
|
?? prettifyName(basename(slug)));
|
package/dist/container-rule.d.ts
CHANGED
|
@@ -58,8 +58,9 @@ import type MarkdownIt from "markdown-it";
|
|
|
58
58
|
* Only a known directive name opens a block in the scan. An unknown `:::word` is a block only if
|
|
59
59
|
* it has a closer of its own and is otherwise text (markUnclosedGenericContainers), and the scan
|
|
60
60
|
* cannot know which, so it does not push one: an unknown block nested at the same colon count
|
|
61
|
-
* as its parent still needs more colons on the parent.
|
|
62
|
-
*
|
|
61
|
+
* as its parent still needs more colons on the parent. `:::` always opens a block that needs a
|
|
62
|
+
* closer; a directive with no body is a leaf, `::youtube{#id}` (plugin-block-leaf.ts), so no
|
|
63
|
+
* opener depends on the line after it.
|
|
63
64
|
*
|
|
64
65
|
* VERIFY. The scan reads raw lines, and markdown-it may not read an opener there as one: in raw
|
|
65
66
|
* HTML, a `$$` math block, a fence that never closes. Where that is depends on where the blocks
|
|
@@ -77,11 +78,6 @@ export interface ContainerOptions {
|
|
|
77
78
|
validate: (params: string, markup: string) => boolean;
|
|
78
79
|
render?: (tokens: unknown[], idx: number) => string;
|
|
79
80
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
* A `leaf` is written with no body (`:::youtube{#id}`) and often with no closer.
|
|
83
|
-
*/
|
|
84
|
-
export declare function registerOpener(name: string, options?: {
|
|
85
|
-
leaf?: boolean;
|
|
86
|
-
}): void;
|
|
81
|
+
/** Make `name` an opener for the nesting scan (the known directives; not the generic container). */
|
|
82
|
+
export declare function registerOpener(name: string): void;
|
|
87
83
|
export declare function containerRule(md: MarkdownIt, name: string, options: ContainerOptions): void;
|
package/dist/container-rule.js
CHANGED
|
@@ -52,31 +52,16 @@ function closesFence(line, markup, after) {
|
|
|
52
52
|
const m = /^[ \t>]*(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
53
53
|
return m !== null && m[1][0] === markup[0] && m[1].length >= markup.length;
|
|
54
54
|
}
|
|
55
|
-
/** The directive names the scan treats as openers (plugin-containers registers them)
|
|
56
|
-
const KNOWN_OPENERS = new
|
|
57
|
-
/**
|
|
58
|
-
function
|
|
55
|
+
/** The directive names the scan treats as openers (plugin-containers registers them). */
|
|
56
|
+
const KNOWN_OPENERS = new Set();
|
|
57
|
+
/** Whether the text after an opener's colons starts with a known directive name. */
|
|
58
|
+
function isKnownOpener(rest) {
|
|
59
59
|
const m = /^\s*([A-Za-z][\w-]*)(?=$|[\s{[])/.exec(rest);
|
|
60
|
-
return m
|
|
60
|
+
return m !== null && KNOWN_OPENERS.has(m[1]);
|
|
61
61
|
}
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*/
|
|
66
|
-
export function registerOpener(name, options = {}) {
|
|
67
|
-
KNOWN_OPENERS.set(name, { leaf: options.leaf === true });
|
|
68
|
-
}
|
|
69
|
-
/** Whether the next non-blank line after `line`, before `endLine`, is a closer for a block opened with `colons`. */
|
|
70
|
-
function closerFollows(state, line, endLine, colons) {
|
|
71
|
-
for (let l = line + 1; l < endLine; l++) {
|
|
72
|
-
const start = state.bMarks[l] + state.tShift[l];
|
|
73
|
-
const max = state.eMarks[l];
|
|
74
|
-
if (start >= max)
|
|
75
|
-
continue;
|
|
76
|
-
const m = /^(:{3,})(.*)$/.exec(state.src.slice(start, max));
|
|
77
|
-
return m !== null && m[1].length >= colons && CLOSER_TAIL.test(m[2]);
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
62
|
+
/** Make `name` an opener for the nesting scan (the known directives; not the generic container). */
|
|
63
|
+
export function registerOpener(name) {
|
|
64
|
+
KNOWN_OPENERS.add(name);
|
|
80
65
|
}
|
|
81
66
|
/** Tokens whose lines are not block openers: an opener the scan pushed inside one is wrong (VERIFY). */
|
|
82
67
|
const READ_AS_OTHER = new Set(["paragraph_open", "heading_open", "html_block", "fence", "code_block", "math_block", "hr", "table_open"]);
|
|
@@ -177,8 +162,11 @@ export function containerRule(md, name, options) {
|
|
|
177
162
|
if (wrong.length === 0 && missed.length === 0)
|
|
178
163
|
break;
|
|
179
164
|
const budget = retryBudget(state);
|
|
180
|
-
if (budget.lines < scan.nextLine - startLine)
|
|
165
|
+
if (budget.lines < scan.nextLine - startLine) {
|
|
166
|
+
// Out of budget: this reading stands, with an opener known to be wrong; say so (diagnostics)
|
|
167
|
+
open.meta = { ...(open.meta ?? {}), unverified: true };
|
|
181
168
|
break;
|
|
169
|
+
}
|
|
182
170
|
budget.lines -= scan.nextLine - startLine;
|
|
183
171
|
for (const l of wrong)
|
|
184
172
|
notOpeners.add(l);
|
|
@@ -238,7 +226,7 @@ export function containerRule(md, name, options) {
|
|
|
238
226
|
continue;
|
|
239
227
|
const rest = state.src.slice(pos, max);
|
|
240
228
|
const tail = CLOSER_TAIL.exec(rest);
|
|
241
|
-
const opener = tail
|
|
229
|
+
const opener = !tail && isKnownOpener(rest);
|
|
242
230
|
if (!tail && !opener)
|
|
243
231
|
continue;
|
|
244
232
|
// a `:::` in a code example is code
|
|
@@ -248,7 +236,7 @@ export function containerRule(md, name, options) {
|
|
|
248
236
|
continue;
|
|
249
237
|
}
|
|
250
238
|
if (opener) {
|
|
251
|
-
if (notOpeners.has(nextLine)
|
|
239
|
+
if (notOpeners.has(nextLine))
|
|
252
240
|
continue;
|
|
253
241
|
const least = Math.min(colons, inner.length > 0 ? inner[inner.length - 1].least : colons);
|
|
254
242
|
inner.push({ colons, indent: state.sCount[nextLine], least });
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface ParseDiagnostic {
|
|
|
2
2
|
/** 1-based line in the source (front matter counted). */
|
|
3
3
|
line: number;
|
|
4
4
|
/** Stable kind, for filtering. */
|
|
5
|
-
code: "unclosed-container" | "stray-closer" | "unknown-container" | "unknown-inline" | "block-directive-inline" | "bad-attributes" | "closer-mismatch" | "dropped-attributes" | "missing-label";
|
|
5
|
+
code: "unclosed-container" | "stray-closer" | "unknown-container" | "unknown-inline" | "block-directive-inline" | "bad-attributes" | "closer-mismatch" | "dropped-attributes" | "missing-label" | "leaf-as-block" | "nesting-unchecked";
|
|
6
6
|
message: string;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
@@ -32,6 +32,7 @@ type Token = {
|
|
|
32
32
|
unclosed?: boolean;
|
|
33
33
|
skippedCloser?: number;
|
|
34
34
|
legacyCloser?: number;
|
|
35
|
+
unverified?: boolean;
|
|
35
36
|
} | null;
|
|
36
37
|
};
|
|
37
38
|
/**
|
|
@@ -40,9 +41,5 @@ type Token = {
|
|
|
40
41
|
* missing brace or an unterminated quote loses attributes silently.
|
|
41
42
|
*/
|
|
42
43
|
export declare function attributeProblem(block: string): string | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* Diagnostics for a token stream. `lines` is the parsed text split at `\n`;
|
|
45
|
-
* `lineOf` maps a 0-based line of it to a 1-based source line.
|
|
46
|
-
*/
|
|
47
44
|
export declare function collectDiagnostics(tokens: Token[], lines: string[], lineOf: (line0: number) => number | undefined): ParseDiagnostic[];
|
|
48
45
|
export {};
|
package/dist/diagnostics.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* here: the parser passes any leaf through as a node of that type, and only a
|
|
9
9
|
* renderer knows whether it can draw it (render-html reports the ones it can't).
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { BLOCK_LEAF_DIRECTIVES, GENERIC_CONTAINER } from "./plugin-containers.js";
|
|
12
12
|
import { PRECEDES_TEXT } from "./plugin-inline-directives.js";
|
|
13
13
|
/**
|
|
14
14
|
* Whether a source line closes a container opened with `colons` colons. The
|
|
@@ -24,10 +24,54 @@ export const closesContainer = (line, colons) => line !== undefined && new RegEx
|
|
|
24
24
|
export function closerLabel(comment) {
|
|
25
25
|
return /^<!--\s*(?:end\s+|\/)?:*([A-Za-z][\w-]*)\s*-->$/.exec(comment.trim())?.[1];
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The line of an attribute block a list or list item was given, so the diagnostic points at the
|
|
29
|
+
* braces rather than the first item. In `from`..`to`, the last line that is only `{…}`, else the
|
|
30
|
+
* last that ends with one (code lines skipped). With `after` (a list's block may follow it, past
|
|
31
|
+
* blank lines), only when the list has none: then a line that is only `{…}`. Else `from`.
|
|
32
|
+
*/
|
|
33
|
+
function braceLine(lines, code, from, to, after = false) {
|
|
34
|
+
const only = (l) => !code.has(l) && /^[\s>]*\{[^{}]*\}\s*$/.test(lines[l] ?? "");
|
|
35
|
+
const ends = (l) => !code.has(l) && /\{[^{}]*\}\s*$/.test(lines[l] ?? "");
|
|
36
|
+
const last = Math.min(to, lines.length - 1);
|
|
37
|
+
for (let l = last; l >= from; l--)
|
|
38
|
+
if (only(l))
|
|
39
|
+
return l;
|
|
40
|
+
for (let l = last; l >= from; l--)
|
|
41
|
+
if (ends(l))
|
|
42
|
+
return l;
|
|
43
|
+
if (after) {
|
|
44
|
+
let l = to;
|
|
45
|
+
while (l < lines.length && (lines[l] ?? "").trim() === "")
|
|
46
|
+
l++;
|
|
47
|
+
if (only(l))
|
|
48
|
+
return l;
|
|
49
|
+
}
|
|
50
|
+
return from;
|
|
51
|
+
}
|
|
52
|
+
/** A line with its inline code spans and `<…>` (raw HTML, autolinks) blanked, keeping offsets. */
|
|
53
|
+
function blankCodeAndTags(line) {
|
|
54
|
+
return line.replace(/(`+)[\s\S]*?\1|<[^<>]*>/g, (c) => " ".repeat(c.length));
|
|
55
|
+
}
|
|
56
|
+
/** Lines inside fenced or indented code, from the tokens. */
|
|
57
|
+
function codeLineSet(tokens) {
|
|
58
|
+
const out = new Set();
|
|
59
|
+
const walk = (list) => {
|
|
60
|
+
for (const t of list) {
|
|
61
|
+
if ((t.type === "fence" || t.type === "code_block") && t.map)
|
|
62
|
+
for (let l = t.map[0]; l < t.map[1]; l++)
|
|
63
|
+
out.add(l);
|
|
64
|
+
if (t.children)
|
|
65
|
+
walk(t.children);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(tokens);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
27
71
|
/** The inline directives the parser turns into nodes (parse.ts inlineDirectiveToNode); any other name stays literal text. */
|
|
28
72
|
export const KNOWN_INLINE_DIRECTIVES = new Set(["kbd", "icon", "math", "mark", "highlight", "button", "badge"]);
|
|
29
73
|
/** Directives that are blocks: `::name{…}` draws one only on a line of its own (parse.ts mapContainerToTreeNode). */
|
|
30
|
-
const BLOCK_DIRECTIVES = new Set(
|
|
74
|
+
const BLOCK_DIRECTIVES = new Set(BLOCK_LEAF_DIRECTIVES);
|
|
31
75
|
/** Inline tokens markdown-it-attrs can give attributes to that the parser has no field for (links and images keep theirs). */
|
|
32
76
|
const ATTRLESS_INLINE = { strong_open: "bold text", em_open: "italic text", s_open: "struck-through text", code_inline: "inline code" };
|
|
33
77
|
/**
|
|
@@ -56,6 +100,20 @@ export function attributeProblem(block) {
|
|
|
56
100
|
* Diagnostics for a token stream. `lines` is the parsed text split at `\n`;
|
|
57
101
|
* `lineOf` maps a 0-based line of it to a 1-based source line.
|
|
58
102
|
*/
|
|
103
|
+
/** Why a `:::youtube` block is wrong (its own body, or another block's closer), or undefined if it is the empty block with its own closer. */
|
|
104
|
+
function youtubeAsBlock(map, closed, lines, around, lineOf) {
|
|
105
|
+
if (!closed)
|
|
106
|
+
return "is never closed, so it takes in everything after it";
|
|
107
|
+
if (lines.slice(map[0] + 1, map[1]).some((l) => l.replace(/^[\s>]*/, "") !== "")) {
|
|
108
|
+
return "is a block, and the lines up to its closer are dropped (a video shows no content)";
|
|
109
|
+
}
|
|
110
|
+
const parent = around[around.length - 1];
|
|
111
|
+
if (around.some((a) => a.map[1] === map[1]))
|
|
112
|
+
return `has no closer of its own: it ends with the :::${parent?.name ?? "block"} around it`;
|
|
113
|
+
if (parent && !parent.closed)
|
|
114
|
+
return `takes the closer on line ${lineOf(map[1]) ?? "?"}, which leaves :::${parent.name} open`;
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
59
117
|
export function collectDiagnostics(tokens, lines, lineOf) {
|
|
60
118
|
const out = [];
|
|
61
119
|
const add = (line0, code, message) => {
|
|
@@ -63,8 +121,16 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
63
121
|
if (line !== undefined)
|
|
64
122
|
out.push({ line, code, message });
|
|
65
123
|
};
|
|
124
|
+
const code = codeLineSet(tokens);
|
|
125
|
+
let row;
|
|
126
|
+
// The `:::` blocks around the current token, outermost first
|
|
127
|
+
const around = [];
|
|
66
128
|
const walk = (list) => {
|
|
67
|
-
|
|
129
|
+
// Indexed: i per token made the diagnostics quadratic in the page's length
|
|
130
|
+
for (let i = 0; i < list.length; i++) {
|
|
131
|
+
const tok = list[i];
|
|
132
|
+
if (/^container_.+_close$/.test(tok.type))
|
|
133
|
+
around.pop();
|
|
68
134
|
const open = /^container_(.+)_open$/.exec(tok.type);
|
|
69
135
|
if (open && tok.map) {
|
|
70
136
|
const generic = open[1] === GENERIC_CONTAINER;
|
|
@@ -74,7 +140,14 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
74
140
|
const closed = closesContainer(lines[tok.map[1]], colons);
|
|
75
141
|
const skipped = tok.meta?.skippedCloser;
|
|
76
142
|
const legacy = tok.meta?.legacyCloser;
|
|
77
|
-
|
|
143
|
+
// A video has no body, so `:::youtube` is a block that has taken in what follows it, or a
|
|
144
|
+
// closer meant for another block. The leaf form says "no body" in the syntax
|
|
145
|
+
// (plans/youtube-leaf.md). A blank line in a quote is `>`, so quote markers are not content.
|
|
146
|
+
const leafAsBlock = name === "youtube" ? youtubeAsBlock(tok.map, closed, lines, around, lineOf) : undefined;
|
|
147
|
+
if (leafAsBlock) {
|
|
148
|
+
add(tok.map[0], "leaf-as-block", `a video has no body: write ::youtube{…} on one line, with no closer — as :::youtube it ${leafAsBlock}`);
|
|
149
|
+
}
|
|
150
|
+
else if (!closed && legacy !== undefined && !generic) {
|
|
78
151
|
// Before the nesting stack, the closer on that line ended this block and the ones inside it
|
|
79
152
|
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${lines[legacy]?.trim()} on line ${lineOf(legacy) ?? "?"} closes only the innermost open block; each block needs its own closer (one \`:::\` used to close several)`);
|
|
80
153
|
}
|
|
@@ -89,6 +162,9 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
89
162
|
else if (generic) {
|
|
90
163
|
add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`);
|
|
91
164
|
}
|
|
165
|
+
if (tok.meta?.unverified) {
|
|
166
|
+
add(tok.map[0], "nesting-unchecked", `the parser stopped re-checking the nested blocks on this page here, so where :::${name} ends may be wrong — check that each block inside it has its own closer, or split the page`);
|
|
167
|
+
}
|
|
92
168
|
if (closed) {
|
|
93
169
|
// The closer's end-tag label: it closes this container whatever it says, so a wrong
|
|
94
170
|
// name means the author thinks it closes another one
|
|
@@ -103,6 +179,8 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
103
179
|
if (problem)
|
|
104
180
|
add(tok.map[0], "bad-attributes", `the attributes of :::${name} ${problem} — they are ignored`);
|
|
105
181
|
}
|
|
182
|
+
// An unknown block never closed is read back as text, so it is no parent (but still popped)
|
|
183
|
+
around.push({ map: tok.map, closed: closed || (generic && tok.meta?.unclosed === true), name });
|
|
106
184
|
}
|
|
107
185
|
// A `:::` line no open block took: every block it could close is already closed
|
|
108
186
|
if (tok.type === "inline" && tok.map) {
|
|
@@ -136,13 +214,13 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
136
214
|
// Attribute blocks markdown-it-attrs read and the tree has nowhere to keep
|
|
137
215
|
if (tok.type === "list_item_open" && tok.map && tok.attrs?.length) {
|
|
138
216
|
// parse.ts keepListItemBraces puts trailing braces back as text, or on a heading; what is left had nowhere to go
|
|
139
|
-
add(tok.map[0], "dropped-attributes", `the attribute block on this list item is dropped — list items take no attributes`);
|
|
217
|
+
add(braceLine(lines, code, tok.map[0], tok.map[1] - 1), "dropped-attributes", `the attribute block on this list item is dropped — list items take no attributes`);
|
|
140
218
|
}
|
|
141
219
|
// A table row that is only an attribute block, where the parser could not move it onto the
|
|
142
220
|
// table (not the last row, or text follows the table with no blank line): an empty row
|
|
143
221
|
if (tok.type === "tr_open" && tok.map) {
|
|
144
222
|
const cells = [];
|
|
145
|
-
for (let k =
|
|
223
|
+
for (let k = i + 1; k < list.length && list[k].type !== "tr_close"; k++)
|
|
146
224
|
cells.push(list[k]);
|
|
147
225
|
const inlines = cells.filter((c) => c.type === "inline");
|
|
148
226
|
if (cells[0]?.attrs?.length && /^\{[^{}]*\}$/.test((inlines[0]?.content ?? "").trim()) && inlines.slice(1).every((c) => c.content.trim() === "")) {
|
|
@@ -153,11 +231,11 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
153
231
|
// Only real attribute names: markdown-it-attrs also reads a leftover include (`{* src.py hl[4] *}`, FastAPI) as keys
|
|
154
232
|
const own = tok.attrs?.filter(([k]) => k !== "start") ?? [];
|
|
155
233
|
if ((tok.type === "bullet_list_open" || tok.type === "ordered_list_open") && tok.map && own.length > 0 && own.every(([k]) => /^[A-Za-z_][\w:.-]*$/.test(k))) {
|
|
156
|
-
add(tok.map[0], "dropped-attributes", `the attribute block on this list is dropped — lists take no attributes`);
|
|
234
|
+
add(braceLine(lines, code, tok.map[0], tok.map[1], true), "dropped-attributes", `the attribute block on this list is dropped — lists take no attributes`);
|
|
157
235
|
}
|
|
158
236
|
// An attribute block alone on its line after a blank line: an empty paragraph that carries it
|
|
159
237
|
if (tok.type === "paragraph_open" && tok.map && tok.attrs?.length) {
|
|
160
|
-
const inline = list[
|
|
238
|
+
const inline = list[i + 1];
|
|
161
239
|
// Only what an author writes as attributes: `{#id}`, `{.class}`, `{key=…}`. markdown-it-attrs also eats a
|
|
162
240
|
// leftover MDX expression (`{/* prettier-ignore */}`), which is an importer's problem, not the author's
|
|
163
241
|
const looksLikeAttrs = /^\{\s*(?:[#.][^\s{}]|[\w-]+\s*=)/.test(inline?.content.trim() ?? "");
|
|
@@ -185,20 +263,58 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
185
263
|
}
|
|
186
264
|
}
|
|
187
265
|
// `:kbd{…}` with no `[label]` is not a directive: the label is what it shows
|
|
266
|
+
const missing = (name) => `:${name}{…} has no [label] — it is shown as text; write :${name}[…]{…}`;
|
|
267
|
+
const inText = new Set();
|
|
268
|
+
// An autolink's text is its URL, not prose
|
|
269
|
+
let autolink = 0;
|
|
188
270
|
for (const child of tok.children) {
|
|
189
|
-
if (child.type
|
|
271
|
+
if (child.type === "link_open" && (child.markup === "autolink" || child.markup === "linkify"))
|
|
272
|
+
autolink++;
|
|
273
|
+
if (child.type === "link_close" && autolink > 0)
|
|
274
|
+
autolink--;
|
|
275
|
+
if (child.type !== "text" || autolink > 0)
|
|
190
276
|
continue;
|
|
191
277
|
for (const m of child.content.matchAll(/:([a-z]+)\{/g)) {
|
|
192
278
|
if (m.index > 0 && PRECEDES_TEXT.test(child.content[m.index - 1]))
|
|
193
279
|
continue;
|
|
194
|
-
if (KNOWN_INLINE_DIRECTIVES.has(m[1]))
|
|
195
|
-
add(
|
|
280
|
+
if (KNOWN_INLINE_DIRECTIVES.has(m[1])) {
|
|
281
|
+
inText.add(m[1]);
|
|
282
|
+
let at = tok.map[0];
|
|
283
|
+
while (at < tok.map[1] - 1 && !(lines[at] ?? "").includes(`:${m[1]}{`))
|
|
284
|
+
at++;
|
|
285
|
+
add(at, "missing-label", missing(m[1]));
|
|
286
|
+
}
|
|
196
287
|
}
|
|
197
288
|
}
|
|
289
|
+
// At the end of a paragraph, heading or cell, markdown-it-attrs takes the braces for the
|
|
290
|
+
// block's own, leaving `:kbd` in the text: read the source lines for what is no longer there
|
|
291
|
+
if (list[i - 1]?.attrs?.length) {
|
|
292
|
+
for (let l = tok.map[0]; l < tok.map[1]; l++) {
|
|
293
|
+
const line = blankCodeAndTags(lines[l] ?? "");
|
|
294
|
+
for (const m of line.matchAll(/:([a-z]+)\{/g)) {
|
|
295
|
+
if (m.index > 0 && PRECEDES_TEXT.test(line[m.index - 1]))
|
|
296
|
+
continue;
|
|
297
|
+
if (KNOWN_INLINE_DIRECTIVES.has(m[1]) && !inText.has(m[1]) && !line.startsWith(`:${m[1]}[`, m.index)) {
|
|
298
|
+
inText.add(m[1]);
|
|
299
|
+
add(l, "missing-label", missing(m[1]));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// A table cell's inline token has no line map: its row's has
|
|
306
|
+
if (tok.type === "tr_open" && tok.map)
|
|
307
|
+
row = tok.map[0];
|
|
308
|
+
if (tok.type === "inline" && !tok.map && row !== undefined && /^t[hd]_open$/.test(list[i - 1]?.type ?? "") && list[i - 1]?.attrs?.length) {
|
|
309
|
+
const cell = blankCodeAndTags(tok.content);
|
|
310
|
+
const m = /:([a-z]+)\{/.exec(cell);
|
|
311
|
+
if (m && KNOWN_INLINE_DIRECTIVES.has(m[1]) && !(m.index > 0 && PRECEDES_TEXT.test(cell[m.index - 1]))) {
|
|
312
|
+
add(row, "missing-label", `:${m[1]}{…} has no [label] — it is shown as text; write :${m[1]}[…]{…}`);
|
|
313
|
+
}
|
|
198
314
|
}
|
|
199
315
|
// A heading's attribute block that markdown-it-attrs could not read stays in its text
|
|
200
316
|
if (tok.type === "heading_open" && tok.map) {
|
|
201
|
-
const inline = list[
|
|
317
|
+
const inline = list[i + 1];
|
|
202
318
|
// markdown-it-attrs takes a block it reads out of the text tokens (not out of `content`)
|
|
203
319
|
const last = [...(inline?.children ?? [])].reverse().find((c) => c.type === "text");
|
|
204
320
|
const tail = /\{\s*(?:[#.][^{}]*|[\w-]+\s*=[^{}]*)\}?\s*$/.exec(last?.content ?? "");
|
package/dist/index.d.ts
CHANGED
|
@@ -8,12 +8,13 @@ export { transformCodeRegions, transformProseLines, transformProseSegments, fenc
|
|
|
8
8
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
|
9
9
|
export type { ParseResult, ParseOptions } from "./parse.js";
|
|
10
10
|
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
11
|
-
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
|
11
|
+
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
12
12
|
export { buildNavFromDirectory } from "./nav.js";
|
|
13
13
|
export type { BuildNavOptions } from "./nav.js";
|
|
14
14
|
export { loadNavFile } from "./nav-file.js";
|
|
15
15
|
export type { NavFile, NavFileItem, LoadNavFileOptions, MissingNavTarget, } from "./nav-file.js";
|
|
16
16
|
export { renderDiagrams } from "./renderers/diagrams.js";
|
|
17
|
-
export type { RenderDiagramsOptions } from "./renderers/diagrams.js";
|
|
17
|
+
export type { RenderDiagramsOptions, DiagramEngine } from "./renderers/diagrams.js";
|
|
18
|
+
export { renderPeleSvg, diagramId } from "./renderers/pele.js";
|
|
18
19
|
export { renderMermaidSvg, setMmdcOverride, findMmdc } from "./renderers/mermaid.js";
|
|
19
20
|
export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN, type TemplatePagesOptions } from "./template-pages.js";
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,10 @@ export { transformCodeRegions, transformProseLines, transformProseSegments, fenc
|
|
|
11
11
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
|
12
12
|
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
13
13
|
// The `:::name` blocks the parser knows (an editor completes names from it)
|
|
14
|
-
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
|
14
|
+
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
15
15
|
export { buildNavFromDirectory } from "./nav.js";
|
|
16
16
|
export { loadNavFile } from "./nav-file.js";
|
|
17
17
|
export { renderDiagrams } from "./renderers/diagrams.js";
|
|
18
|
+
export { renderPeleSvg, diagramId } from "./renderers/pele.js";
|
|
18
19
|
export { renderMermaidSvg, setMmdcOverride, findMmdc } from "./renderers/mermaid.js";
|
|
19
20
|
export { TemplatePages, TemplatePageError, declaresParams, ATTRIBUTE_BLOCK_OPEN } from "./template-pages.js";
|
package/dist/parse-attrs.d.ts
CHANGED
|
@@ -21,3 +21,12 @@ export declare function parseAttrs(inner: string): ParsedAttrs;
|
|
|
21
21
|
* `id` and `classes` become `id` and `class`; other props flow through.
|
|
22
22
|
*/
|
|
23
23
|
export declare function attrsToProps(attrs: ParsedAttrs): Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* The index of the `}` that closes an attribute block whose `{` is just before `start`, or -1.
|
|
26
|
+
* Braces inside a double-quoted value (`title="a } b"`, with `\"` escapes, as escapeAttrValue
|
|
27
|
+
* writes them) do not count: a brace-only count ended a leaf `::youtube{#a title="a } b"}` at the
|
|
28
|
+
* first `}` and lost the video. A `"` opens a value only straight after `=`, as parseAttrs reads
|
|
29
|
+
* it: a stray `"` in an unquoted value (`title=5"`) is a character, or the scan would run on through
|
|
30
|
+
* the paragraph. Single quotes are not quotes here, so an apostrophe reads as before.
|
|
31
|
+
*/
|
|
32
|
+
export declare function attrBlockEnd(src: string, start: number, max: number): number;
|
package/dist/parse-attrs.js
CHANGED
|
@@ -114,3 +114,31 @@ export function attrsToProps(attrs) {
|
|
|
114
114
|
}
|
|
115
115
|
return props;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The index of the `}` that closes an attribute block whose `{` is just before `start`, or -1.
|
|
119
|
+
* Braces inside a double-quoted value (`title="a } b"`, with `\"` escapes, as escapeAttrValue
|
|
120
|
+
* writes them) do not count: a brace-only count ended a leaf `::youtube{#a title="a } b"}` at the
|
|
121
|
+
* first `}` and lost the video. A `"` opens a value only straight after `=`, as parseAttrs reads
|
|
122
|
+
* it: a stray `"` in an unquoted value (`title=5"`) is a character, or the scan would run on through
|
|
123
|
+
* the paragraph. Single quotes are not quotes here, so an apostrophe reads as before.
|
|
124
|
+
*/
|
|
125
|
+
export function attrBlockEnd(src, start, max) {
|
|
126
|
+
let depth = 1;
|
|
127
|
+
let quoted = false;
|
|
128
|
+
for (let i = start; i < max; i++) {
|
|
129
|
+
const c = src.charCodeAt(i);
|
|
130
|
+
if (quoted) {
|
|
131
|
+
if (c === 0x5c /* \ */)
|
|
132
|
+
i++;
|
|
133
|
+
else if (c === 0x22 /* " */)
|
|
134
|
+
quoted = false;
|
|
135
|
+
}
|
|
136
|
+
else if (c === 0x22 && i > start && src.charCodeAt(i - 1) === 0x3d /* = */)
|
|
137
|
+
quoted = true;
|
|
138
|
+
else if (c === 0x7b /* { */)
|
|
139
|
+
depth++;
|
|
140
|
+
else if (c === 0x7d /* } */ && --depth === 0)
|
|
141
|
+
return i;
|
|
142
|
+
}
|
|
143
|
+
return -1;
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAttrs } from "./parse-attrs.js";
|
|
1
|
+
import { attrBlockEnd, parseAttrs } from "./parse-attrs.js";
|
|
2
2
|
export function blockLeafDirectivePlugin(md) {
|
|
3
3
|
md.block.ruler.before("paragraph", "dogsbay_block_leaf_directive", blockLeafRule);
|
|
4
4
|
}
|
|
@@ -42,20 +42,8 @@ function blockLeafRule(state, startLine, endLine, silent) {
|
|
|
42
42
|
let attrsStr = "";
|
|
43
43
|
if (state.src.charCodeAt(pos) === 0x7b /* { */) {
|
|
44
44
|
const attrStart = pos + 1;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
while (ap < max) {
|
|
48
|
-
const c = state.src.charCodeAt(ap);
|
|
49
|
-
if (c === 0x7b)
|
|
50
|
-
depth++;
|
|
51
|
-
else if (c === 0x7d) {
|
|
52
|
-
depth--;
|
|
53
|
-
if (depth === 0)
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
ap++;
|
|
57
|
-
}
|
|
58
|
-
if (ap >= max)
|
|
45
|
+
const ap = attrBlockEnd(state.src, attrStart, max);
|
|
46
|
+
if (ap < 0)
|
|
59
47
|
return false; // unterminated
|
|
60
48
|
attrsStr = state.src.slice(attrStart, ap);
|
|
61
49
|
pos = ap + 1;
|
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type MarkdownIt from "markdown-it";
|
|
9
9
|
export declare const KNOWN_BLOCK_DIRECTIVES: readonly ["tabs", "tab", "steps", "step", "cards", "card", "grid", "grid-item", "details", "callout", "note", "tip", "info", "important", "warning", "caution", "danger", "success", "hint", "attention", "error", "failure", "bug", "question", "abstract", "quote", "button", "figure", "math", "api-endpoint", "api-params", "api-response", "config", "files", "example", "accordion", "accordion-item", "link-card", "autodoc", "api-class", "api-member", "api-doc", "api-source", "youtube", "openapi", "theme"];
|
|
10
|
+
/**
|
|
11
|
+
* Directives with no body, written only as the leaf `::name{…}` (parse.ts mapContainerToTreeNode
|
|
12
|
+
* drops any children; the serializer writes `::`). `youtube` is also a container above, for the
|
|
13
|
+
* empty `:::youtube{…}` + `:::` older serializers wrote; `:::avatar` and `:::child-links` are not
|
|
14
|
+
* blocks at all. Exported for editors, so completion does not offer `:::` or a closer for them.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LEAF_ONLY_DIRECTIVES: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* Every name `::name{…}` draws as a block on a line of its own: the containers (an empty one,
|
|
19
|
+
* `::grid-item{…}` = `:::grid-item{…}` + `:::`) and the body-less leaves. Inside a line, these
|
|
20
|
+
* are reported (`block-directive-inline`) and kept as text.
|
|
21
|
+
*/
|
|
22
|
+
export declare const BLOCK_LEAF_DIRECTIVES: readonly string[];
|
|
10
23
|
/**
|
|
11
24
|
* The token name for a `:::name` block with a name no renderer knows. It becomes a node of that
|
|
12
25
|
* type (parse.ts), which renderers show as a plain container and report, rather than literal text.
|
|
@@ -54,8 +54,9 @@ export const KNOWN_BLOCK_DIRECTIVES = [
|
|
|
54
54
|
"api-member",
|
|
55
55
|
"api-doc",
|
|
56
56
|
"api-source",
|
|
57
|
-
// Lazy-loading video embed (YouTube).
|
|
58
|
-
//
|
|
57
|
+
// Lazy-loading video embed (YouTube). No body, so written as the leaf
|
|
58
|
+
// `::youtube{#<videoId> title="…"}`; the block form here is the empty
|
|
59
|
+
// `:::youtube{…}` + `:::` earlier serializers wrote. Renders via @ui/youtube/YouTube.astro.
|
|
59
60
|
"youtube",
|
|
60
61
|
// An OpenAPI spec embedded in a page: `:::openapi{spec="api.yaml"}`,
|
|
61
62
|
// expanded into endpoint cards from the spec at build time (the
|
|
@@ -65,6 +66,19 @@ export const KNOWN_BLOCK_DIRECTIVES = [
|
|
|
65
66
|
// set (plans/dita-variants-themes.md). A wrapper, so it takes blocks.
|
|
66
67
|
"theme",
|
|
67
68
|
];
|
|
69
|
+
/**
|
|
70
|
+
* Directives with no body, written only as the leaf `::name{…}` (parse.ts mapContainerToTreeNode
|
|
71
|
+
* drops any children; the serializer writes `::`). `youtube` is also a container above, for the
|
|
72
|
+
* empty `:::youtube{…}` + `:::` older serializers wrote; `:::avatar` and `:::child-links` are not
|
|
73
|
+
* blocks at all. Exported for editors, so completion does not offer `:::` or a closer for them.
|
|
74
|
+
*/
|
|
75
|
+
export const LEAF_ONLY_DIRECTIVES = ["youtube", "avatar", "child-links"];
|
|
76
|
+
/**
|
|
77
|
+
* Every name `::name{…}` draws as a block on a line of its own: the containers (an empty one,
|
|
78
|
+
* `::grid-item{…}` = `:::grid-item{…}` + `:::`) and the body-less leaves. Inside a line, these
|
|
79
|
+
* are reported (`block-directive-inline`) and kept as text.
|
|
80
|
+
*/
|
|
81
|
+
export const BLOCK_LEAF_DIRECTIVES = [...KNOWN_BLOCK_DIRECTIVES, ...LEAF_ONLY_DIRECTIVES.filter((n) => !KNOWN_BLOCK_DIRECTIVES.includes(n))];
|
|
68
82
|
/**
|
|
69
83
|
* The token name for a `:::name` block with a name no renderer knows. It becomes a node of that
|
|
70
84
|
* type (parse.ts), which renderers show as a plain container and report, rather than literal text.
|
|
@@ -72,8 +86,7 @@ export const KNOWN_BLOCK_DIRECTIVES = [
|
|
|
72
86
|
export const GENERIC_CONTAINER = "dogsbay_generic";
|
|
73
87
|
export function containersPlugin(md) {
|
|
74
88
|
for (const name of KNOWN_BLOCK_DIRECTIVES) {
|
|
75
|
-
|
|
76
|
-
registerOpener(name, { leaf: name === "youtube" });
|
|
89
|
+
registerOpener(name);
|
|
77
90
|
containerRule(md, name, {
|
|
78
91
|
validate: (params) => {
|
|
79
92
|
const trimmed = params.trim();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAttrs } from "./parse-attrs.js";
|
|
1
|
+
import { attrBlockEnd, parseAttrs } from "./parse-attrs.js";
|
|
2
2
|
/** A character a directive cannot follow directly (see inlineDirectiveRule). */
|
|
3
3
|
export const PRECEDES_TEXT = /[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}0-9:]/u;
|
|
4
4
|
export function inlineDirectivesPlugin(md) {
|
|
@@ -78,20 +78,8 @@ function inlineDirectiveRule(state, silent) {
|
|
|
78
78
|
let attrs = "";
|
|
79
79
|
if (src.charCodeAt(pos) === 0x7b /* { */) {
|
|
80
80
|
const attrStart = pos + 1;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
while (ap < src.length) {
|
|
84
|
-
const c = src.charCodeAt(ap);
|
|
85
|
-
if (c === 0x7b)
|
|
86
|
-
depth++;
|
|
87
|
-
else if (c === 0x7d) {
|
|
88
|
-
depth--;
|
|
89
|
-
if (depth === 0)
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
ap++;
|
|
93
|
-
}
|
|
94
|
-
if (ap < src.length) {
|
|
81
|
+
const ap = attrBlockEnd(src, attrStart, src.length);
|
|
82
|
+
if (ap >= 0) {
|
|
95
83
|
attrs = src.slice(attrStart, ap);
|
|
96
84
|
pos = ap + 1;
|
|
97
85
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { TreeNode } from "@dogsbay/types";
|
|
2
|
+
/** Who draws Mermaid fences (`content.diagrams`): mmdc, as Mermaid draws them, or Pele, themed. */
|
|
3
|
+
export type DiagramEngine = "mermaid-cli" | "pele";
|
|
2
4
|
export interface RenderDiagramsOptions {
|
|
5
|
+
/** Default `mermaid-cli`. */
|
|
6
|
+
engine?: DiagramEngine;
|
|
3
7
|
/**
|
|
4
8
|
* Suppress the one-time "mmdc not found" warning printed when
|
|
5
9
|
* the binary can't be resolved. Default false. Tests pass true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderMermaidSvg } from "./mermaid.js";
|
|
2
|
+
import { diagramId, renderPeleSvg } from "./pele.js";
|
|
2
3
|
const DIAGRAM_LANGS = new Set(["mermaid"]);
|
|
3
4
|
/**
|
|
4
5
|
* Walk a TreeNode tree and replace `code` blocks with a diagram
|
|
@@ -16,6 +17,10 @@ const DIAGRAM_LANGS = new Set(["mermaid"]);
|
|
|
16
17
|
*/
|
|
17
18
|
export async function renderDiagrams(tree, options) {
|
|
18
19
|
let warned = false;
|
|
20
|
+
const pele = options?.engine === "pele";
|
|
21
|
+
// Pele: a diagram shown twice on the page gets its own ids the second time
|
|
22
|
+
const seen = new Map();
|
|
23
|
+
const failed = [];
|
|
19
24
|
async function walk(nodes) {
|
|
20
25
|
for (const node of nodes) {
|
|
21
26
|
if (node.children)
|
|
@@ -42,7 +47,20 @@ export async function renderDiagrams(tree, options) {
|
|
|
42
47
|
: "";
|
|
43
48
|
if (!code)
|
|
44
49
|
continue;
|
|
45
|
-
|
|
50
|
+
let svg;
|
|
51
|
+
if (pele) {
|
|
52
|
+
const base = diagramId(code);
|
|
53
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
54
|
+
seen.set(base, n);
|
|
55
|
+
const r = await renderPeleSvg(code, n === 1 ? base : `${base}-${n}`);
|
|
56
|
+
if ("error" in r) {
|
|
57
|
+
failed.push(r.error);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
svg = r.svg;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
svg = await renderMermaidSvg(code);
|
|
46
64
|
if (svg === null) {
|
|
47
65
|
if (!warned && !options?.silent) {
|
|
48
66
|
console.warn("[diagrams] mmdc (mermaid-cli) not installed or render " +
|
|
@@ -67,4 +85,8 @@ export async function renderDiagrams(tree, options) {
|
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
await walk(tree);
|
|
88
|
+
if (failed.length > 0 && !options?.silent) {
|
|
89
|
+
console.warn(`[diagrams] Pele could not draw ${failed.length} diagram(s), left as code blocks: ` +
|
|
90
|
+
failed[0].split("\n")[0].slice(0, 160));
|
|
91
|
+
}
|
|
70
92
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Longest diagram source drawn; a longer one is refused rather than laid out (as dogsbay-ssg). */
|
|
2
|
+
export declare const MAX_DIAGRAM_CHARS = 100000;
|
|
3
|
+
/**
|
|
4
|
+
* Draw one diagram. `id` prefixes the ids Pele writes (a title and description, for
|
|
5
|
+
* aria-labelledby), so two diagrams on a page do not share them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function renderPeleSvg(code: string, id: string): Promise<{
|
|
8
|
+
svg: string;
|
|
9
|
+
} | {
|
|
10
|
+
error: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** A stable id from the source, valid in XML (`dg-` and hex). */
|
|
13
|
+
export declare function diagramId(code: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid drawn by Pele (`content.diagrams: pele`; plans/pele-diagrams.md): plain JavaScript,
|
|
3
|
+
* synchronous, no DOM and no browser, unlike mmdc. Its colours and fonts are CSS variables
|
|
4
|
+
* (`--pele-*`), which DocsLayout maps to the site theme, so a diagram follows light and dark mode
|
|
5
|
+
* instead of being inverted.
|
|
6
|
+
*
|
|
7
|
+
* Loaded with import() on the first diagram: a site without diagrams, or one drawn by mmdc,
|
|
8
|
+
* never loads it. dogsbay-ssg draws diagrams the same way (its core/pele.ts).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
/** Longest diagram source drawn; a longer one is refused rather than laid out (as dogsbay-ssg). */
|
|
12
|
+
export const MAX_DIAGRAM_CHARS = 100_000;
|
|
13
|
+
let pele;
|
|
14
|
+
/**
|
|
15
|
+
* Draw one diagram. `id` prefixes the ids Pele writes (a title and description, for
|
|
16
|
+
* aria-labelledby), so two diagrams on a page do not share them.
|
|
17
|
+
*/
|
|
18
|
+
export async function renderPeleSvg(code, id) {
|
|
19
|
+
if (code.length > MAX_DIAGRAM_CHARS)
|
|
20
|
+
return { error: `the diagram is longer than ${MAX_DIAGRAM_CHARS} characters` };
|
|
21
|
+
let p;
|
|
22
|
+
try {
|
|
23
|
+
p = await (pele ??= import("pele"));
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
pele = undefined;
|
|
27
|
+
return { error: `could not load Pele (pele): ${e instanceof Error ? e.message : String(e)}` };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// No images: an `img:` node would have every reader fetch its URL (a third-party one too),
|
|
31
|
+
// and a local path is neither copied as an asset nor given the site's base path
|
|
32
|
+
const { svg } = p.render(code, { idPrefix: id, limit: MAX_DIAGRAM_CHARS, images: false });
|
|
33
|
+
return svg ? { svg } : { error: "Pele returned no SVG" };
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** A stable id from the source, valid in XML (`dg-` and hex). */
|
|
40
|
+
export function diagramId(code) {
|
|
41
|
+
return "dg-" + createHash("sha256").update(code).digest("hex").slice(0, 12);
|
|
42
|
+
}
|
package/dist/serialize.js
CHANGED
|
@@ -116,8 +116,9 @@ function renderNodeBody(node, ctx) {
|
|
|
116
116
|
case "button":
|
|
117
117
|
return renderButton(node, ctx);
|
|
118
118
|
case "youtube":
|
|
119
|
-
//
|
|
120
|
-
|
|
119
|
+
// A video has no body, so it is a leaf: `::youtube{#<id> title="…"}`. `:::` always opens a
|
|
120
|
+
// block that needs a closer (the empty `:::youtube` … `:::` form still reads).
|
|
121
|
+
return `::youtube${renderAttrs(splitProps({ ...(node.props ?? {}), id: node.props?.id }, []))}`;
|
|
121
122
|
case "deflist":
|
|
122
123
|
return renderDeflist(node, ctx);
|
|
123
124
|
case "theme":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-dogsbay-md",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.129",
|
|
4
4
|
"description": "Dogsbay Markdown dialect — canonical format for TreeNode[] with directives, YAML bodies, and intelligent fallbacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@mermaid-js/mermaid-cli": "^11.16.0",
|
|
27
28
|
"@types/js-yaml": "^4.0.9",
|
|
28
29
|
"@types/markdown-it": "^14.1.2",
|
|
29
30
|
"@types/node": "^25.9.5",
|
|
30
31
|
"typescript": "^5.9.3",
|
|
31
|
-
"vitest": "^4.1.10"
|
|
32
|
-
"@mermaid-js/mermaid-cli": "^11.16.0"
|
|
32
|
+
"vitest": "^4.1.10"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"gray-matter": "^4.0.3",
|
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
"markdown-it-deflist": "^3.0.1",
|
|
40
40
|
"markdown-it-footnote": "^4.0.0",
|
|
41
41
|
"markdown-it-front-matter": "^0.2.4",
|
|
42
|
-
"
|
|
43
|
-
"@dogsbay/
|
|
44
|
-
"@dogsbay/
|
|
42
|
+
"pele": "1.6.0",
|
|
43
|
+
"@dogsbay/minja": "0.2.0-beta.129",
|
|
44
|
+
"@dogsbay/serialize-core": "0.2.0-beta.129",
|
|
45
|
+
"@dogsbay/types": "0.2.0-beta.129"
|
|
45
46
|
},
|
|
46
47
|
"license": "MIT",
|
|
47
48
|
"repository": {
|