@dogsbay/format-dogsbay-md 0.2.0-beta.129 → 0.2.0-beta.130

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.
@@ -4,6 +4,12 @@ export interface ParseDiagnostic {
4
4
  /** Stable kind, for filtering. */
5
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
+ /**
8
+ * The directive's name, without colons, on `unknown-container` and `unknown-inline`: a host that
9
+ * also reports unknown names (render-html's `ctx.unknown`) can tell it is the same one, without
10
+ * reading it out of the message.
11
+ */
12
+ name?: string;
7
13
  }
8
14
  /**
9
15
  * Whether a source line closes a container opened with `colons` colons. The
@@ -114,12 +114,17 @@ function youtubeAsBlock(map, closed, lines, around, lineOf) {
114
114
  return `takes the closer on line ${lineOf(map[1]) ?? "?"}, which leaves :::${parent.name} open`;
115
115
  return undefined;
116
116
  }
117
+ /** A source line quoted in a message, trimmed and cut short: a closer may carry a comment of any length. */
118
+ const clip = (line) => {
119
+ const t = (line ?? "").trim();
120
+ return t.length > 60 ? `${t.slice(0, 59)}…` : t;
121
+ };
117
122
  export function collectDiagnostics(tokens, lines, lineOf) {
118
123
  const out = [];
119
- const add = (line0, code, message) => {
124
+ const add = (line0, code, message, name) => {
120
125
  const line = line0 === undefined ? undefined : lineOf(line0);
121
126
  if (line !== undefined)
122
- out.push({ line, code, message });
127
+ out.push({ line, code, message, ...(name === undefined ? {} : { name }) });
123
128
  };
124
129
  const code = codeLineSet(tokens);
125
130
  let row;
@@ -149,10 +154,10 @@ export function collectDiagnostics(tokens, lines, lineOf) {
149
154
  }
150
155
  else if (!closed && legacy !== undefined && !generic) {
151
156
  // Before the nesting stack, the closer on that line ended this block and the ones inside it
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)`);
157
+ 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)`);
153
158
  }
154
159
  else if (!closed && skipped !== undefined) {
155
- add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${lines[skipped]?.trim()} on line ${lineOf(skipped) ?? "?"} is inside a code block, so it does not close it (add a closer after the code)`);
160
+ 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)`);
156
161
  }
157
162
  else if (!closed) {
158
163
  add(tok.map[0], "unclosed-container", generic
@@ -160,7 +165,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
160
165
  : `:::${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)})`);
161
166
  }
162
167
  else if (generic) {
163
- add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`);
168
+ add(tok.map[0], "unknown-container", `:::${name} is not a known directive — it is shown as a plain box`, name);
164
169
  }
165
170
  if (tok.meta?.unverified) {
166
171
  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`);
@@ -186,7 +191,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
186
191
  if (tok.type === "inline" && tok.map) {
187
192
  (tok.content ?? "").split("\n").forEach((l, k) => {
188
193
  if (/^\s*:{3,}[ \t]*(?:<!--(?:(?!-->)[\s\S])*-->[ \t]*)?$/.test(l)) {
189
- add(tok.map[0] + k, "stray-closer", `stray \`${l.trim()}\` — it closes nothing and is shown as text: every block before it is already closed (each block takes one closer)`);
194
+ 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)`);
190
195
  }
191
196
  });
192
197
  }
@@ -207,7 +212,7 @@ export function collectDiagnostics(tokens, lines, lineOf) {
207
212
  add(tok.map[0] + 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`);
208
213
  }
209
214
  else {
210
- add(tok.map[0] + offset, "unknown-inline", `${shown} is not a known inline directive — it is shown as text`);
215
+ add(tok.map[0] + offset, "unknown-inline", `${shown} is not a known inline directive — it is shown as text`, child.tag);
211
216
  }
212
217
  }
213
218
  }
package/dist/parse.js CHANGED
@@ -27,6 +27,13 @@ import { treeToDogsbayMd } from "./serialize.js";
27
27
  * Parse Dogsbay Markdown into TreeNode[] plus extracted frontmatter.
28
28
  */
