@dogsbay/format-dogsbay-md 0.2.0-beta.124 → 0.2.0-beta.125
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 -0
- package/dist/browser.js +1 -0
- package/dist/diagnostics.d.ts +39 -0
- package/dist/diagnostics.js +117 -0
- package/dist/directive-preserve.d.ts +6 -0
- package/dist/directive-preserve.js +23 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parse.d.ts +16 -0
- package/dist/parse.js +56 -9
- package/package.json +4 -4
package/dist/browser.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* module load and would drag `node:fs` back into this graph.
|
|
13
13
|
*/
|
|
14
14
|
export { dogsbayMdToTree, parseInline, type ParseResult, type ParseOptions } from "./parse.js";
|
|
15
|
+
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
15
16
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
16
17
|
export type { SerializeOptions } from "./types.js";
|
|
17
18
|
export { inlineToDogsbayMd } from "./inline.js";
|
package/dist/browser.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* module load and would drag `node:fs` back into this graph.
|
|
13
13
|
*/
|
|
14
14
|
export { dogsbayMdToTree, parseInline } from "./parse.js";
|
|
15
|
+
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
15
16
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
16
17
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
17
18
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ParseDiagnostic {
|
|
2
|
+
/** 1-based line in the source (front matter counted). */
|
|
3
|
+
line: number;
|
|
4
|
+
/** Stable kind, for filtering. */
|
|
5
|
+
code: "unclosed-container" | "stray-closer" | "unknown-container" | "unknown-inline" | "bad-attributes";
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Whether a source line closes a container opened with `colons` colons. The
|
|
10
|
+
* line may sit in a blockquote or a list item: its `>` markers and
|
|
11
|
+
* indentation come first.
|
|
12
|
+
*/
|
|
13
|
+
export declare const closesContainer: (line: string | undefined, colons: number) => boolean;
|
|
14
|
+
/** The inline directives the parser turns into nodes (parse.ts inlineDirectiveToNode); any other name stays literal text. */
|
|
15
|
+
export declare const KNOWN_INLINE_DIRECTIVES: ReadonlySet<string>;
|
|
16
|
+
type Token = {
|
|
17
|
+
type: string;
|
|
18
|
+
tag: string;
|
|
19
|
+
info: string;
|
|
20
|
+
markup: string;
|
|
21
|
+
content: string;
|
|
22
|
+
map?: [number, number] | null;
|
|
23
|
+
children: Token[] | null;
|
|
24
|
+
meta: {
|
|
25
|
+
unclosed?: boolean;
|
|
26
|
+
} | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Why an attribute block `{…}` cannot be read, or undefined when it can.
|
|
30
|
+
* parseAttrs never fails: it reads what it can and drops the rest, so a
|
|
31
|
+
* missing brace or an unterminated quote loses attributes silently.
|
|
32
|
+
*/
|
|
33
|
+
export declare function attributeProblem(block: string): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Diagnostics for a token stream. `lines` is the parsed text split at `\n`;
|
|
36
|
+
* `lineOf` maps a 0-based line of it to a 1-based source line.
|
|
37
|
+
*/
|
|
38
|
+
export declare function collectDiagnostics(tokens: Token[], lines: string[], lineOf: (line0: number) => number | undefined): ParseDiagnostic[];
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Problems in Dogsbay MD source that the parser recovers from without a word
|
|
3
|
+
* (ParseOptions.diagnostics): each would otherwise reach the page as literal
|
|
4
|
+
* text, a plain box, or a block that swallows the rest of the document.
|
|
5
|
+
*
|
|
6
|
+
* Read from markdown-it's tokens, so they describe what the parser actually
|
|
7
|
+
* did, not a second grammar's guess. A `::leaf` with an unknown name is not
|
|
8
|
+
* here: the parser passes any leaf through as a node of that type, and only a
|
|
9
|
+
* renderer knows whether it can draw it (render-html reports the ones it can't).
|
|
10
|
+
*/
|
|
11
|
+
import { GENERIC_CONTAINER } from "./plugin-containers.js";
|
|
12
|
+
/**
|
|
13
|
+
* Whether a source line closes a container opened with `colons` colons. The
|
|
14
|
+
* line may sit in a blockquote or a list item: its `>` markers and
|
|
15
|
+
* indentation come first.
|
|
16
|
+
*/
|
|
17
|
+
export const closesContainer = (line, colons) => line !== undefined && new RegExp(`^:{${colons},}\\s*$`).test(line.replace(/^[\s>]*/, ""));
|
|
18
|
+
/** The inline directives the parser turns into nodes (parse.ts inlineDirectiveToNode); any other name stays literal text. */
|
|
19
|
+
export const KNOWN_INLINE_DIRECTIVES = new Set(["kbd", "icon", "math", "mark", "highlight", "button", "badge"]);
|
|
20
|
+
/**
|
|
21
|
+
* Why an attribute block `{…}` cannot be read, or undefined when it can.
|
|
22
|
+
* parseAttrs never fails: it reads what it can and drops the rest, so a
|
|
23
|
+
* missing brace or an unterminated quote loses attributes silently.
|
|
24
|
+
*/
|
|
25
|
+
export function attributeProblem(block) {
|
|
26
|
+
if (!block.trimEnd().endsWith("}"))
|
|
27
|
+
return "has no closing }";
|
|
28
|
+
const inner = block.trim().slice(1, -1);
|
|
29
|
+
let quote;
|
|
30
|
+
for (const c of inner) {
|
|
31
|
+
if (quote) {
|
|
32
|
+
if (c === quote)
|
|
33
|
+
quote = undefined;
|
|
34
|
+
}
|
|
35
|
+
else if (c === '"' || c === "'")
|
|
36
|
+
quote = c;
|
|
37
|
+
}
|
|
38
|
+
if (quote)
|
|
39
|
+
return `has an unterminated ${quote === '"' ? "double" : "single"} quote`;
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Diagnostics for a token stream. `lines` is the parsed text split at `\n`;
|
|
44
|
+
* `lineOf` maps a 0-based line of it to a 1-based source line.
|
|
45
|
+
*/
|
|
46
|
+
export function collectDiagnostics(tokens, lines, lineOf) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const add = (line0, code, message) => {
|
|
49
|
+
const line = line0 === undefined ? undefined : lineOf(line0);
|
|
50
|
+
if (line !== undefined)
|
|
51
|
+
out.push({ line, code, message });
|
|
52
|
+
};
|
|
53
|
+
const walk = (list) => {
|
|
54
|
+
for (const tok of list) {
|
|
55
|
+
const open = /^container_(.+)_open$/.exec(tok.type);
|
|
56
|
+
if (open && tok.map) {
|
|
57
|
+
const generic = open[1] === GENERIC_CONTAINER;
|
|
58
|
+
const info = (tok.info ?? "").trim();
|
|
59
|
+
const name = generic ? (/^[A-Za-z][\w-]*/.exec(info)?.[0] ?? info) : open[1];
|
|
60
|
+
const colons = (tok.markup ?? ":::").length;
|
|
61
|
+
const closed = closesContainer(lines[tok.map[1]], colons);
|
|
62
|
+
if (!closed) {
|
|
63
|
+
add(tok.map[0], "unclosed-container", generic
|
|
64
|
+
? `:::${name} is never closed — it is shown as text (close it with a line of ${":".repeat(colons)})`
|
|
65
|
+
: `:::${name} is never closed — it takes in everything after it, to the end of the page or of the block it sits in (close it with a line of ${":".repeat(colons)})`);
|
|
66
|
+
}
|
|
67
|
+
else if (generic) {
|
|
68
|
+
add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`);
|
|
69
|
+
}
|
|
70
|
+
const rest = info.slice(name.length).trim();
|
|
71
|
+
if (rest.startsWith("{")) {
|
|
72
|
+
const problem = attributeProblem(rest);
|
|
73
|
+
if (problem)
|
|
74
|
+
add(tok.map[0], "bad-attributes", `the attributes of :::${name} ${problem} — they are ignored`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A `:::` line markdown-it-container did not take as a closer: the container it was
|
|
78
|
+
// meant to close ended earlier (an inner block of the same fence closed it), or there is none
|
|
79
|
+
if (tok.type === "inline" && tok.map) {
|
|
80
|
+
(tok.content ?? "").split("\n").forEach((l, k) => {
|
|
81
|
+
if (/^\s*:{3,}\s*$/.test(l)) {
|
|
82
|
+
add(tok.map[0] + k, "stray-closer", `stray \`${l.trim()}\` — it closes nothing and is shown as text; to nest a directive, give the outer one more colons (::::steps around :::tabs)`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (tok.type === "inline" && tok.map && tok.children) {
|
|
87
|
+
const text = tok.content ?? "";
|
|
88
|
+
let from = 0;
|
|
89
|
+
for (const child of tok.children) {
|
|
90
|
+
if (child.type !== "dogsbay_inline_directive_open" && child.type !== "dogsbay_leaf_directive")
|
|
91
|
+
continue;
|
|
92
|
+
if (KNOWN_INLINE_DIRECTIVES.has(child.tag))
|
|
93
|
+
continue;
|
|
94
|
+
const form = child.type === "dogsbay_leaf_directive" ? `::${child.tag}` : `:${child.tag}[`;
|
|
95
|
+
const at = text.indexOf(form, from);
|
|
96
|
+
from = at >= 0 ? at + form.length : from;
|
|
97
|
+
const offset = at >= 0 ? (text.slice(0, at).match(/\n/g) ?? []).length : 0;
|
|
98
|
+
const shown = child.type === "dogsbay_leaf_directive" ? `::${child.tag}` : `:${child.tag}[…]`;
|
|
99
|
+
add(tok.map[0] + offset, "unknown-inline", `${shown} is not a known inline directive — it is shown as text`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// A heading's attribute block that markdown-it-attrs could not read stays in its text
|
|
103
|
+
if (tok.type === "heading_open" && tok.map) {
|
|
104
|
+
const inline = list[list.indexOf(tok) + 1];
|
|
105
|
+
// markdown-it-attrs takes a block it reads out of the text tokens (not out of `content`)
|
|
106
|
+
const last = [...(inline?.children ?? [])].reverse().find((c) => c.type === "text");
|
|
107
|
+
const tail = /\{\s*(?:[#.][^{}]*|[\w-]+\s*=[^{}]*)\}?\s*$/.exec(last?.content ?? "");
|
|
108
|
+
if (tail) {
|
|
109
|
+
const problem = attributeProblem(tail[0]) ?? "could not be read";
|
|
110
|
+
add(tok.map[0], "bad-attributes", `the heading's attribute block ${problem} — it is shown as text`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
walk(tokens);
|
|
116
|
+
return out.sort((a, b) => a.line - b.line);
|
|
117
|
+
}
|
|
@@ -11,6 +11,12 @@ export interface DirectiveSpan {
|
|
|
11
11
|
export interface ProtectResult {
|
|
12
12
|
source: string;
|
|
13
13
|
table: DirectiveSpan[];
|
|
14
|
+
/**
|
|
15
|
+
* For each line of `source`, the 0-based line of the input it came from: a
|
|
16
|
+
* stashed construct that spanned lines is one placeholder line, and an
|
|
17
|
+
* isolating blank line maps to the line it was inserted before.
|
|
18
|
+
*/
|
|
19
|
+
lineMap: number[];
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* Replace directive constructs in prose with placeholders and isolate
|
|
@@ -72,7 +72,18 @@ export function protectDirectives(source, options = {}) {
|
|
|
72
72
|
// lines, so markdown-it gives each its own paragraph. Variables stay
|
|
73
73
|
// inline (a lone `{{ x }}` line is inline content, not structure).
|
|
74
74
|
const lines = out.split("\n");
|
|
75
|
+
// Each placeholder stands for its construct's lines, so the input line of
|
|
76
|
+
// every output line is the count of line breaks consumed before it
|
|
77
|
+
const inputLine = [];
|
|
78
|
+
let at = 0;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
inputLine.push(at);
|
|
81
|
+
at += 1;
|
|
82
|
+
for (const m of line.matchAll(/\uE000(\d+)\uE001/g))
|
|
83
|
+
at += (table[Number(m[1])]?.raw.match(/\n/g) ?? []).length;
|
|
84
|
+
}
|
|
75
85
|
const isolated = [];
|
|
86
|
+
const lineMap = [];
|
|
76
87
|
for (let i = 0; i < lines.length; i++) {
|
|
77
88
|
const line = lines[i];
|
|
78
89
|
const only = line.match(/^\s*\uE000(\d+)\uE001\s*$/);
|
|
@@ -82,14 +93,17 @@ export function protectDirectives(source, options = {}) {
|
|
|
82
93
|
((options.isolate ?? "all") === "all" || entry.keyword === "include");
|
|
83
94
|
if (isBlockish && isolated.length && isolated[isolated.length - 1].trim() !== "") {
|
|
84
95
|
isolated.push("");
|
|
96
|
+
lineMap.push(inputLine[i]);
|
|
85
97
|
}
|
|
86
98
|
isolated.push(line);
|
|
99
|
+
lineMap.push(inputLine[i]);
|
|
87
100
|
if (isBlockish && i + 1 < lines.length && lines[i + 1].trim() !== "") {
|
|
88
101
|
isolated.push("");
|
|
102
|
+
lineMap.push(inputLine[i + 1]);
|
|
89
103
|
}
|
|
90
104
|
}
|
|
91
105
|
out = isolated.join("\n");
|
|
92
|
-
return { source: out, table };
|
|
106
|
+
return { source: out, table, lineMap };
|
|
93
107
|
}
|
|
94
108
|
/** Stash whole `{% raw %}…{% endraw %}` regions found in prose. */
|
|
95
109
|
function stashRawRegions(source, stash) {
|
|
@@ -170,29 +184,32 @@ function foldBlocks(nodes, table, nodesFor = "all") {
|
|
|
170
184
|
continue;
|
|
171
185
|
}
|
|
172
186
|
const entry = table[index];
|
|
187
|
+
// The node keeps the source line of the paragraph it replaces (ParseOptions.sourceLines)
|
|
188
|
+
const line = node.meta?.line;
|
|
189
|
+
const at = (made) => (line === undefined ? made : { ...made, meta: { ...made.meta, line } });
|
|
173
190
|
// resolve-first: only an include becomes a node; the rest is literal text.
|
|
174
191
|
if (nodesFor === "include" && entry.keyword !== "include") {
|
|
175
|
-
target().push({ type: "paragraph", inline: [{ type: "text", text: entry.raw }] });
|
|
192
|
+
target().push(at({ type: "paragraph", inline: [{ type: "text", text: entry.raw }] }));
|
|
176
193
|
continue;
|
|
177
194
|
}
|
|
178
195
|
const test = (entry.rest ?? "").trim();
|
|
179
196
|
if (entry.keyword === "if" && test) {
|
|
180
|
-
stack.push({ node: { type: "conditional", props: { test }, children: [] }, opener: entry });
|
|
197
|
+
stack.push({ node: at({ type: "conditional", props: { test }, children: [] }), opener: entry });
|
|
181
198
|
}
|
|
182
199
|
else if (entry.keyword === "endif") {
|
|
183
200
|
const open = stack.pop();
|
|
184
201
|
if (open)
|
|
185
202
|
target().push(open.node);
|
|
186
203
|
else
|
|
187
|
-
target().push(rawBlock(entry)); // orphan endif — preserved verbatim
|
|
204
|
+
target().push(at(rawBlock(entry))); // orphan endif — preserved verbatim
|
|
188
205
|
}
|
|
189
206
|
else if (entry.keyword === "elif" || entry.keyword === "else") {
|
|
190
207
|
// Not modeled first-class: preserved as raw chips inside (or beside)
|
|
191
208
|
// the conditional; the serializer emits them in place, byte-exact.
|
|
192
|
-
target().push(rawBlock(entry));
|
|
209
|
+
target().push(at(rawBlock(entry)));
|
|
193
210
|
}
|
|
194
211
|
else {
|
|
195
|
-
target().push(tagToBlock(entry));
|
|
212
|
+
target().push(at(tagToBlock(entry)));
|
|
196
213
|
}
|
|
197
214
|
}
|
|
198
215
|
// Unclosed conditionals: demote each opener to directive-raw and splice
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { extractHeadings, slugifyHeadingText, type HeadingInfo } from "./heading
|
|
|
7
7
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker } from "./code-regions.js";
|
|
8
8
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
|
9
9
|
export type { ParseResult, ParseOptions } from "./parse.js";
|
|
10
|
+
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
10
11
|
export { buildNavFromDirectory } from "./nav.js";
|
|
11
12
|
export type { BuildNavOptions } from "./nav.js";
|
|
12
13
|
export { loadNavFile } from "./nav-file.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { dogsbayMdToTree, parseInline } from "./parse.js";
|
|
|
9
9
|
export { extractHeadings, slugifyHeadingText } from "./headings.js";
|
|
10
10
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker } from "./code-regions.js";
|
|
11
11
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
|
12
|
+
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
12
13
|
export { buildNavFromDirectory } from "./nav.js";
|
|
13
14
|
export { loadNavFile } from "./nav-file.js";
|
|
14
15
|
export { renderDiagrams } from "./renderers/diagrams.js";
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { TreeNode, InlineNode } from "@dogsbay/types";
|
|
2
|
+
import { type ParseDiagnostic } from "./diagnostics.js";
|
|
2
3
|
export interface ParseResult {
|
|
3
4
|
tree: TreeNode[];
|
|
4
5
|
frontmatter: Record<string, unknown>;
|
|
6
|
+
/** With ParseOptions.diagnostics: what the parser recovered from, by line. */
|
|
7
|
+
diagnostics?: ParseDiagnostic[];
|
|
5
8
|
}
|
|
6
9
|
export interface ParseOptions {
|
|
7
10
|
/** Strip surrounding whitespace from block content. Default: true */
|
|
@@ -17,6 +20,19 @@ export interface ParseOptions {
|
|
|
17
20
|
* plans/editor-directive-model.md.
|
|
18
21
|
*/
|
|
19
22
|
directives?: "resolve-first" | "preserve";
|
|
23
|
+
/**
|
|
24
|
+
* Put each block node's 1-based line in the source (front matter counted)
|
|
25
|
+
* on `meta.line`, so a host can map the rendered page back to the source
|
|
26
|
+
* (render-html writes it as `data-line` when asked). Off by default. Lines
|
|
27
|
+
* end at `\n` or `\r\n`; a file with lone `\r` line breaks gets none.
|
|
28
|
+
*/
|
|
29
|
+
sourceLines?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Report what the parser recovers from silently, with source lines, in
|
|
32
|
+
* `diagnostics`: an unclosed `:::container`, an unknown `:::name` or inline
|
|
33
|
+
* `:name[]`, an attribute block it cannot read. Off by default.
|
|
34
|
+
*/
|
|
35
|
+
diagnostics?: boolean;
|
|
20
36
|
}
|
|
21
37
|
/**
|
|
22
38
|
* Parse Dogsbay Markdown into TreeNode[] plus extracted frontmatter.
|
package/dist/parse.js
CHANGED
|
@@ -21,6 +21,7 @@ import { mathPlugin } from "./plugin-math.js";
|
|
|
21
21
|
import { attrsToProps } from "./parse-attrs.js";
|
|
22
22
|
import { splitFrontmatter } from "./frontmatter.js";
|
|
23
23
|
import { protectDirectives, restoreDirectives } from "./directive-preserve.js";
|
|
24
|
+
import { closesContainer, collectDiagnostics } from "./diagnostics.js";
|
|
24
25
|
import { treeToDogsbayMd } from "./serialize.js";
|
|
25
26
|
/**
|
|
26
27
|
* Parse Dogsbay Markdown into TreeNode[] plus extracted frontmatter.
|
|
@@ -52,6 +53,11 @@ export function dogsbayMdToTree(source, options) {
|
|
|
52
53
|
// resolves them in a preprocessing pass beforehand. Protecting in both modes
|
|
53
54
|
// makes the contract true; the RESTORE below is what differs.
|
|
54
55
|
let directiveTable = null;
|
|
56
|
+
// Source lines: the line of the full source each line of `content` came from,
|
|
57
|
+
// through the front matter split and each pre-pass below
|
|
58
|
+
const wantLines = (options?.sourceLines === true || options?.diagnostics === true) && !/\r(?!\n)/.test(source);
|
|
59
|
+
const bodyStart = wantLines ? (source.slice(0, source.length - content.length).match(/\n/g) ?? []).length : 0;
|
|
60
|
+
let lineMap = null;
|
|
55
61
|
const preserveMode = options?.directives === "preserve";
|
|
56
62
|
{
|
|
57
63
|
// resolve-first leaves `{% raw %}` regions alone — see ProtectOptions.
|
|
@@ -61,17 +67,33 @@ export function dogsbayMdToTree(source, options) {
|
|
|
61
67
|
});
|
|
62
68
|
content = protectedResult.source;
|
|
63
69
|
directiveTable = protectedResult.table;
|
|
70
|
+
if (wantLines)
|
|
71
|
+
lineMap = protectedResult.lineMap;
|
|
64
72
|
}
|
|
65
73
|
// Pre-pass: lift YAML body forms (e.g. `:::card\n---\n...\n---\n:::`)
|
|
66
74
|
// into a synthetic `__yaml__` attr on the directive opener. Done at
|
|
67
75
|
// the source level because markdown-it would otherwise mangle the
|
|
68
76
|
// YAML — the leading `---` becomes a horizontal rule and the body's
|
|
69
77
|
// newlines collapse into a single inline run. See preprocessYamlBodies.
|
|
70
|
-
|
|
78
|
+
const yamlLifted = preprocessYamlBodies(content);
|
|
79
|
+
content = yamlLifted.source;
|
|
80
|
+
const lineOf = lineMap
|
|
81
|
+
? (line0) => {
|
|
82
|
+
const protectedLine = yamlLifted.lineMap[line0];
|
|
83
|
+
const input = protectedLine === undefined ? undefined : lineMap[protectedLine];
|
|
84
|
+
return input === undefined ? undefined : bodyStart + input + 1;
|
|
85
|
+
}
|
|
86
|
+
: undefined;
|
|
71
87
|
const md = sharedParser();
|
|
72
88
|
const tokens = md.parse(content, {});
|
|
73
89
|
markUnclosedGenericContainers(tokens, content);
|
|
74
|
-
|
|
90
|
+
// Before tokensToTree: it changes nothing these read, but read the parse as markdown-it left it
|
|
91
|
+
const diagnostics = options?.diagnostics
|
|
92
|
+
? collectDiagnostics(tokens,
|
|
93
|
+
// markdown-it splits at \r too; a lone-CR file has no line map, so its lines are counted here
|
|
94
|
+
content.split(/\r\n|\r|\n/), lineOf ?? ((line0) => (source.slice(0, source.length - content.length).match(/\r\n|\r|\n/g) ?? []).length + line0 + 1))
|
|
95
|
+
: undefined;
|
|
96
|
+
let tree = foldRelated(tokensToTree(tokens, options?.sourceLines ? lineOf : undefined));
|
|
75
97
|
if (directiveTable) {
|
|
76
98
|
// `preserve` restores every construct as a first-class node. `resolve-first`
|
|
77
99
|
// restores INCLUDES as nodes too — they are transclusions of real content,
|
|
@@ -81,7 +103,7 @@ export function dogsbayMdToTree(source, options) {
|
|
|
81
103
|
nodesFor: preserveMode ? "all" : "include",
|
|
82
104
|
});
|
|
83
105
|
}
|
|
84
|
-
return { tree, frontmatter };
|
|
106
|
+
return { tree, frontmatter, ...(diagnostics ? { diagnostics } : {}) };
|
|
85
107
|
}
|
|
86
108
|
// #384 R3: roles whose caption + following list fold into a semantic `related`
|
|
87
109
|
// node (maps to DITA <related-links>). Mirrors CONTAINER_ROLES in
|
|
@@ -180,6 +202,7 @@ const YAML_BODY_COMPONENTS = new Set([
|
|
|
180
202
|
function preprocessYamlBodies(source) {
|
|
181
203
|
const lines = source.split("\n");
|
|
182
204
|
const out = [];
|
|
205
|
+
const lineMap = [];
|
|
183
206
|
let i = 0;
|
|
184
207
|
while (i < lines.length) {
|
|
185
208
|
const line = lines[i];
|
|
@@ -210,14 +233,16 @@ function preprocessYamlBodies(source) {
|
|
|
210
233
|
newAttrs = `{__yaml__="${encoded}"}`;
|
|
211
234
|
}
|
|
212
235
|
out.push(`${fence}${name}${newAttrs}`);
|
|
236
|
+
lineMap.push(i);
|
|
213
237
|
i = j + 1; // skip past the closing `---`
|
|
214
238
|
continue;
|
|
215
239
|
}
|
|
216
240
|
}
|
|
217
241
|
out.push(line);
|
|
242
|
+
lineMap.push(i);
|
|
218
243
|
i++;
|
|
219
244
|
}
|
|
220
|
-
return out.join("\n");
|
|
245
|
+
return { source: out.join("\n"), lineMap };
|
|
221
246
|
}
|
|
222
247
|
/**
|
|
223
248
|
* Parse a single inline string to InlineNode[] (useful for testing).
|
|
@@ -285,20 +310,20 @@ function createParser() {
|
|
|
285
310
|
* holding the rest of the page, so such an opener is marked and parsed back as text.
|
|
286
311
|
*/
|
|
287
312
|
function markUnclosedGenericContainers(tokens, src) {
|
|
288
|
-
|
|
313
|
+
// markdown-it's lines: it splits at \r\n, \r and \n
|
|
314
|
+
const lines = src.split(/\r\n|\r|\n/);
|
|
289
315
|
const open = `container_${GENERIC_CONTAINER}_open`;
|
|
290
316
|
for (const tok of tokens) {
|
|
291
317
|
const map = tok.map;
|
|
292
318
|
if (tok.type !== open || !map)
|
|
293
319
|
continue;
|
|
294
|
-
|
|
295
|
-
if (close === undefined || !/^\s*:{3,}\s*$/.test(close))
|
|
320
|
+
if (!closesContainer(lines[map[1]], (tok.markup ?? ":::").length))
|
|
296
321
|
tok.meta = { ...(tok.meta ?? {}), unclosed: true };
|
|
297
322
|
}
|
|
298
323
|
}
|
|
299
|
-
function tokensToTree(tokens) {
|
|
324
|
+
function tokensToTree(tokens, lineOf) {
|
|
300
325
|
const nodes = [];
|
|
301
|
-
const ctx = { tokens, i: 0 };
|
|
326
|
+
const ctx = { tokens, i: 0, ...(lineOf ? { lineOf } : {}) };
|
|
302
327
|
while (ctx.i < ctx.tokens.length) {
|
|
303
328
|
const node = consumeBlock(ctx);
|
|
304
329
|
if (node) {
|
|
@@ -316,6 +341,20 @@ function tokensToTree(tokens) {
|
|
|
316
341
|
return nodes;
|
|
317
342
|
}
|
|
318
343
|
function consumeBlock(ctx) {
|
|
344
|
+
const tok = ctx.tokens[ctx.i];
|
|
345
|
+
const result = consumeBlockAt(ctx);
|
|
346
|
+
// Source lines: every node the block became starts at its first token's line
|
|
347
|
+
// (nested blocks came through here first and keep their own)
|
|
348
|
+
const line = ctx.lineOf && tok?.map ? ctx.lineOf(tok.map[0]) : undefined;
|
|
349
|
+
if (line !== undefined && result) {
|
|
350
|
+
for (const node of Array.isArray(result) ? result : [result]) {
|
|
351
|
+
if (node.meta?.line === undefined)
|
|
352
|
+
node.meta = { ...node.meta, line };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
function consumeBlockAt(ctx) {
|
|
319
358
|
const tok = ctx.tokens[ctx.i];
|
|
320
359
|
if (!tok)
|
|
321
360
|
return null;
|
|
@@ -581,6 +620,14 @@ function consumeList(ctx, ordered) {
|
|
|
581
620
|
return node;
|
|
582
621
|
}
|
|
583
622
|
function consumeListItem(ctx) {
|
|
623
|
+
const open = ctx.tokens[ctx.i];
|
|
624
|
+
const item = consumeListItemAt(ctx);
|
|
625
|
+
const line = ctx.lineOf && open?.map ? ctx.lineOf(open.map[0]) : undefined;
|
|
626
|
+
if (line !== undefined && item.meta?.line === undefined)
|
|
627
|
+
item.meta = { ...item.meta, line };
|
|
628
|
+
return item;
|
|
629
|
+
}
|
|
630
|
+
function consumeListItemAt(ctx) {
|
|
584
631
|
ctx.i++; // list_item_open
|
|
585
632
|
const children = [];
|
|
586
633
|
let depth = 1;
|
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.125",
|
|
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",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"markdown-it-deflist": "^3.0.1",
|
|
41
41
|
"markdown-it-footnote": "^4.0.0",
|
|
42
42
|
"markdown-it-front-matter": "^0.2.4",
|
|
43
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/serialize-core": "0.2.0-beta.
|
|
45
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
43
|
+
"@dogsbay/minja": "0.2.0-beta.125",
|
|
44
|
+
"@dogsbay/serialize-core": "0.2.0-beta.125",
|
|
45
|
+
"@dogsbay/types": "0.2.0-beta.125"
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"repository": {
|