@helping-ai-workflow/md2doc 3.17.2 → 3.17.4

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/lib/md2doc.js CHANGED
@@ -379,6 +379,9 @@ function renderEditModeList(listToken, blocks, biRef, out) {
379
379
  const check = b.task
380
380
  ? `<span class="ed-li-check" data-checked="${b.checked ? 1 : 0}" role="checkbox" aria-checked="${!!b.checked}"></span>`
381
381
  : '';
382
+ // v3.17.3 C10: an svg in the item's own text gets the same white plate
383
+ // in dark as a top-level svg block (the attribute is read by the dark CSS).
384
+ const svgPlate = ownTokens.some((tk) => /<svg[\s>]/i.test(String(tk.raw || ''))) ? ' data-svg-plate="1"' : '';
382
385
  const runStart = liRunStartsHere(biRef, b) ? ' data-run-start="1"' : '';
383
386
  const listStartAttr = listStart ? ' data-list-start="1"' : '';
384
387
  listStart = false;
@@ -389,7 +392,7 @@ function renderEditModeList(listToken, blocks, biRef, out) {
389
392
  parts.push(
390
393
  `<div class="ed-block" data-block-id="${b.id}" data-block-type="li"` +
391
394
  ` data-list-type="${b.listType}" data-task="${b.task ? 1 : 0}" data-indent="${b.indent}"` +
392
- runStart + listStartAttr + looseAttr + ` style="--ed-indent:${b.indent}">` +
395
+ runStart + listStartAttr + looseAttr + svgPlate + ` style="--ed-indent:${b.indent}">` +
393
396
  `<span class="ed-li-marker" aria-hidden="true"></span>` + check +
394
397
  `<div class="ed-li-text">${inner}</div>` +
395
398
  `</div>`
@@ -1079,6 +1082,13 @@ ${itemsHtml}
1079
1082
  openSvg = 0;
1080
1083
  return isSvgOnly(src);
1081
1084
  }
1085
+ // v3.17.4 E5: an svg that is real inline html among a block's own inline
1086
+ // tokens (a code span naming it is not one). The reader marks the block
1087
+ // with data-svg-plate so dark mode plates it, whatever text sits beside it.
1088
+ function hasInlineSvg(toks) {
1089
+ return Array.isArray(toks) && toks.some((tk) => tk && ((tk.type === 'html' && /<svg[\s>]/i.test(String(tk.raw || ''))) ||
1090
+ (tk.type === 'text' && hasInlineSvg(tk.tokens))));
1091
+ }
1082
1092
 
1083
1093
  const baseHtml = renderer.html.bind(renderer);
1084
1094
  renderer.html = function(token) {
@@ -1238,6 +1248,9 @@ ${itemsHtml}
1238
1248
  };
1239
1249
  const baseParagraph = renderer.paragraph.bind(renderer);
1240
1250
  renderer.paragraph = function(token) {
1251
+ // A loose item's text paragraph is made up by marked from the already
1252
+ // rendered body, so its svg is real html there (a code span is escaped).
1253
+ const madeUp = skipMadeUpParagraph;
1241
1254
  if (skipMadeUpParagraph) {
1242
1255
  skipMadeUpParagraph = false;
1243
1256
  } else {
@@ -1247,6 +1260,9 @@ ${itemsHtml}
1247
1260
  if (!opts.editMode && wrapsSvgParagraph(token.raw)) {
1248
1261
  return baseParagraph(token).replace(/^<p>/, '<p class="inline-svg">');
1249
1262
  }
1263
+ if (!opts.editMode && (hasInlineSvg(token.tokens) || (madeUp && /<svg[\s>]/i.test(String(token.text || ''))))) {
1264
+ return baseParagraph(token).replace(/^<p>/, '<p data-svg-plate="1">');
1265
+ }
1250
1266
  return baseParagraph(token);
1251
1267
  };
1252
1268
 
@@ -1261,7 +1277,11 @@ ${itemsHtml}
1261
1277
  } finally {
1262
1278
  looseItemText = outerLoose;
1263
1279
  }
1264
- if (!token.task) return html;
1280
+ // v3.17.4 E5: a tight item's own text sits straight in the li (no p). Marked
1281
+ // last, because the task wrapping below anchors on a bare li tag.
1282
+ const plateTight = (h) => (!opts.editMode && !token.loose && hasInlineSvg(token.tokens))
1283
+ ? h.replace(/^<li( class="[^"]*")?/, '$& data-svg-plate="1"') : h;
1284
+ if (!token.task) return plateTight(html);
1265
1285
  // v3.10.0: a task item drops its bullet and keeps its own text in a span,
1266
1286
  // so a checked item's strike-through stops at its own words — CSS
1267
1287
  // text-decoration cannot be undone on a nested child list. Shapes: tight
@@ -1270,8 +1290,8 @@ ${itemsHtml}
1270
1290
  // first: a block child's opening tag, the loose item's </p>, or </li>.
1271
1291
  const cls = token.checked ? 'task-item is-checked' : 'task-item';
1272
1292
  const m = html.match(/^<li>([\s\S]*?<input[^>]*>)([\s\S]*?)(<(?:ul|ol|blockquote|pre|table|div|p|h[1-6])[\s>][\s\S]*|<\/p>[\s\S]*|<\/li>\s*)$/);
1273
- if (!m) return html.replace(/^<li>/, '<li class="' + cls + '">');
1274
- return '<li class="' + cls + '">' + m[1] + ' <span class="task-text">' + m[2].trim() + '</span>' + m[3];
1293
+ if (!m) return plateTight(html.replace(/^<li>/, '<li class="' + cls + '">'));
1294
+ return plateTight('<li class="' + cls + '">' + m[1] + ' <span class="task-text">' + m[2].trim() + '</span>' + m[3]);
1275
1295
  };
1276
1296
 
1277
1297
  const baseBlockquote = renderer.blockquote.bind(renderer);
@@ -1402,7 +1422,7 @@ ${itemsHtml}
1402
1422
  // v3.14.0 T10: the reader wraps an svg-only block in .inline-svg for the
1403
1423
  // dark white plate; edit mode keeps its DOM, so it marks the wrapper.
1404
1424
  const svgPlate = ((t.type === 'html' && t.block && wrapsSvg(t.text)) ||
1405
- (t.type === 'paragraph' && wrapsSvgParagraph(t.raw))) ? ' data-svg-plate="1"' : '';
1425
+ (t.type === 'paragraph' && (wrapsSvgParagraph(t.raw) || hasInlineSvg(t.tokens)))) ? ' data-svg-plate="1"' : '';
1406
1426
  parts.push(
1407
1427
  `<div class="ed-block" data-block-id="${b.id}" data-block-type="${b.type}"${svgPlate}>` +
1408
1428
  inner + '</div>'
@@ -2767,6 +2787,7 @@ const html = `<!DOCTYPE html>
2767
2787
  border-radius: 3px;
2768
2788
  }
2769
2789
  mark.search-hit-block { display: block; }
2790
+ svg .svg-hit { stroke: var(--md-svg-hit); stroke-width: 6px; stroke-linejoin: round; paint-order: stroke; }
2770
2791
  .toc {
2771
2792
  display: flex;
2772
2793
  flex-direction: column;
@@ -4799,6 +4820,15 @@ ${mermaidInitTag}`)
4799
4820
  }
