@dogsbay/format-dogsbay-md 0.2.0-beta.142 → 0.2.0-beta.144
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 +9 -1
- package/dist/diagnostics.js +83 -29
- package/dist/directive-descriptions.d.ts +11 -0
- package/dist/directive-descriptions.js +71 -0
- package/dist/directive-props.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/parse.js +49 -4
- package/package.json +5 -5
package/dist/browser.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js"
|
|
|
16
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 { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
19
|
+
export { DIRECTIVE_DESCRIPTIONS, INLINE_DIRECTIVE_DESCRIPTIONS } from "./directive-descriptions.js";
|
|
19
20
|
export type { SerializeOptions } from "./types.js";
|
|
20
21
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
21
22
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
|
package/dist/browser.js
CHANGED
|
@@ -18,6 +18,7 @@ export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } f
|
|
|
18
18
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
19
19
|
// The props each known directive draws (an editor completes prop names from it)
|
|
20
20
|
export { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
21
|
+
export { DIRECTIVE_DESCRIPTIONS, INLINE_DIRECTIVE_DESCRIPTIONS } from "./directive-descriptions.js";
|
|
21
22
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
22
23
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker, } from "./code-regions.js";
|
|
23
24
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ export interface ParseDiagnostic {
|
|
|
10
10
|
* reading it out of the message.
|
|
11
11
|
*/
|
|
12
12
|
name?: string;
|
|
13
|
+
/**
|
|
14
|
+
* 1-based column where the problem starts on `line`, and `endColumn` just past its end (also
|
|
15
|
+
* 1-based, exclusive), when the parser knows which part of the line it is about: the `:::name`
|
|
16
|
+
* opener, the prop key, the closer's comment, the `{…}` block. Found in the SOURCE line, so a
|
|
17
|
+
* pre-pass that rewrites the text cannot shift it. Left out, the whole line is meant.
|
|
18
|
+
*/
|
|
19
|
+
column?: number;
|
|
20
|
+
endColumn?: number;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
23
|
* Whether a source line closes a container opened with `colons` colons. The
|
|
@@ -47,5 +55,5 @@ type Token = {
|
|
|
47
55
|
* missing brace or an unterminated quote loses attributes silently.
|
|
48
56
|
*/
|
|
49
57
|
export declare function attributeProblem(block: string): string | undefined;
|
|
50
|
-
export declare function collectDiagnostics(tokens: Token[], lines: string[], lineOf: (line0: number) => number | undefined): ParseDiagnostic[];
|
|
58
|
+
export declare function collectDiagnostics(tokens: Token[], lines: string[], lineOf: (line0: number) => number | undefined, sourceLine?: (line: number) => string | undefined): ParseDiagnostic[];
|
|
51
59
|
export {};
|
package/dist/diagnostics.js
CHANGED
|
@@ -12,6 +12,36 @@ import { BLOCK_LEAF_DIRECTIVES, GENERIC_CONTAINER, LEAF_ONLY_DIRECTIVES } from "
|
|
|
12
12
|
import { LABEL_PROP } from "./plugin-inline-directives.js";
|
|
13
13
|
import { DIRECTIVE_PROPS, unknownProps } from "./directive-props.js";
|
|
14
14
|
import * as yaml from "js-yaml";
|
|
15
|
+
/** The first match of `re` at or after `from`; a group named `at` narrows it (a key before its `=`). */
|
|
16
|
+
function find(re, mask) {
|
|
17
|
+
const span = (text, from) => {
|
|
18
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
19
|
+
g.lastIndex = from;
|
|
20
|
+
const m = g.exec(mask ? mask(text) : text);
|
|
21
|
+
if (!m || m[0].length === 0)
|
|
22
|
+
return undefined;
|
|
23
|
+
const at = m.groups?.at;
|
|
24
|
+
const start = at !== undefined ? m.index + m[0].indexOf(at) : m.index;
|
|
25
|
+
return [start, start + (at ?? m[0]).length];
|
|
26
|
+
};
|
|
27
|
+
span.pattern = re.source;
|
|
28
|
+
return span;
|
|
29
|
+
}
|
|
30
|
+
/** Inline code spans blanked, offsets kept: a directive shown in code is not the one meant. */
|
|
31
|
+
const maskCode = (text) => text.replace(/(`+)[\s\S]*?\1/g, (c) => " ".repeat(c.length));
|
|
32
|
+
/** Quoted attribute values blanked, offsets kept: a key's name inside another's value is not the key. */
|
|
33
|
+
const maskQuotes = (text) => text.replace(/"[^"]*"|'[^']*'/g, (c) => " ".repeat(c.length));
|
|
34
|
+
/**
|
|
35
|
+
* The attribute block a line ends with (`{…}`, maybe unclosed, then spaces), found from the last
|
|
36
|
+
* `{` so the test is linear: anchored at `$`, the regex backtracked quadratically on a long line.
|
|
37
|
+
*/
|
|
38
|
+
function trailingBraces(text, inner = /^\{[^{}]*\}?$/) {
|
|
39
|
+
const t = text.trimEnd();
|
|
40
|
+
const i = t.lastIndexOf("{");
|
|
41
|
+
return i >= 0 && inner.test(t.slice(i)) ? [i, t.length] : undefined;
|
|
42
|
+
}
|
|
43
|
+
const HEADING_ATTRS = /^\{\s*(?:[#.][^{}]*|[\w-]+\s*=[^{}]*)\}?$/;
|
|
44
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
45
|
/**
|
|
16
46
|
* Whether a source line closes a container opened with `colons` colons. The
|
|
17
47
|
* line may sit in a blockquote or a list item: its `>` markers and
|
|
@@ -155,12 +185,32 @@ function propKeysOfAttrs(props) {
|
|
|
155
185
|
}
|
|
156
186
|
return keys;
|
|
157
187
|
}
|
|
158
|
-
export function collectDiagnostics(tokens, lines, lineOf) {
|
|
188
|
+
export function collectDiagnostics(tokens, lines, lineOf, sourceLine) {
|
|
159
189
|
const out = [];
|
|
160
|
-
|
|
190
|
+
// Where the last span of each (line, code) ended: two of one kind on a line take successive matches
|
|
191
|
+
const cursor = new Map();
|
|
192
|
+
const add = (line0, code, message, name, span) => {
|
|
161
193
|
const line = line0 === undefined ? undefined : lineOf(line0);
|
|
162
|
-
if (line
|
|
163
|
-
|
|
194
|
+
if (line === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const text = span && sourceLine?.(line);
|
|
197
|
+
let cols;
|
|
198
|
+
if (text !== undefined && span) {
|
|
199
|
+
const locate = span instanceof RegExp ? find(span) : span;
|
|
200
|
+
// Per pattern too: bold `{…}` and `:kbd[…]{…}` on one line are found independently
|
|
201
|
+
const key = `${line}\0${code}\0${locate.pattern ?? ""}`;
|
|
202
|
+
cols = locate(text, cursor.get(key) ?? 0);
|
|
203
|
+
if (cols)
|
|
204
|
+
cursor.set(key, cols[1]);
|
|
205
|
+
}
|
|
206
|
+
out.push({ line, code, message, ...(name === undefined ? {} : { name }), ...(cols ? { column: cols[0] + 1, endColumn: cols[1] + 1 } : {}) });
|
|
207
|
+
};
|
|
208
|
+
const opener = (name) => new RegExp(`:{3,}[ \\t]*${escapeRe(name)}(?![\\w-])`);
|
|
209
|
+
const BRACES = (text) => trailingBraces(text);
|
|
210
|
+
/** Lines into an inline token where each child starts: soft and hard breaks move to the next. */
|
|
211
|
+
const childLines = (children) => {
|
|
212
|
+
let n = 0;
|
|
213
|
+
return children.map((c) => (c.type === "softbreak" || c.type === "hardbreak" ? n++ : n));
|
|
164
214
|
};
|
|
165
215
|
const code = codeLineSet(tokens);
|
|
166
216
|
let row;
|
|
@@ -186,22 +236,22 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
186
236
|
// (plans/youtube-leaf.md). A blank line in a quote is `>`, so quote markers are not content.
|
|
187
237
|
const leafAsBlock = name === "youtube" ? youtubeAsBlock(tok.map, closed, lines, around, lineOf) : undefined;
|
|
188
238
|
if (leafAsBlock) {
|
|
189
|
-
add(tok.map[0], "leaf-as-block", `a video has no body: write ::youtube{…} on one line, with no closer — as :::youtube it ${leafAsBlock}
|
|
239
|
+
add(tok.map[0], "leaf-as-block", `a video has no body: write ::youtube{…} on one line, with no closer — as :::youtube it ${leafAsBlock}`, undefined, opener(name));
|
|
190
240
|
}
|
|
191
241
|
else if (!closed && legacy !== undefined && !generic) {
|
|
192
242
|
// Before the nesting stack, the closer on that line ended this block and the ones inside it
|
|
193
|
-
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${clip(lines[legacy])} on line ${lineOf(legacy) ?? "?"} closes only the innermost open block; each block needs its own closer (one \`:::\` used to close several)
|
|
243
|
+
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${clip(lines[legacy])} on line ${lineOf(legacy) ?? "?"} closes only the innermost open block; each block needs its own closer (one \`:::\` used to close several)`, undefined, opener(name));
|
|
194
244
|
}
|
|
195
245
|
else if (!closed && skipped !== undefined) {
|
|
196
|
-
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${clip(lines[skipped])} on line ${lineOf(skipped) ?? "?"} is inside a code block, so it does not close it (add a closer after the code)
|
|
246
|
+
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${clip(lines[skipped])} on line ${lineOf(skipped) ?? "?"} is inside a code block, so it does not close it (add a closer after the code)`, undefined, opener(name));
|
|
197
247
|
}
|
|
198
248
|
else if (!closed) {
|
|
199
249
|
add(tok.map[0], "unclosed-container", generic
|
|
200
250
|
? `:::${name} is never closed — it is shown as text (close it with a line of ${":".repeat(colons)})`
|
|
201
|
-
: `:::${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)})
|
|
251
|
+
: `:::${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)})`, undefined, opener(name));
|
|
202
252
|
}
|
|
203
253
|
else if (generic) {
|
|
204
|
-
add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`, name);
|
|
254
|
+
add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`, name, opener(name));
|
|
205
255
|
}
|
|
206
256
|
else {
|
|
207
257
|
// A prop no renderer of this directive reads is carried, never drawn: say so (a typo
|
|
@@ -209,26 +259,28 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
209
259
|
// lifted into `__yaml__` by preprocessYamlBodies.
|
|
210
260
|
// markdown-it-attrs put the opener's `{…}` on the token's attrs; the lifted YAML block is `__yaml__`
|
|
211
261
|
const pairs = (tok.attrs ?? []);
|
|
262
|
+
const inline = new Set(pairs.map(([k]) => k));
|
|
212
263
|
for (const key of unknownProps(name, propKeysOfAttrs(Object.fromEntries(pairs)))) {
|
|
213
|
-
|
|
264
|
+
// A key from a `---` props block is on another line: no column
|
|
265
|
+
add(tok.map[0], "unknown-prop", `${key} is not a prop of :::${name} — it is kept but not drawn`, name, inline.has(key) ? find(new RegExp(`(?<![\\w-])(?<at>${escapeRe(key)})\\s*=`), maskQuotes) : undefined);
|
|
214
266
|
}
|
|
215
267
|
}
|
|
216
268
|
if (tok.meta?.unverified) {
|
|
217
|
-
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
|
|
269
|
+
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`, undefined, opener(name));
|
|
218
270
|
}
|
|
219
271
|
if (closed) {
|
|
220
272
|
// The closer's end-tag label: it closes this container whatever it says, so a wrong
|
|
221
273
|
// name means the author thinks it closes another one
|
|
222
274
|
const label = closerLabel((lines[tok.map[1]] ?? "").replace(/^[\s>]*:+/, ""));
|
|
223
275
|
if (label && label !== name) {
|
|
224
|
-
add(tok.map[1], "closer-mismatch", `this closer is labelled ${label}, but it closes :::${name} (opened on line ${lineOf(tok.map[0]) ?? "?"}) — check which block you meant to end here
|
|
276
|
+
add(tok.map[1], "closer-mismatch", `this closer is labelled ${label}, but it closes :::${name} (opened on line ${lineOf(tok.map[0]) ?? "?"}) — check which block you meant to end here`, undefined, /<!--[\s\S]*?-->/);
|
|
225
277
|
}
|
|
226
278
|
}
|
|
227
279
|
const rest = info.slice(name.length).trim();
|
|
228
280
|
if (rest.startsWith("{")) {
|
|
229
281
|
const problem = attributeProblem(rest);
|
|
230
282
|
if (problem)
|
|
231
|
-
add(tok.map[0], "bad-attributes", `the attributes of :::${name} ${problem} — they are ignored
|
|
283
|
+
add(tok.map[0], "bad-attributes", `the attributes of :::${name} ${problem} — they are ignored`, undefined, /\{.*$/);
|
|
232
284
|
}
|
|
233
285
|
// An unknown block never closed is read back as text, so it is no parent (but still popped)
|
|
234
286
|
around.push({ map: tok.map, closed: closed || (generic && tok.meta?.unclosed === true), name });
|
|
@@ -237,7 +289,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
237
289
|
if (tok.type === "inline" && tok.map) {
|
|
238
290
|
(tok.content ?? "").split("\n").forEach((l, k) => {
|
|
239
291
|
if (/^\s*:{3,}[ \t]*(?:<!--(?:(?!-->)[\s\S])*-->[ \t]*)?$/.test(l)) {
|
|
240
|
-
add(tok.map[0] + k, "stray-closer", `stray \`${clip(l)}\` — it closes nothing and is shown as text: every block before it is already closed (each block takes one closer)
|
|
292
|
+
add(tok.map[0] + k, "stray-closer", `stray \`${clip(l)}\` — it closes nothing and is shown as text: every block before it is already closed (each block takes one closer)`, undefined, /:{3,}/);
|
|
241
293
|
}
|
|
242
294
|
});
|
|
243
295
|
}
|
|
@@ -264,7 +316,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
264
316
|
const at = text.indexOf(form, from);
|
|
265
317
|
from = at >= 0 ? at + form.length : from;
|
|
266
318
|
const offset = at >= 0 ? (text.slice(0, at).match(/\n/g) ?? []).length : 0;
|
|
267
|
-
add(base + offset, "missing-label", `:${child.tag}{…} has no label — it is shown as text; write :${child.tag}[…]{…} or :${child.tag}{${key}="…"}`);
|
|
319
|
+
add(base + offset, "missing-label", `:${child.tag}{…} has no label — it is shown as text; write :${child.tag}[…]{…} or :${child.tag}{${key}="…"}`, undefined, find(new RegExp(`(?<!:):${escapeRe(child.tag)}\\{[^{}]*\\}?`), maskCode));
|
|
268
320
|
}
|
|
269
321
|
continue;
|
|
270
322
|
}
|
|
@@ -294,21 +346,21 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
294
346
|
const shown = child.type === "dogsbay_leaf_directive" ? `::${child.tag}` : `:${child.tag}[…]`;
|
|
295
347
|
if (child.type === "dogsbay_leaf_directive" && BLOCK_DIRECTIVES.has(child.tag)) {
|
|
296
348
|
if (labelled) {
|
|
297
|
-
add(base + offset, "block-directive-inline", `${shown}[…] is shown as text: a block directive takes no [label]. ${blockForm(child.tag)}
|
|
349
|
+
add(base + offset, "block-directive-inline", `${shown}[…] is shown as text: a block directive takes no [label]. ${blockForm(child.tag)}`, undefined, find(new RegExp(`::${escapeRe(child.tag)}\\[[^\\]]*\\]?`), maskCode));
|
|
298
350
|
}
|
|
299
351
|
else {
|
|
300
|
-
add(base + offset, "block-directive-inline", `${shown} is a block directive — inside a line of text it is shown as text; put it on a line of its own`);
|
|
352
|
+
add(base + offset, "block-directive-inline", `${shown} is a block directive — inside a line of text it is shown as text; put it on a line of its own`, undefined, find(new RegExp(`::${escapeRe(child.tag)}(?![\\w-])`), maskCode));
|
|
301
353
|
}
|
|
302
354
|
}
|
|
303
355
|
else {
|
|
304
|
-
add(base + offset, "unknown-inline", `${shown} is not a known inline directive — it is shown as text`, child.tag);
|
|
356
|
+
add(base + offset, "unknown-inline", `${shown} is not a known inline directive — it is shown as text`, child.tag, find(child.type === "dogsbay_leaf_directive" ? new RegExp(`::${escapeRe(child.tag)}(?![\\w-])`) : new RegExp(`(?<!:):${escapeRe(child.tag)}\\[[^\\]]*\\]?`), maskCode));
|
|
305
357
|
}
|
|
306
358
|
}
|
|
307
359
|
}
|
|
308
360
|
// Attribute blocks markdown-it-attrs read and the tree has nowhere to keep
|
|
309
361
|
if (tok.type === "list_item_open" && tok.map && tok.attrs?.length) {
|
|
310
362
|
// parse.ts keepListItemBraces puts trailing braces back as text, or on a heading; what is left had nowhere to go
|
|
311
|
-
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
|
|
363
|
+
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`, undefined, BRACES);
|
|
312
364
|
}
|
|
313
365
|
// A table row that is only an attribute block, where the parser could not move it onto the
|
|
314
366
|
// table (not the last row, or text follows the table with no blank line): an empty row
|
|
@@ -318,14 +370,14 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
318
370
|
cells.push(list[k]);
|
|
319
371
|
const inlines = cells.filter((c) => c.type === "inline");
|
|
320
372
|
if (cells[0]?.attrs?.length && /^\{[^{}]*\}$/.test((inlines[0]?.content ?? "").trim()) && inlines.slice(1).every((c) => c.content.trim() === "")) {
|
|
321
|
-
add(tok.map[0], "dropped-attributes", `this attribute block is read as an empty table row — put it on the line directly under the table's last row, or after a blank line
|
|
373
|
+
add(tok.map[0], "dropped-attributes", `this attribute block is read as an empty table row — put it on the line directly under the table's last row, or after a blank line`, undefined, BRACES);
|
|
322
374
|
}
|
|
323
375
|
}
|
|
324
376
|
// markdown-it-attrs gives a list a block on the line after it (or after its last item's text); the tree keeps no list attributes
|
|
325
377
|
// Only real attribute names: markdown-it-attrs also reads a leftover include (`{* src.py hl[4] *}`, FastAPI) as keys
|
|
326
378
|
const own = tok.attrs?.filter(([k]) => k !== "start") ?? [];
|
|
327
379
|
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))) {
|
|
328
|
-
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
|
|
380
|
+
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`, undefined, BRACES);
|
|
329
381
|
}
|
|
330
382
|
// An attribute block alone on its line after a blank line: an empty paragraph that carries it
|
|
331
383
|
if (tok.type === "paragraph_open" && tok.map && tok.attrs?.length) {
|
|
@@ -334,17 +386,18 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
334
386
|
// leftover MDX expression (`{/* prettier-ignore */}`), which is an importer's problem, not the author's
|
|
335
387
|
const looksLikeAttrs = /^\{\s*(?:[#.][^\s{}]|[\w-]+\s*=)/.test(inline?.content.trim() ?? "");
|
|
336
388
|
if (inline?.type === "inline" && looksLikeAttrs && (inline.children ?? []).every((c) => c.type === "text" && c.content.trim() === "")) {
|
|
337
|
-
add(tok.map[0], "dropped-attributes", `this attribute block is on a line of its own and applies to nothing — put it at the end of the block's last line
|
|
389
|
+
add(tok.map[0], "dropped-attributes", `this attribute block is on a line of its own and applies to nothing — put it at the end of the block's last line`, undefined, BRACES);
|
|
338
390
|
}
|
|
339
391
|
}
|
|
340
392
|
if (tok.type === "inline" && tok.map && tok.children) {
|
|
341
|
-
|
|
393
|
+
const at = childLines(tok.children);
|
|
394
|
+
for (const [k, child] of tok.children.entries()) {
|
|
342
395
|
const what = ATTRLESS_INLINE[child.type];
|
|
343
396
|
if (what && child.attrs?.length)
|
|
344
|
-
add(tok.map[0], "dropped-attributes", `the attribute block on ${what} is dropped — only links, images and inline directives take attributes
|
|
397
|
+
add(tok.map[0] + at[k], "dropped-attributes", `the attribute block on ${what} is dropped — only links, images and inline directives take attributes`, undefined, /(?<=[*_`~])\{[^{}]*\}/);
|
|
345
398
|
}
|
|
346
399
|
// Inline directives that keep no attributes (only badge and button do)
|
|
347
|
-
for (const child of tok.children) {
|
|
400
|
+
for (const [k, child] of tok.children.entries()) {
|
|
348
401
|
if (child.type !== "dogsbay_inline_directive_open" || child.tag === "badge" || child.tag === "button" || !KNOWN_INLINE_DIRECTIVES.has(child.tag))
|
|
349
402
|
continue;
|
|
350
403
|
let parsed = {};
|
|
@@ -363,7 +416,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
363
416
|
delete props[key];
|
|
364
417
|
}
|
|
365
418
|
if (parsed.id || parsed.classes?.length || Object.keys(props).length) {
|
|
366
|
-
add(tok.map[0], "dropped-attributes", `the attribute block on :${child.tag}[…] is dropped — of the inline directives only :badge and :button take attributes`);
|
|
419
|
+
add(tok.map[0] + at[k], "dropped-attributes", `the attribute block on :${child.tag}[…] is dropped — of the inline directives only :badge and :button take attributes`, undefined, find(new RegExp(`:${escapeRe(child.tag)}\\[[^\\]]*\\](?<at>\\{[^{}]*\\})`), maskCode));
|
|
367
420
|
}
|
|
368
421
|
}
|
|
369
422
|
}
|
|
@@ -375,10 +428,11 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
375
428
|
const inline = list[i + 1];
|
|
376
429
|
// markdown-it-attrs takes a block it reads out of the text tokens (not out of `content`)
|
|
377
430
|
const last = [...(inline?.children ?? [])].reverse().find((c) => c.type === "text");
|
|
378
|
-
const
|
|
431
|
+
const content = last?.content ?? "";
|
|
432
|
+
const tail = trailingBraces(content, HEADING_ATTRS);
|
|
379
433
|
if (tail) {
|
|
380
|
-
const problem = attributeProblem(tail[0]) ?? "could not be read";
|
|
381
|
-
add(tok.map[0], "bad-attributes", `the heading's attribute block ${problem} — it is shown as text
|
|
434
|
+
const problem = attributeProblem(content.slice(tail[0])) ?? "could not be read";
|
|
435
|
+
add(tok.map[0], "bad-attributes", `the heading's attribute block ${problem} — it is shown as text`, undefined, (text) => trailingBraces(text, HEADING_ATTRS));
|
|
382
436
|
}
|
|
383
437
|
}
|
|
384
438
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line per directive name, for an editor's completion detail (the VS Code extension asked,
|
|
3
|
+
* 2026-10-29): the name lists (`KNOWN_BLOCK_DIRECTIVES`, `LEAF_ONLY_DIRECTIVES`,
|
|
4
|
+
* `KNOWN_INLINE_DIRECTIVES`) are bare strings. Block and leaf names share one map; an inline
|
|
5
|
+
* directive (`:name[…]`) means something else under the same name (`:math[x]` vs `:::math`),
|
|
6
|
+
* so it has its own. A test fails on a listed name with no description.
|
|
7
|
+
*/
|
|
8
|
+
/** `:::name` / `::name` — block and leaf directives. */
|
|
9
|
+
export declare const DIRECTIVE_DESCRIPTIONS: Readonly<Record<string, string>>;
|
|
10
|
+
/** `:name[…]{…}` — inline directives. */
|
|
11
|
+
export declare const INLINE_DIRECTIVE_DESCRIPTIONS: Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One line per directive name, for an editor's completion detail (the VS Code extension asked,
|
|
3
|
+
* 2026-10-29): the name lists (`KNOWN_BLOCK_DIRECTIVES`, `LEAF_ONLY_DIRECTIVES`,
|
|
4
|
+
* `KNOWN_INLINE_DIRECTIVES`) are bare strings. Block and leaf names share one map; an inline
|
|
5
|
+
* directive (`:name[…]`) means something else under the same name (`:math[x]` vs `:::math`),
|
|
6
|
+
* so it has its own. A test fails on a listed name with no description.
|
|
7
|
+
*/
|
|
8
|
+
const CALLOUT = (kind) => `Callout: ${kind}. \`title\`, \`collapsed\` for a fold`;
|
|
9
|
+
/** `:::name` / `::name` — block and leaf directives. */
|
|
10
|
+
export const DIRECTIVE_DESCRIPTIONS = {
|
|
11
|
+
tabs: "Tab group; each `:::tab[Label]` is one tab. `syncKey` syncs groups across the site",
|
|
12
|
+
tab: "One tab of a `:::tabs` group, named by its label",
|
|
13
|
+
steps: "Numbered procedure; each `:::step[Title]` is one step",
|
|
14
|
+
step: "One step of a `:::steps` procedure",
|
|
15
|
+
cards: "Card grid; `:::card` children, or a list of links. `cols` sets the columns",
|
|
16
|
+
card: "Card with a title, optional `href`, `icon` and `image`",
|
|
17
|
+
"link-card": "Card that is one link: `href`, `title`, `description`",
|
|
18
|
+
grid: "Layout grid of `::grid-item` cells. `cols`, `gap`",
|
|
19
|
+
"grid-item": "One cell of a `:::grid`; `span`, `rowSpan`",
|
|
20
|
+
details: "Collapsible section; the label is the summary. `open` starts expanded",
|
|
21
|
+
callout: "Callout of a given `type` (note, tip, warning, …)",
|
|
22
|
+
note: CALLOUT("note"),
|
|
23
|
+
tip: CALLOUT("tip"),
|
|
24
|
+
info: CALLOUT("info"),
|
|
25
|
+
important: CALLOUT("important"),
|
|
26
|
+
warning: CALLOUT("warning"),
|
|
27
|
+
caution: CALLOUT("caution"),
|
|
28
|
+
danger: CALLOUT("danger"),
|
|
29
|
+
success: CALLOUT("success"),
|
|
30
|
+
hint: CALLOUT("hint"),
|
|
31
|
+
attention: CALLOUT("attention"),
|
|
32
|
+
error: CALLOUT("error"),
|
|
33
|
+
failure: CALLOUT("failure"),
|
|
34
|
+
bug: CALLOUT("bug"),
|
|
35
|
+
question: CALLOUT("question"),
|
|
36
|
+
abstract: CALLOUT("abstract"),
|
|
37
|
+
quote: CALLOUT("quote"),
|
|
38
|
+
button: "Link styled as a button: `href`, `variant`, `size`",
|
|
39
|
+
figure: "Image or content with a `caption`",
|
|
40
|
+
math: "Display math: the LaTeX as its body, or `::math{latex=\"…\"}`; same as `$$…$$`",
|
|
41
|
+
"api-endpoint": "API operation card (method, path, description)",
|
|
42
|
+
"api-params": "API parameter table",
|
|
43
|
+
"api-response": "API response with status and schema",
|
|
44
|
+
config: "Configuration reference block",
|
|
45
|
+
files: "File tree, from a nested list",
|
|
46
|
+
example: "Example: rendered output beside its source",
|
|
47
|
+
accordion: "Accordion; `:::accordion-item` children. `type` single or multiple",
|
|
48
|
+
"accordion-item": "One fold of an `:::accordion`, titled by its label",
|
|
49
|
+
autodoc: "Python API reference generated from source at build time",
|
|
50
|
+
"api-class": "Generated API reference: a class",
|
|
51
|
+
"api-member": "Generated API reference: a member",
|
|
52
|
+
"api-doc": "Generated API reference: a docstring",
|
|
53
|
+
"api-source": "Generated API reference: source code",
|
|
54
|
+
youtube: "YouTube video, loaded on click: `::youtube{#id title=\"…\"}`",
|
|
55
|
+
openapi: "API reference from an OpenAPI spec: `spec=\"/path/openapi.yaml\"`",
|
|
56
|
+
theme: "Content in another theme's tokens: `name=\"dark\"`",
|
|
57
|
+
avatar: "Avatar image with a fallback: `src`, `alt`, `fallback`, `size`",
|
|
58
|
+
"child-links": "Links to this page's children in the menu; `depth`",
|
|
59
|
+
"data-table": "Table from a data file: `src`, `path` to a list in it, `columns`, `headers`",
|
|
60
|
+
"data-cards": "Cards from a data file: `src`, `path`; `title`, `description`, `href`, `icon` name its keys",
|
|
61
|
+
};
|
|
62
|
+
/** `:name[…]{…}` — inline directives. */
|
|
63
|
+
export const INLINE_DIRECTIVE_DESCRIPTIONS = {
|
|
64
|
+
kbd: "Keyboard key: `:kbd[Ctrl+C]`",
|
|
65
|
+
icon: "Icon by name: `:icon[check]`",
|
|
66
|
+
math: "Inline math: `:math[x^2]`",
|
|
67
|
+
mark: "Highlighted text",
|
|
68
|
+
highlight: "Highlighted text (same as `mark`)",
|
|
69
|
+
button: "Inline link button: `:button[Label]{href=\"…\"}`",
|
|
70
|
+
badge: "Badge: `:badge[New]{variant=\"…\"}`",
|
|
71
|
+
};
|
package/dist/directive-props.js
CHANGED
|
@@ -31,9 +31,10 @@ export const DIRECTIVE_PROPS = {
|
|
|
31
31
|
avatar: ["src", "alt", "fallback", "source", "size"],
|
|
32
32
|
accordion: ["type", "defaultValue", "variant"],
|
|
33
33
|
"accordion-item": ["title", "label", "value", "open"],
|
|
34
|
-
math: [],
|
|
35
|
-
|
|
36
|
-
"data-
|
|
34
|
+
math: ["latex"],
|
|
35
|
+
// What data-directives.ts reads (docs/data-files.md)
|
|
36
|
+
"data-table": ["src", "path", "columns", "headers"],
|
|
37
|
+
"data-cards": ["src", "path", "title", "description", "href", "icon"],
|
|
37
38
|
"child-links": ["depth", "title"],
|
|
38
39
|
};
|
|
39
40
|
/** Keys of `props` that `name` does not draw; empty when the directive is not checked. */
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { ParseResult, ParseOptions } from "./parse.js";
|
|
|
10
10
|
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
11
11
|
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
12
12
|
export { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
13
|
+
export { DIRECTIVE_DESCRIPTIONS, INLINE_DIRECTIVE_DESCRIPTIONS } from "./directive-descriptions.js";
|
|
13
14
|
export { buildNavFromDirectory } from "./nav.js";
|
|
14
15
|
export type { BuildNavOptions } from "./nav.js";
|
|
15
16
|
export { loadNavFile } from "./nav-file.js";
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
|
13
13
|
// The `:::name` blocks the parser knows (an editor completes names from it)
|
|
14
14
|
export { KNOWN_BLOCK_DIRECTIVES, LEAF_ONLY_DIRECTIVES, BLOCK_LEAF_DIRECTIVES } from "./plugin-containers.js";
|
|
15
15
|
export { DIRECTIVE_PROPS, PASSTHROUGH_PROP, unknownProps } from "./directive-props.js";
|
|
16
|
+
export { DIRECTIVE_DESCRIPTIONS, INLINE_DIRECTIVE_DESCRIPTIONS } from "./directive-descriptions.js";
|
|
16
17
|
export { buildNavFromDirectory } from "./nav.js";
|
|
17
18
|
export { loadNavFile } from "./nav-file.js";
|
|
18
19
|
export { renderDiagrams } from "./renderers/diagrams.js";
|
package/dist/parse.js
CHANGED
|
@@ -25,6 +25,11 @@ import { splitFrontmatter } from "./frontmatter.js";
|
|
|
25
25
|
import { protectDirectives, restoreDirectives } from "./directive-preserve.js";
|
|
26
26
|
import { closesContainer, collectDiagnostics } from "./diagnostics.js";
|
|
27
27
|
import { treeToDogsbayMd } from "./serialize.js";
|
|
28
|
+
/** A 1-based line of the source as written, for a diagnostic's columns (split once, on first use). */
|
|
29
|
+
function sourceLineOf(source) {
|
|
30
|
+
let lines;
|
|
31
|
+
return (line) => (lines ??= source.split(/\r\n|\n/))[line - 1];
|
|
32
|
+
}
|
|
28
33
|
/**
|
|
29
34
|
* Parse Dogsbay Markdown into TreeNode[] plus extracted frontmatter.
|
|
30
35
|
*/
|
|
@@ -100,7 +105,7 @@ export function dogsbayMdToTree(source, options) {
|
|
|
100
105
|
attachTableAttrsRow(tokens, content);
|
|
101
106
|
// Before tokensToTree: it changes nothing these read, but read the parse as markdown-it left it
|
|
102
107
|
// A lone-CR source never gets here with diagnostics (see the top), so `lineOf` is set
|
|
103
|
-
let diagnostics = options?.diagnostics ? collectDiagnostics(tokens, content.split(/\r\n|\n/), lineOf ?? (() => undefined)) : undefined;
|
|
108
|
+
let diagnostics = options?.diagnostics ? collectDiagnostics(tokens, content.split(/\r\n|\n/), lineOf ?? (() => undefined), sourceLineOf(source)) : undefined;
|
|
104
109
|
if (diagnostics && lineOf && yamlLifted.badProps.length) {
|
|
105
110
|
for (const bad of yamlLifted.badProps) {
|
|
106
111
|
// The line YAML stops at, mapped itself: protected directives can add or stash lines in
|
|
@@ -112,7 +117,7 @@ export function dogsbayMdToTree(source, options) {
|
|
|
112
117
|
}
|
|
113
118
|
diagnostics = diagnostics.sort((a, b) => a.line - b.line);
|
|
114
119
|
}
|
|
115
|
-
let tree = foldRelated(tokensToTree(tokens, options?.sourceLines ? lineOf : undefined));
|
|
120
|
+
let tree = foldRelated(tokensToTree(tokens, options?.sourceLines ? lineOf : undefined, content.split(/\r\n|\n/)));
|
|
116
121
|
if (directiveTable) {
|
|
117
122
|
// `preserve` restores every construct as a first-class node. `resolve-first`
|
|
118
123
|
// restores INCLUDES as nodes too — they are transclusions of real content,
|
|
@@ -518,9 +523,9 @@ function markUnclosedGenericContainers(tokens, src) {
|
|
|
518
523
|
tok.meta = { ...(tok.meta ?? {}), unclosed: true };
|
|
519
524
|
}
|
|
520
525
|
}
|
|
521
|
-
function tokensToTree(tokens, lineOf) {
|
|
526
|
+
function tokensToTree(tokens, lineOf, lines) {
|
|
522
527
|
const nodes = [];
|
|
523
|
-
const ctx = { tokens, i: 0, ...(lineOf ? { lineOf } : {}) };
|
|
528
|
+
const ctx = { tokens, i: 0, ...(lineOf ? { lineOf } : {}), ...(lines ? { lines } : {}) };
|
|
524
529
|
while (ctx.i < ctx.tokens.length) {
|
|
525
530
|
const node = consumeBlock(ctx);
|
|
526
531
|
if (node) {
|
|
@@ -1173,6 +1178,13 @@ function consumeContainer(ctx) {
|
|
|
1173
1178
|
ctx.i++;
|
|
1174
1179
|
}
|
|
1175
1180
|
}
|
|
1181
|
+
// A `:::math` body is LaTeX, not Markdown: read its lines as written (parsed, `\\` lost a
|
|
1182
|
+
// backslash and `{aligned}` became attributes), and give it the shape `$$…$$` has, which every
|
|
1183
|
+
// renderer draws. Its children were rendered by nothing: render-html drew an empty block.
|
|
1184
|
+
if (name === "math" && registered !== GENERIC_CONTAINER) {
|
|
1185
|
+
const latex = openTok.map && ctx.lines ? containerBody(ctx.lines, openTok.map) : children.map(nodeText).join("\n");
|
|
1186
|
+
return { type: "math-block", props: { latex } };
|
|
1187
|
+
}
|
|
1176
1188
|
if (registered === GENERIC_CONTAINER) {
|
|
1177
1189
|
// `:::name` with no closing `:::`: the line was prose (as before generic containers), not a block
|
|
1178
1190
|
// that swallows the rest of the page.
|
|
@@ -1196,6 +1208,34 @@ function consumeContainer(ctx) {
|
|
|
1196
1208
|
const node = mapContainerToTreeNode(name, attrs, children);
|
|
1197
1209
|
return closer && !Array.isArray(node) ? { ...node, meta: { ...(node.meta ?? {}), closer } } : node;
|
|
1198
1210
|
}
|
|
1211
|
+
/**
|
|
1212
|
+
* A container's body lines as written: between its opener and its closer (`map[1]`, or the end
|
|
1213
|
+
* when unclosed), less the opener's own indent and `>` markers, blank lines at either end dropped.
|
|
1214
|
+
*/
|
|
1215
|
+
function containerBody(lines, map) {
|
|
1216
|
+
const prefix = /^[ \t>]*/.exec(lines[map[0]] ?? "")[0];
|
|
1217
|
+
const body = lines.slice(map[0] + 1, Math.min(map[1], lines.length)).map((l) => {
|
|
1218
|
+
if (l.startsWith(prefix))
|
|
1219
|
+
return l.slice(prefix.length);
|
|
1220
|
+
// A lazy or blank quote line: drop what of the prefix it has
|
|
1221
|
+
let k = 0;
|
|
1222
|
+
while (k < prefix.length && k < l.length && l[k] === prefix[k])
|
|
1223
|
+
k++;
|
|
1224
|
+
return l.slice(k);
|
|
1225
|
+
});
|
|
1226
|
+
while (body.length && body[0].trim() === "")
|
|
1227
|
+
body.shift();
|
|
1228
|
+
while (body.length && body[body.length - 1].trim() === "")
|
|
1229
|
+
body.pop();
|
|
1230
|
+
return body.join("\n");
|
|
1231
|
+
}
|
|
1232
|
+
/** A block's text, its paragraphs' inline text joined (the fallback with no source lines). */
|
|
1233
|
+
function nodeText(n) {
|
|
1234
|
+
const inline = n.inline;
|
|
1235
|
+
if (inline)
|
|
1236
|
+
return inline.map((x) => x.text ?? x.latex ?? "").join("");
|
|
1237
|
+
return (n.children ?? []).map(nodeText).join("\n");
|
|
1238
|
+
}
|
|
1199
1239
|
/**
|
|
1200
1240
|
* Convert a container + children into the appropriate TreeNode(s).
|
|
1201
1241
|
* Handles callout type aliases, tabs definition-list recovery, cards list form, etc.
|
|
@@ -1251,6 +1291,11 @@ function mapContainerToTreeNode(name, attrs, children) {
|
|
|
1251
1291
|
"quote",
|
|
1252
1292
|
"callout",
|
|
1253
1293
|
]);
|
|
1294
|
+
// `::math{latex="…"}` (the leaf in docs/dogsbay-md.md): the shape `$$…$$` has. As a `math`
|
|
1295
|
+
// block it was written back as nothing and drawn empty.
|
|
1296
|
+
if (name === "math" && typeof props.latex === "string" && children.length === 0) {
|
|
1297
|
+
return { type: "math-block", props: { latex: props.latex } };
|
|
1298
|
+
}
|
|
1254
1299
|
if (CALLOUT_TYPES.has(name)) {
|
|
1255
1300
|
const calloutProps = { ...props };
|
|
1256
1301
|
if (name !== "callout")
|
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.144",
|
|
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,10 +40,10 @@
|
|
|
40
40
|
"markdown-it-footnote": "^4.0.0",
|
|
41
41
|
"markdown-it-front-matter": "^0.2.4",
|
|
42
42
|
"pele": "1.6.0",
|
|
43
|
-
"@dogsbay/corpus": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
45
|
-
"@dogsbay/
|
|
46
|
-
"@dogsbay/
|
|
43
|
+
"@dogsbay/corpus": "0.2.0-beta.144",
|
|
44
|
+
"@dogsbay/minja": "0.2.0-beta.144",
|
|
45
|
+
"@dogsbay/serialize-core": "0.2.0-beta.144",
|
|
46
|
+
"@dogsbay/types": "0.2.0-beta.144"
|
|
47
47
|
},
|
|
48
48
|
"license": "MIT",
|
|
49
49
|
"repository": {
|