@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 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
- content = preprocessYamlBodies(content);
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
- let tree = foldRelated(tokensToTree(tokens));
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
- const lines = src.split("\n");
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
- const close = lines[map[1]];
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.124",
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.124",
44
- "@dogsbay/serialize-core": "0.2.0-beta.124",
45
- "@dogsbay/types": "0.2.0-beta.124"
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": {