dotmd-cli 0.76.7 → 0.76.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotmd-cli",
3
- "version": "0.76.7",
3
+ "version": "0.76.8",
4
4
  "description": "CLI for managing markdown documents with YAML frontmatter — index, query, validate, graph, export, lifecycle, and AI summaries.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,3 +1,5 @@
1
+ import { maskInlineCodeSpans } from './markdown-code-spans.mjs';
2
+
1
3
  export function extractFirstHeading(body) {
2
4
  return body.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? null;
3
5
  }
@@ -49,9 +51,7 @@ export function extractBodyLinks(body) {
49
51
  // every one of them was also never checked for breakage. Masking to same-length
50
52
  // filler keeps the link matchable while still neutralizing a link that is
51
53
  // itself inside code (`[fake](x.md)` stays unmatched), and preserves offsets.
52
- const stripped = body
53
- .replace(/^```[\s\S]*?^```/gm, '')
54
- .replace(/`[^`]+`/g, match => 'x'.repeat(match.length));
54
+ const stripped = maskInlineCodeSpans(body.replace(/^```[\s\S]*?^```/gm, ''));
55
55
  const links = [];
56
56
  // Supported inline destinations are a whitespace-free token (with Markdown
57
57
  // backslash escapes) or an angle-bracket destination, plus an optional
@@ -0,0 +1,47 @@
1
+ function maskRange(value, start, end) {
2
+ return value.slice(0, start)
3
+ + value.slice(start, end).replace(/[^\n]/g, 'x')
4
+ + value.slice(end);
5
+ }
6
+
7
+ // Markdown code spans close only on a backtick run of the same length as their
8
+ // opener. The mask is deliberately the same length as the source so consumers
9
+ // can inspect neutralized Markdown while applying any edits at source offsets.
10
+ // `state` lets the conservative multiline behavior used by the reference
11
+ // rewriter survive: after an unmatched opener, nothing is treated as prose
12
+ // until a compatible closer appears.
13
+ export function maskInlineCodeLine(line, state = { run: null }) {
14
+ const ranges = [];
15
+ const runs = [...line.matchAll(/`+/g)];
16
+ let index = 0;
17
+
18
+ if (state.run !== null) {
19
+ const closingIndex = runs.findIndex(candidate => candidate[0].length === state.run);
20
+ if (closingIndex === -1) return maskRange(line, 0, line.length);
21
+ const closing = runs[closingIndex];
22
+ ranges.push([0, closing.index + closing[0].length]);
23
+ index = closingIndex + 1;
24
+ state.run = null;
25
+ }
26
+
27
+ for (; index < runs.length; index++) {
28
+ const opening = runs[index];
29
+ const closingIndex = runs.findIndex((candidate, candidateIndex) =>
30
+ candidateIndex > index && candidate[0].length === opening[0].length);
31
+ if (closingIndex === -1) {
32
+ ranges.push([opening.index, line.length]);
33
+ state.run = opening[0].length;
34
+ break;
35
+ }
36
+ const closing = runs[closingIndex];
37
+ ranges.push([opening.index, closing.index + closing[0].length]);
38
+ index = closingIndex;
39
+ }
40
+
41
+ return ranges.reduceRight((masked, [start, end]) => maskRange(masked, start, end), line);
42
+ }
43
+
44
+ export function maskInlineCodeSpans(value) {
45
+ const state = { run: null };
46
+ return value.split('\n').map(line => maskInlineCodeLine(line, state)).join('\n');
47
+ }
@@ -1,6 +1,7 @@
1
1
  import { realpathSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { extractFrontmatter } from './frontmatter.mjs';
4
+ import { maskInlineCodeLine } from './markdown-code-spans.mjs';
4
5
 
5
6
  function slash(value) { return value.split(path.sep).join('/'); }
6
7
 
@@ -233,29 +234,37 @@ function rewriteDestination(raw, args) {
233
234
  return parsed.angle ? `<${rendered}>` : rendered;
234
235
  }
235
236
 
236
- function rewriteMarkdownSegment(segment, args) {
237
- // Inline links: angle destinations, escaped whitespace, optional titles.
238
- let next = segment.replace(/(\[[^\]]*\]\(\s*)(<[^>\n]+>|(?:\\.|[^\s()])+)(\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?(\s*\))/g,
239
- (match, prefix, destination, title = '', close, offset) => {
240
- let escapes = 0;
241
- for (let index = offset - 1; index >= 0 && segment[index] === '\\'; index--) escapes++;
242
- if (escapes % 2 === 1) return match;
243
- const rewritten = rewriteDestination(destination, args);
244
- return rewritten === destination ? match : `${prefix}${rewritten}${title}${close}`;
245
- });
237
+ function rewriteMarkdownLine(line, masked, args) {
238
+ const edits = [];
239
+ const inline = /(\[[^\]]*\]\(\s*)(<[^>\n]+>|(?:\\.|[^\s()])+)(\s+(?:"[^"]*"|'[^']*'|\([^)]*\)))?(\s*\))/g;
240
+ let match;
241
+ while ((match = inline.exec(masked)) !== null) {
242
+ let escapes = 0;
243
+ for (let index = match.index - 1; index >= 0 && line[index] === '\\'; index--) escapes++;
244
+ if (escapes % 2 === 1) continue;
245
+ const start = match.index + match[1].length;
246
+ const destination = line.slice(start, start + match[2].length);
247
+ const rewritten = rewriteDestination(destination, args);
248
+ if (rewritten !== destination) edits.push({ start, end: start + destination.length, value: rewritten });
249
+ }
250
+
246
251
  // Reference definitions preserve labels, spacing, destinations, and titles.
247
- next = next.replace(/^(\s{0,3}\[[^\]]+\]:\s*)(<[^>\n]+>|(?:\\.|[^\s])+)(.*)$/,
248
- (match, prefix, destination, tail) => {
249
- const rewritten = rewriteDestination(destination, args);
250
- return rewritten === destination ? match : `${prefix}${rewritten}${tail}`;
251
- });
252
- return next;
252
+ const definition = /^(\s{0,3}\[[^\]]+\]:\s*)(<[^>\n]+>|(?:\\.|[^\s])+)(.*)$/.exec(masked);
253
+ if (definition) {
254
+ const start = definition[1].length;
255
+ const destination = line.slice(start, start + definition[2].length);
256
+ const rewritten = rewriteDestination(destination, args);
257
+ if (rewritten !== destination) edits.push({ start, end: start + destination.length, value: rewritten });
258
+ }
259
+
260
+ return edits.sort((left, right) => right.start - left.start)
261
+ .reduce((next, edit) => next.slice(0, edit.start) + edit.value + next.slice(edit.end), line);
253
262
  }
254
263
 
255
264
  function rewriteMarkdown(body, args) {
256
265
  const lines = body.split('\n');
257
266
  let fence = null;
258
- let inlineRun = null;
267
+ const inlineState = { run: null };
259
268
  let indentedCode = false;
260
269
  let htmlBlock = null;
261
270
  const containerContext = line => {
@@ -317,36 +326,7 @@ function rewriteMarkdown(body, args) {
317
326
  fence = { char: opener[1][0], length: opener[1].length, prefixes: container.prefixes };
318
327
  return line;
319
328
  }
320
- let output = '';
321
- let cursor = 0;
322
- const runs = [...line.matchAll(/`+/g)];
323
- let index = 0;
324
- if (inlineRun !== null) {
325
- const closingIndex = runs.findIndex(candidate => candidate[0].length === inlineRun);
326
- if (closingIndex === -1) return line;
327
- const closing = runs[closingIndex];
328
- output += line.slice(0, closing.index + closing[0].length);
329
- cursor = closing.index + closing[0].length;
330
- index = closingIndex + 1;
331
- inlineRun = null;
332
- }
333
- for (; index < runs.length; index++) {
334
- const opening = runs[index];
335
- const closingIndex = runs.findIndex((candidate, candidateIndex) => candidateIndex > index && candidate[0].length === opening[0].length);
336
- if (closingIndex === -1) {
337
- output += rewriteMarkdownSegment(line.slice(cursor, opening.index), args);
338
- output += line.slice(opening.index);
339
- inlineRun = opening[0].length;
340
- cursor = line.length;
341
- break;
342
- }
343
- const closing = runs[closingIndex];
344
- output += rewriteMarkdownSegment(line.slice(cursor, opening.index), args);
345
- output += line.slice(opening.index, closing.index + closing[0].length);
346
- cursor = closing.index + closing[0].length;
347
- index = closingIndex;
348
- }
349
- return output + rewriteMarkdownSegment(line.slice(cursor), args);
329
+ return rewriteMarkdownLine(line, maskInlineCodeLine(line, inlineState), args);
350
330
  }).join('\n');
351
331
  }
352
332