4800
4821
 
4801
4822
  const SKIP_SELECTOR = 'svg, .mermaid, .graphviz, script, style';
4823
+ // v3.17.3 C11: a Markdown-native inline svg is read (its text is in the
4824
+ // search index); a diagram's own svg (mermaid, graphviz, draw.io, WaveDrom,
4825
+ // a formula) is not. Inside a native svg every text node is walked, the
4826
+ // ones in its title and desc elements too: the index holds those as well
4827
+ // (the paragraph's text tokens between the html tags), so a query that
4828
+ // finds the section can find them here.
4829
+ function isNativeSvg(el) {
4830
+ return el.matches('svg') && !el.closest('.mermaid, .graphviz, .drawio, .katex, .code-block, [class*="wave"], [id^="WaveDrom"]');
4831
+ }
4802
4832
 
4803
4833
  function buildSnippet(section, query) {
4804
4834
  const haystack = section.searchText || section.title || '';
@@ -4845,6 +4875,7 @@ ${mermaidInitTag}`)
4845
4875
  ? readerState.activeHighlights
4846
4876
  : (readerState.activeHighlight ? [readerState.activeHighlight] : []);
4847
4877
  marks.forEach((mark) => {
4878
+ if (mark.namespaceURI === 'http://www.w3.org/2000/svg') { mark.classList.remove('svg-hit'); if (!mark.getAttribute('class')) mark.removeAttribute('class'); return; }
4848
4879
  const parent = mark.parentNode;
4849
4880
  if (!parent) return;
4850
4881
  while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
@@ -4975,7 +5006,8 @@ ${mermaidInitTag}`)
4975
5006
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
4976
5007
  acceptNode(node) {
4977
5008
  if (node.nodeType === Node.ELEMENT_NODE) {
4978
- if (node.matches(SKIP_SELECTOR) || node.matches('.reader-sidebar, .heading-anchor, button')) return NodeFilter.FILTER_REJECT;
5009
+ if (node.matches(SKIP_SELECTOR) && !isNativeSvg(node)) return NodeFilter.FILTER_REJECT;
5010
+ if (node.matches('.reader-sidebar, .heading-anchor, button')) return NodeFilter.FILTER_REJECT;
4979
5011
  return node.matches('br, .katex') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
4980
5012
  }
4981
5013
  if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
@@ -5065,6 +5097,14 @@ ${mermaidInitTag}`)
5065
5097
  const marks = [];
5066
5098
  Array.from(spans.keys()).sort((a, b) => b - a).forEach((ni) => {
5067
5099
  const node = nodes[ni];
5100
+ // v3.17.3 C11: svg cannot hold a mark element; the text node's parent
5101
+ // (text or tspan) is flagged instead and painted by .svg-hit.
5102
+ if (node.nodeType === Node.TEXT_NODE && node.parentElement && node.parentElement.closest('svg')) {
5103
+ // Two text nodes can share one parent (a tspan or a comment splits it).
5104
+ node.parentElement.classList.add('svg-hit');
5105
+ if (marks.indexOf(node.parentElement) === -1) marks.unshift(node.parentElement);
5106
+ return;
5107
+ }
5068
5108
  const mark = document.createElement('mark');
5069
5109
  mark.className = 'search-hit is-selected';
5070
5110
  if (node.nodeType === Node.ELEMENT_NODE) {
@@ -34,6 +34,9 @@ const THEME_TOKENS = [
34
34
  { name: 'active-bg', light: ['#dbe6f3'], dark: '#24364f' },
35
35
  { name: 'hit-bg', light: ['#fff5c2'], dark: '#4d4417' },
36
36
  { name: 'mark-bg', light: ['#fde68a'], dark: '#6e5e14' },
37
+ // A search hit inside an inline svg is painted on the svg plate, which is white in both themes,
38
+ // so its halo is the same yellow in both (the dark mark-bg would read worse than no hit at all).
39
+ { name: 'svg-hit', light: ['#fde68a'], dark: '#fde68a', direct: true },
37
40
  { name: 'code', light: ['#b93a0c'], dark: '#ff9f70' },
38
41
  { name: 'accent-soft', light: ['#93c5fd'], dark: '#6ea8f5' },
39
42
  { name: 'error', light: ['#b91c1c'], dark: '#ff8a8a' },
@@ -165,7 +168,8 @@ function themeCss() {
165
168
  ' ' + D + ' .content .drawio { background: #ffffff; border-radius: 6px; box-shadow: 0 0 0 10px #ffffff; }',
166
169
  // R7 甲: a Markdown-native inline svg (wrapped in .inline-svg by the renderer) gets the
167
170
  // same white plate; its ink stays the light text colour because the plate is always white.
168
- ' ' + D + ' .content .inline-svg > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] > p > svg { background: #ffffff; color: #1f2328; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
171
+ // A task item's svg sits one level down, inside span.task-text (tight li or loose p).
172
+ ' ' + D + ' .content .inline-svg > svg, ' + D + ' .content p[data-svg-plate="1"] > svg, ' + D + ' .content li[data-svg-plate="1"] > svg, ' + D + ' .content li[data-svg-plate="1"] > p > svg, ' + D + ' .content li[data-svg-plate="1"] > .task-text > svg, ' + D + ' .content p[data-svg-plate="1"] > .task-text > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] > p > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] .ed-li-text > svg, ' + D + ' .content .ed-block[data-svg-plate="1"] .ed-li-text > p > svg { background: #ffffff; color: #1f2328; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
169
173
  ' ' + D + ' .lightbox-canvas > svg[data-md2doc-recoloured] { background: var(--md-bg); }',
170
174
  ' }',
171
175
  ' .md2doc-theme-toggle { position: fixed; right: 20px; bottom: 20px; z-index: 60; width: 40px; height: 40px;',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helping-ai-workflow/md2doc",
3
- "version": "3.17.2",
3
+ "version": "3.17.4",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",