@helping-ai-workflow/md2doc 3.13.1 → 3.14.0

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.
@@ -267,6 +267,10 @@
267
267
  // STRONG, DEL/S, U, the citation label) only ever needs `.md`, because
268
268
  // its own outer delimiter is a fixed, known tag regardless of what's
269
269
  // nested inside.
270
+ function markIsSearchHit(node) {
271
+ if (typeof node.getAttribute !== 'function') return false;
272
+ return (' ' + (node.getAttribute('class') || '') + ' ').indexOf(' search-hit ') !== -1;
273
+ }
270
274
  function walkChildren(nodes, unsupported) {
271
275
  let out = '';
272
276
  let firstSegment = true;
@@ -292,6 +296,15 @@
292
296
  firstSegment = false;
293
297
  continue;
294
298
  }
299
+ if (name === 'MARK' && markIsSearchHit(node)) {
300
+ // v3.14.0 T9: the reader's search paints its hits as this element and
301
+ // it is transient, so it serializes as what it wraps.
302
+ const inner = walkChildren(node.childNodes, unsupported);
303
+ out += inner.md;
304
+ if (inner.md.length) lastMarkTag = inner.lastMarkTag;
305
+ firstSegment = false;
306
+ continue;
307
+ }
295
308
  if (name === 'SPAN') {
296
309
  if (spanHasAttributes(node)) {
297
310
  unsupported.push(name);
@@ -356,6 +369,14 @@
356
369
  firstSegment = false;
357
370
  continue;
358
371
  }
372
+ if (name === 'BR' && typeof node.getAttribute === 'function' &&
373
+ node.getAttribute('data-ed-tail') === '1') {
374
+ // v3.14.0 T8: the editor's tail <br> (insertBrAtCaret() in client.js)
375
+ // only gives a line break at the end of a surface a line to stand on.
376
+ // It is not content: nothing written, and like an empty text node it
377
+ // leaves lastMarkTag / firstSegment as they were.
378
+ continue;
379
+ }
359
380
  if (name === 'BR') {
360
381
  // Spec §3.12. Two different things arrive here as the same element:
361
382
  // * a <br> the EDIT-MODE renderer marked (lib/md2doc.js's
@@ -101,7 +101,7 @@ const MARK_IDS = Object.freeze(Object.keys(MARK_TAG_BY_ID));
101
101
  // 'inert' pressing the button would do nothing at all
102
102
 
103
103
  // ctx: {blockType, indent, headingDepth, inList, listOrdered, hasSelection,
104
- // marks, dirty}
104
+ // marks, dirty, transient}
105
105
  // -> { [id]: {active: bool, mixed: bool, disabled: bool, label?: string} }
106
106
  function deriveState(ctx) {
107
107
  const c = ctx || {};
@@ -151,6 +151,16 @@ function deriveState(ctx) {
151
151
  // primary case (spec review ruling: gating it on inList made it dead for
152
152
  // that case).
153
153
 
154
+ // v3.14.0 T4 (D15): an EMPTY transient paragraph has no text to convert, so
155
+ // the conversions that wrap existing text (quote, numbered list, check) and
156
+ // the list-depth buttons stay off until it has some. ctx.transient is
157
+ // 'empty' | 'text' | null; the client derives it from the DOM.
158
+ if (c.transient === 'empty') {
159
+ for (const id of ['quote', 'ordered-list', 'check', 'indent', 'outdent']) {
160
+ state[id].disabled = true;
161
+ }
162
+ }
163
+
154
164
  // No block at all (rerenderAll's zeroed state): everything disabled except
155
165
  // the handful of buttons that don't depend on having a focused block.
156
166
  if (!hasBlock) {
package/lib/md2doc.js CHANGED
@@ -1395,8 +1395,12 @@ ${itemsHtml}
1395
1395
  resetLiRunState(biRef); // §3.8 rule (c): a non-li block ends every run
1396
1396
  const b = blocks[biRef.v++];
1397
1397
  const inner = marked.parser([t]);
1398
+ // v3.14.0 T10: the reader wraps an svg-only block in .inline-svg for the
1399
+ // dark white plate; edit mode keeps its DOM, so it marks the wrapper.
1400
+ const svgPlate = ((t.type === 'html' && t.block && wrapsSvg(t.text)) ||
1401
+ (t.type === 'paragraph' && wrapsSvgParagraph(t.raw))) ? ' data-svg-plate="1"' : '';
1398
1402
  parts.push(
1399
- `<div class="ed-block" data-block-id="${b.id}" data-block-type="${b.type}">` +
1403
+ `<div class="ed-block" data-block-id="${b.id}" data-block-type="${b.type}"${svgPlate}>` +
1400
1404
  inner + '</div>'
1401
1405
  );
1402
1406
  }
@@ -3446,6 +3450,16 @@ ${usesDrawio ? ` /* v3.4.0 §4: a document with no drawio reference must not pa
3446
3450
  outline: 2px solid var(--md-accent); outline-offset: 2px;
3447
3451
  caret-color: var(--md-accent);
3448
3452
  }
3453
+ /* v3.14.0 T4: the empty-paragraph hint. syncEmptyMark() in the client sets
3454
+ data-ed-empty on a paragraph surface with no text; a real paragraph shows
3455
+ the hint while focused, the transient (an empty document's, or one Enter
3456
+ left behind) shows it even unfocused so a blank line is never a blank
3457
+ page. An empty transient also gets a line of height of its own. */
3458
+ .ed-block[data-block-type="paragraph"] > .ed-wys-armed[data-ed-empty]:focus::before,
3459
+ .ed-block[data-ed-transient] > .ed-wys-armed[data-ed-empty]::before {
3460
+ content: attr(data-placeholder); color: var(--md-muted-2); pointer-events: none;
3461
+ }
3462
+ .ed-block[data-ed-transient] > .ed-wys-armed { min-height: 1.6em; }
3449
3463
  /* Task 5: table cells armed PERMANENTLY (one edit surface per CELL, not
3450
3464
  per table root — see armEditables()'s 'table' branch in the client
3451
3465
  runtime) — same "no persistent outline, blue on :focus" language as
@@ -4584,23 +4598,31 @@ ${mermaidInitTag}`)
4584
4598
  )).join('');
4585
4599
  }
4586
4600
 
4601
+ // On the edit page every block sits in a .ed-block wrapper, so the heading's
4602
+ // siblings are wrappers: the section ends at the next wrapper that is, or
4603
+ // directly holds, a heading of the same or a shallower depth. In the reader
4604
+ // the unit is the heading itself and the walk is the one it always was.
4587
4605
  function sectionBoundary(sectionId) {
4588
4606
  const start = document.getElementById(sectionId);
4589
4607
  if (!start) return null;
4590
4608
  const startDepth = Number(start.getAttribute('data-reader-depth') || '1');
4609
+ const unit = start.closest('.ed-block') || start;
4591
4610
  let end = null;
4592
- let node = start.nextElementSibling;
4611
+ let node = unit.nextElementSibling;
4593
4612
  while (node) {
4594
- if (node.matches && node.matches('[data-reader-heading]')) {
4595
- const depth = Number(node.getAttribute('data-reader-depth') || '1');
4613
+ let heading = null;
4614
+ if (node.matches && node.matches('[data-reader-heading]')) heading = node;
4615
+ else if (unit !== start && node.querySelector) heading = node.querySelector(':scope > [data-reader-heading]');
4616
+ if (heading) {
4617
+ const depth = Number(heading.getAttribute('data-reader-depth') || '1');
4596
4618
  if (depth <= startDepth) {
4597
- end = node;
4619
+ end = heading;
4598
4620
  break;
4599
4621
  }
4600
4622
  }
4601
4623
  node = node.nextElementSibling;
4602
4624
  }
4603
- return { start, end };
4625
+ return { start, end, container: unit.parentNode };
4604
4626
  }
4605
4627
 
4606
4628
  // A whitespace-only text node is the real space between words when it sits in
@@ -4634,13 +4656,12 @@ ${mermaidInitTag}`)
4634
4656
  function highlightFirstOccurrence(sectionId, query) {
4635
4657
  const bounds = sectionBoundary(sectionId);
4636
4658
  if (!bounds) return null;
4637
- const { start, end } = bounds;
4638
- const container = start.parentNode;
4659
+ const { start, end, container } = bounds;
4639
4660
  if (!container) return null;
4640
4661
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
4641
4662
  acceptNode(node) {
4642
4663
  if (node.nodeType === Node.ELEMENT_NODE) {
4643
- if (node.matches(SKIP_SELECTOR) || node.matches('.reader-sidebar, .heading-anchor')) return NodeFilter.FILTER_REJECT;
4664
+ if (node.matches(SKIP_SELECTOR) || node.matches('.reader-sidebar, .heading-anchor, button')) return NodeFilter.FILTER_REJECT;
4644
4665
  return node.matches('br, .katex') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
4645
4666
  }
4646
4667
  if (!node.nodeValue) return NodeFilter.FILTER_REJECT;
@@ -4650,7 +4671,7 @@ ${mermaidInitTag}`)
4650
4671
  });