29
29
  export function dogsbayMdToTree(source, options) {
30
+ // Lone-CR line breaks: the pre-passes' line maps count `\n` only, so diagnostics read a copy with
31
+ // `\n` breaks — markdown-it breaks lines at a lone `\r` too, so every line keeps its number.
32
+ // The tree still comes from the source as given.
33
+ if (options?.diagnostics && /\r(?!\n)/.test(source)) {
34
+ const { diagnostics } = dogsbayMdToTree(source.replace(/\r(?!\n)/g, "\n"), { ...options, sourceLines: false });
35
+ return { ...dogsbayMdToTree(source, { ...options, diagnostics: false }), ...(diagnostics ? { diagnostics } : {}) };
36
+ }
30
37
  // Extract frontmatter first (js-yaml splitter — browser-safe, no node:fs).
31
38
  // Fall back to parsing source as-is if frontmatter is malformed.
32
39
  let content = source;
@@ -90,11 +97,8 @@ export function dogsbayMdToTree(source, options) {
90
97
  keepListItemBraces(tokens, content);
91
98
  attachTableAttrsRow(tokens, content);
92
99
  // Before tokensToTree: it changes nothing these read, but read the parse as markdown-it left it
93
- const diagnostics = options?.diagnostics
94
- ? collectDiagnostics(tokens,
95
- // markdown-it splits at \r too; a lone-CR file has no line map, so its lines are counted here
96
- content.split(/\r\n|\r|\n/), lineOf ?? ((line0) => (source.slice(0, source.length - content.length).match(/\r\n|\r|\n/g) ?? []).length + line0 + 1))
97
- : undefined;
100
+ // A lone-CR source never gets here with diagnostics (see the top), so `lineOf` is set
101
+ const diagnostics = options?.diagnostics ? collectDiagnostics(tokens, content.split(/\r\n|\n/), lineOf ?? (() => undefined)) : undefined;
98
102
  let tree = foldRelated(tokensToTree(tokens, options?.sourceLines ? lineOf : undefined));
99
103
  if (directiveTable) {
100
104
  // `preserve` restores every construct as a first-class node. `resolve-first`
@@ -1605,7 +1609,14 @@ function tokensToInline(tokens, inheritedFmt = []) {
1605
1609
  break;
1606
1610
  }
1607
1611
  case "softbreak": {
1608
- // Treat as space in inline output
1612
+ // Treat as space in inline output, but say where a bold run ended its line (stepLead)
1613
+ // (markdown-it leaves an empty text token between the bold's close and the break)
1614
+ let k = result.length - 1;
1615
+ while (k > 0 && result[k].type === "text" && result[k].text === "")
1616
+ k--;
1617
+ const last = result[k];
1618
+ if (last && !fmtStack.includes("bold") && isBoldRun(last))
1619
+ last.lineEnd = true;
1609
1620
  result.push(makeText(" ", fmtStack));
1610
1621
  break;
1611
1622
  }
@@ -1806,7 +1817,7 @@ function mergeAdjacentText(nodes) {
1806
1817
  !!prev.strikethrough === !!node.strikethrough) {
1807
1818
  // Mutate-on-replace: build a fresh node so callers holding a
1808
1819
  // reference to the original `prev` don't see in-place edits.
1809
- out[out.length - 1] = { ...prev, text: prev.text + node.text };
1820
+ out[out.length - 1] = { ...prev, text: prev.text + node.text, ...(node.lineEnd ? { lineEnd: true } : {}) };
1810
1821
  }
1811
1822
  else {
1812
1823
  out.push(node);
@@ -1814,6 +1825,15 @@ function mergeAdjacentText(nodes) {
1814
1825
  }
1815
1826
  return out;
1816
1827
  }
1828
+ /** Bold text, or a link whose text is all bold (`**Install the [CLI](…)**`). */
1829
+ function isBoldRun(node) {
1830
+ if (node.type === "text")
1831
+ return node.bold === true && node.text !== "";
1832
+ if (node.type !== "link")
1833
+ return false;
1834
+ const texts = (node.children ?? []).filter((c) => c.type === "text");
1835
+ return texts.length > 0 && texts.every((c) => c.type === "text" && c.bold === true);
1836
+ }
1817
1837
  function makeText(text, fmtStack) {
1818
1838
  const node = { type: "text", text };
1819
1839
  if (fmtStack.includes("bold"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/format-dogsbay-md",
3
- "version": "0.2.0-beta.129",
3
+ "version": "0.2.0-beta.130",
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-footnote": "^4.0.0",
41
41
  "markdown-it-front-matter": "^0.2.4",
42
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"
43
+ "@dogsbay/minja": "0.2.0-beta.130",
44
+ "@dogsbay/serialize-core": "0.2.0-beta.130",
45
+ "@dogsbay/types": "0.2.0-beta.130"
46
46
  },
47
47
  "license": "MIT",
48
48
  "repository": {