4651
4672
  // Nothing before the heading is in range, so the walk starts at it.
4652
4673
  walker.currentNode = start;
4653
- const BLOCK = 'p, li, td, th, h1, h2, h3, h4, h5, h6, pre, blockquote, dt, dd, figcaption, .katex-display';
4674
+ const BLOCK = 'p, li, .ed-li-text, td, th, h1, h2, h3, h4, h5, h6, pre, blockquote, dt, dd, figcaption, .katex-display';
4654
4675
  const nodes = [];
4655
4676
  let flat = '';
4656
4677
  const at = [];
@@ -165,7 +165,7 @@ function themeCss() {
165
165
  ' ' + D + ' .content .drawio { background: #ffffff; border-radius: 6px; box-shadow: 0 0 0 10px #ffffff; }',
166
166
  // R7 甲: a Markdown-native inline svg (wrapped in .inline-svg by the renderer) gets the
167
167
  // same white plate; its ink stays the light text colour because the plate is always white.
168
- ' ' + D + ' .content .inline-svg > svg { background: #ffffff; color: #1f2328; border-radius: 4px; box-shadow: 0 0 0 10px #ffffff; }',
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; }',
169
169
  ' ' + D + ' .lightbox-canvas > svg[data-md2doc-recoloured] { background: var(--md-bg); }',
170
170
  ' }',
171
171
  ' .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.13.1",
3
+ "version": "3.14.0",
4
4
  "description": "Markdown → HTML / PDF renderer with WaveDrom, Mermaid, Graphviz, and draw.io support",
5
5
  "keywords": [
6
6
  "markdown",