pi-studio 0.5.48 → 0.5.49

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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.49] — 2026-04-09
8
+
9
+ ### Fixed
10
+ - Markdown editor-preview comments are now substantially more robust on real pandoc-rendered documents, including smart punctuation and ellipsis normalization, mixed prose plus display-math sections, standalone display equations treated as whole units, MathJax fallback display math, standalone page-break commands, and raw-TeX commands that disappear from preview output.
11
+ - Preview comment block matching now avoids unsafe tiny substring fallbacks, so extra blank-line splits that create short paragraphs like `or` no longer derail later Markdown comment targets.
12
+ - Markdown HTML comments like `<!-- ... -->` are now stripped more consistently across both the main pandoc preview path and plain-markdown fallback rendering, including edge cases after unmatched inline backticks at line breaks.
13
+
7
14
  ## [0.5.48] — 2026-04-07
8
15
 
9
16
  ### Added
@@ -510,6 +510,71 @@
510
510
  });
511
511
  }
512
512
 
513
+ function stripMarkdownHtmlCommentsInSegment(markdown) {
514
+ const source = String(markdown || "");
515
+ let out = "";
516
+ let index = 0;
517
+ let codeSpanFenceLength = 0;
518
+ let inHtmlComment = false;
519
+
520
+ while (index < source.length) {
521
+ if (inHtmlComment) {
522
+ if (source.startsWith("-->", index)) {
523
+ inHtmlComment = false;
524
+ index += 3;
525
+ continue;
526
+ }
527
+ const ch = source[index];
528
+ if (ch === "\n" || ch === "\r") out += ch;
529
+ index += 1;
530
+ continue;
531
+ }
532
+
533
+ if (codeSpanFenceLength > 0) {
534
+ const fence = "`".repeat(codeSpanFenceLength);
535
+ if (source.startsWith(fence, index)) {
536
+ out += fence;
537
+ index += codeSpanFenceLength;
538
+ codeSpanFenceLength = 0;
539
+ continue;
540
+ }
541
+ const ch = source[index];
542
+ out += ch;
543
+ index += 1;
544
+ if (ch === "\n" || ch === "\r") {
545
+ codeSpanFenceLength = 0;
546
+ }
547
+ continue;
548
+ }
549
+
550
+ const backtickMatch = source.slice(index).match(/^`+/);
551
+ if (backtickMatch) {
552
+ const fence = backtickMatch[0] || "`";
553
+ codeSpanFenceLength = fence.length;
554
+ out += fence;
555
+ index += fence.length;
556
+ continue;
557
+ }
558
+
559
+ if (source.startsWith("<!--", index)) {
560
+ inHtmlComment = true;
561
+ index += 4;
562
+ continue;
563
+ }
564
+
565
+ out += source[index];
566
+ index += 1;
567
+ }
568
+
569
+ return out;
570
+ }
571
+
572
+ function stripMarkdownHtmlComments(text) {
573
+ return transformMarkdownOutsideFences(text, function(segment) {
574
+ return stripMarkdownHtmlCommentsInSegment(segment);
575
+ });
576
+ }
577
+
513
578
  function prepareMarkdownForPandocPreview(markdown, placeholderPrefix) {
514
579
  const placeholders = [];
515
580
  const prefix = typeof placeholderPrefix === "string" && placeholderPrefix
@@ -536,6 +601,7 @@
536
601
  renderPreviewAnnotationHtml: renderPreviewAnnotationHtml,
537
602
  replaceInlineAnnotationMarkers: replaceInlineAnnotationMarkers,
538
603
  stripAnnotationMarkers: stripAnnotationMarkers,
604
+ stripMarkdownHtmlComments: stripMarkdownHtmlComments,
539
605
  transformMarkdownOutsideFences: transformMarkdownOutsideFences,
540
606
  });
541
607
 
@@ -1461,6 +1461,13 @@
1461
1461
  return annotationHelpers.stripAnnotationMarkers(text);
1462
1462
  }
1463
1463
 
1464
+ function stripMarkdownHtmlComments(text) {
1465
+ if (annotationHelpers && typeof annotationHelpers.stripMarkdownHtmlComments === "function") {
1466
+ return annotationHelpers.stripMarkdownHtmlComments(text);
1467
+ }
1468
+ return String(text || "");
1469
+ }
1470
+
1464
1471
  function prepareEditorTextForSend(text) {
1465
1472
  const raw = String(text || "");
1466
1473
  return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
@@ -1524,15 +1531,17 @@
1524
1531
  syncBadgeEl.classList.remove("sync");
1525
1532
  }
1526
1533
 
1527
- function buildPlainMarkdownHtml(markdown) {
1528
- return "<pre class='plain-markdown'>" + escapeHtml(String(markdown || "")) + "</pre>";
1534
+ function buildPlainMarkdownHtml(markdown, options) {
1535
+ const shouldStripHtmlComments = Boolean(options && options.stripMarkdownHtmlComments);
1536
+ const source = shouldStripHtmlComments ? stripMarkdownHtmlComments(markdown) : String(markdown || "");
1537
+ return "<pre class='plain-markdown'>" + escapeHtml(source) + "</pre>";
1529
1538
  }
1530
1539
 
1531
- function buildPreviewErrorHtml(message, markdown) {
1532
- return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown);
1540
+ function buildPreviewErrorHtml(message, markdown, options) {
1541
+ return "<div class='preview-error'>" + escapeHtml(String(message || "Preview rendering failed.")) + "</div>" + buildPlainMarkdownHtml(markdown, options);
1533
1542
  }
1534
1543
 
1535
- function sanitizeRenderedHtml(html, markdown) {
1544
+ function sanitizeRenderedHtml(html, markdown, options) {
1536
1545
  const rawHtml = typeof html === "string" ? html : "";
1537
1546
  const mathAnnotationPreserved = rawHtml.replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (match, attrs, inner) => {
1538
1547
  const texAnnotationMatch = String(inner || "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
@@ -1556,7 +1565,7 @@
1556
1565
  ADD_DATA_URI_TAGS: ["embed"],
1557
1566
  });
1558
1567
  }
1559
- return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1568
+ return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown, options);
1560
1569
  }
1561
1570
 
1562
1571
  function isPdfPreviewSource(src) {
@@ -2536,6 +2545,10 @@
2536
2545
  const previewPrepared = annotationsEnabled
2537
2546
  ? prepareMarkdownForPandocPreview(markdown)
2538
2547
  : { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
2548
+ const previewingEditorText = pane === "source" || rightView === "editor-preview";
2549
+ const previewFallbackOptions = {
2550
+ stripMarkdownHtmlComments: !previewingEditorText || editorLanguage !== "latex",
2551
+ };
2539
2552
 
2540
2553
  try {
2541
2554
  const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown, {
@@ -2550,7 +2563,7 @@
2550
2563
 
2551
2564
  clearPreviewJumpHighlight(targetEl);
2552
2565
  finishPreviewRender(targetEl);
2553
- targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2566
+ targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
2554
2567
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
2555
2568
  await renderAnnotationMathInElement(targetEl);
2556
2569
  decoratePdfEmbeds(targetEl);
@@ -2594,7 +2607,7 @@
2594
2607
  const detail = error && error.message ? error.message : String(error || "unknown error");
2595
2608
  clearPreviewJumpHighlight(targetEl);
2596
2609
  finishPreviewRender(targetEl);
2597
- targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2610
+ targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
2598
2611
  if (pane === "response") {
2599
2612
  applyPendingResponseScrollReset();
2600
2613
  scheduleResponsePaneRepaintNudge();
@@ -4444,6 +4457,8 @@
4444
4457
  if (kind === "heading") return "heading";
4445
4458
  if (kind === "blockquote") return "quote block";
4446
4459
  if (kind === "list") return "list";
4460
+ if (kind === "math") return "equation";
4461
+ if (kind === "page-break") return "page break";
4447
4462
  if (kind === "code") return "code block";
4448
4463
  if (kind === "table") return "table";
4449
4464
  if (kind === "code-line") return "code line";
@@ -4457,13 +4472,304 @@
4457
4472
  || kind === "heading"
4458
4473
  || kind === "blockquote"
4459
4474
  || kind === "list"
4475
+ || kind === "math"
4460
4476
  || kind === "code-line"
4461
4477
  || kind === "diff-line"
4462
4478
  || kind === "text-line";
4463
4479
  }
4464
4480
 
4481
+ const DISPLAY_MATH_ENV_NAMES = new Set([
4482
+ "displaymath",
4483
+ "equation",
4484
+ "equation*",
4485
+ "align",
4486
+ "align*",
4487
+ "aligned",
4488
+ "gather",
4489
+ "gather*",
4490
+ "multline",
4491
+ "multline*",
4492
+ "eqnarray",
4493
+ "eqnarray*",
4494
+ "split",
4495
+ ]);
4496
+
4497
+ function isEscapedAt(text, index) {
4498
+ let slashCount = 0;
4499
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
4500
+ slashCount += 1;
4501
+ }
4502
+ return (slashCount % 2) === 1;
4503
+ }
4504
+
4505
+ function readBalancedLatexGroup(source, startIndex, openChar, closeChar) {
4506
+ if (!source || source[startIndex] !== openChar) return null;
4507
+ let depth = 0;
4508
+ for (let index = startIndex; index < source.length; index += 1) {
4509
+ const ch = source[index];
4510
+ if (ch === "\\") {
4511
+ index += 1;
4512
+ continue;
4513
+ }
4514
+ if (ch === openChar) {
4515
+ depth += 1;
4516
+ continue;
4517
+ }
4518
+ if (ch === closeChar) {
4519
+ depth -= 1;
4520
+ if (depth === 0) {
4521
+ return {
4522
+ start: startIndex,
4523
+ contentStart: startIndex + 1,
4524
+ contentEnd: index,
4525
+ end: index + 1,
4526
+ };
4527
+ }
4528
+ }
4529
+ }
4530
+ return null;
4531
+ }
4532
+
4533
+ const DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS = new Set([
4534
+ "textbf",
4535
+ "textit",
4536
+ "emph",
4537
+ "underline",
4538
+ "texttt",
4539
+ "textrm",
4540
+ "textsf",
4541
+ "textsc",
4542
+ "mbox",
4543
+ "makebox",
4544
+ "framebox",
4545
+ "fbox",
4546
+ "url",
4547
+ "path",
4548
+ "nolinkurl",
4549
+ ]);
4550
+ const DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS = new Set([
4551
+ "href",
4552
+ "hyperref",
4553
+ ]);
4554
+ const DROPPED_MARKDOWN_RAW_TEX_STANDALONE_COMMANDS = new Set([
4555
+ "latex",
4556
+ "tex",
4557
+ "newpage",
4558
+ "pagebreak",
4559
+ "clearpage",
4560
+ ]);
4561
+
4562
+ function skipLatexWhitespace(source, startIndex) {
4563
+ let index = startIndex;
4564
+ while (index < source.length && /\s/.test(source[index])) index += 1;
4565
+ return index;
4566
+ }
4567
+
4568
+ function parseLatexCommandAt(source, startIndex) {
4569
+ if (!source || source[startIndex] !== "\\") return null;
4570
+ let index = startIndex + 1;
4571
+ if (index >= source.length) {
4572
+ return { name: "", end: index };
4573
+ }
4574
+ if (/[A-Za-z@]/.test(source[index])) {
4575
+ const nameStart = index;
4576
+ while (index < source.length && /[A-Za-z@]/.test(source[index])) index += 1;
4577
+ if (source[index] === "*") index += 1;
4578
+ return {
4579
+ name: source.slice(nameStart, index),
4580
+ end: index,
4581
+ };
4582
+ }
4583
+ return {
4584
+ name: source[index],
4585
+ end: index + 1,
4586
+ };
4587
+ }
4588
+
4589
+ function collectDisplayMathRanges(text) {
4590
+ const source = String(text || "");
4591
+ const ranges = [];
4592
+ let index = 0;
4593
+
4594
+ while (index < source.length) {
4595
+ if (source[index] === "%" && !isEscapedAt(source, index)) {
4596
+ while (index < source.length && source[index] !== "\n") index += 1;
4597
+ continue;
4598
+ }
4599
+ if (source.startsWith("$$", index)) {
4600
+ const close = source.indexOf("$$", index + 2);
4601
+ if (close >= 0) {
4602
+ ranges.push({
4603
+ start: index,
4604
+ end: close + 2,
4605
+ bodyStart: index + 2,
4606
+ bodyEnd: close,
4607
+ bodyText: source.slice(index + 2, close),
4608
+ });
4609
+ index = close + 2;
4610
+ continue;
4611
+ }
4612
+ }
4613
+ if (source.startsWith("\\[", index)) {
4614
+ const close = source.indexOf("\\]", index + 2);
4615
+ if (close >= 0) {
4616
+ ranges.push({
4617
+ start: index,
4618
+ end: close + 2,
4619
+ bodyStart: index + 2,
4620
+ bodyEnd: close,
4621
+ bodyText: source.slice(index + 2, close),
4622
+ });
4623
+ index = close + 2;
4624
+ continue;
4625
+ }
4626
+ }
4627
+ if (source.startsWith("\\begin{", index)) {
4628
+ const envGroup = readBalancedLatexGroup(source, index + 6, "{", "}");
4629
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim() : "";
4630
+ if (envName && DISPLAY_MATH_ENV_NAMES.has(envName)) {
4631
+ const closeToken = "\\end{" + envName + "}";
4632
+ const close = source.indexOf(closeToken, envGroup.end);
4633
+ if (close >= 0) {
4634
+ ranges.push({
4635
+ start: index,
4636
+ end: close + closeToken.length,
4637
+ bodyStart: envGroup.end,
4638
+ bodyEnd: close,
4639
+ bodyText: source.slice(envGroup.end, close),
4640
+ });
4641
+ index = close + closeToken.length;
4642
+ continue;
4643
+ }
4644
+ }
4645
+ }
4646
+ index += 1;
4647
+ }
4648
+
4649
+ return ranges;
4650
+ }
4651
+
4652
+ function getStandaloneDisplayMathRange(text) {
4653
+ const source = String(text || "");
4654
+ const leadingMatch = source.match(/^\s*/);
4655
+ const trailingMatch = source.match(/\s*$/);
4656
+ const leadingLength = leadingMatch ? leadingMatch[0].length : 0;
4657
+ const trailingLength = trailingMatch ? trailingMatch[0].length : 0;
4658
+ const trimmedEnd = Math.max(leadingLength, source.length - trailingLength);
4659
+ const trimmed = source.slice(leadingLength, trimmedEnd);
4660
+ if (!trimmed) return null;
4661
+ const ranges = collectDisplayMathRanges(trimmed);
4662
+ if (ranges.length !== 1) return null;
4663
+ const range = ranges[0];
4664
+ if (!range || range.start !== 0 || range.end !== trimmed.length) return null;
4665
+ return {
4666
+ start: leadingLength + range.start,
4667
+ end: leadingLength + range.end,
4668
+ bodyStart: leadingLength + range.bodyStart,
4669
+ bodyEnd: leadingLength + range.bodyEnd,
4670
+ bodyText: String(range.bodyText || ""),
4671
+ };
4672
+ }
4673
+ function normalizePreviewComparableCharacter(character) {
4674
+ switch (String(character || "")) {
4675
+ case "\u2018":
4676
+ case "\u2019":
4677
+ case "\u201A":
4678
+ case "\u201B":
4679
+ return "'";
4680
+ case "\u201C":
4681
+ case "\u201D":
4682
+ case "\u201E":
4683
+ case "\u201F":
4684
+ return '"';
4685
+ case "\u2013":
4686
+ case "\u2014":
4687
+ case "\u2212":
4688
+ return "-";
4689
+ case "\u2026":
4690
+ return "…";
4691
+ default:
4692
+ return String(character || "");
4693
+ }
4694
+ }
4695
+
4465
4696
  function normalizeVisiblePreviewText(text) {
4466
- return String(text || "").replace(/\s+/g, " ").trim();
4697
+ const source = String(text || "");
4698
+ let normalized = "";
4699
+ let pendingWhitespace = false;
4700
+ for (let i = 0; i < source.length; i += 1) {
4701
+ let character = source[i] === "." && source.slice(i, i + 3) === "..."
4702
+ ? "…"
4703
+ : normalizePreviewComparableCharacter(source[i]);
4704
+ if (character === "…" && source[i] === "." && source.slice(i, i + 3) === "...") {
4705
+ i += 2;
4706
+ }
4707
+ if (/\s/.test(character)) {
4708
+ if (normalized) {
4709
+ pendingWhitespace = true;
4710
+ }
4711
+ continue;
4712
+ }
4713
+ if (pendingWhitespace && normalized) {
4714
+ normalized += " ";
4715
+ pendingWhitespace = false;
4716
+ }
4717
+ normalized += character;
4718
+ }
4719
+ return normalized.trim();
4720
+ }
4721
+
4722
+ function splitSourcePreviewCommentBlockByDisplayMath(sourceText, block) {
4723
+ if (!block || block.kind !== "paragraph") {
4724
+ return block ? [block] : [];
4725
+ }
4726
+ const source = String(sourceText || "");
4727
+ const blockStart = Math.max(0, Math.min(Number(block.start) || 0, source.length));
4728
+ const blockEnd = Math.max(blockStart, Math.min(Number(block.end) || blockStart, source.length));
4729
+ const blockText = source.slice(blockStart, blockEnd);
4730
+ const mathRanges = collectDisplayMathRanges(blockText);
4731
+ if (mathRanges.length === 0) {
4732
+ return [block];
4733
+ }
4734
+
4735
+ const segments = [];
4736
+ function pushSegment(kind, relativeStart, relativeEnd) {
4737
+ const safeRelativeStart = Math.max(0, Math.min(relativeStart, blockText.length));
4738
+ const safeRelativeEnd = Math.max(safeRelativeStart, Math.min(relativeEnd, blockText.length));
4739
+ if (safeRelativeEnd <= safeRelativeStart) return;
4740
+ const absoluteStart = blockStart + safeRelativeStart;
4741
+ const absoluteEnd = blockStart + safeRelativeEnd;
4742
+ const segmentText = source.slice(absoluteStart, absoluteEnd);
4743
+ if (kind === "paragraph" && !normalizeVisiblePreviewText(segmentText)) {
4744
+ return;
4745
+ }
4746
+ segments.push({
4747
+ kind,
4748
+ start: absoluteStart,
4749
+ end: absoluteEnd,
4750
+ lineStart: getLineNumberAtOffset(source, absoluteStart),
4751
+ lineEnd: getLineNumberAtOffset(source, Math.max(absoluteStart, absoluteEnd - 1)),
4752
+ });
4753
+ }
4754
+
4755
+ let cursor = 0;
4756
+ mathRanges.forEach((mathRange) => {
4757
+ if (!mathRange) return;
4758
+ pushSegment("paragraph", cursor, mathRange.start);
4759
+ pushSegment("math", mathRange.start, mathRange.end);
4760
+ cursor = mathRange.end;
4761
+ });
4762
+ pushSegment("paragraph", cursor, blockText.length);
4763
+
4764
+ return segments.length > 0 ? segments : [block];
4765
+ }
4766
+
4767
+ function expandSourcePreviewCommentBlocksByDisplayMath(sourceText, blocks) {
4768
+ const expanded = [];
4769
+ (Array.isArray(blocks) ? blocks : []).forEach((block) => {
4770
+ expanded.push(...splitSourcePreviewCommentBlockByDisplayMath(sourceText, block));
4771
+ });
4772
+ return expanded;
4467
4773
  }
4468
4774
 
4469
4775
  function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
@@ -4516,6 +4822,14 @@
4516
4822
  }
4517
4823
  }
4518
4824
 
4825
+ if (kind === "math") {
4826
+ const mathRange = getStandaloneDisplayMathRange(source);
4827
+ if (mathRange) {
4828
+ appendMappedPreviewSlice(chars, rawOffsets, source, 0, mathRange.bodyStart, mathRange.bodyEnd);
4829
+ return { text: chars.join(""), rawOffsets };
4830
+ }
4831
+ }
4832
+
4519
4833
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4520
4834
  const line = lines[lineIndex] || "";
4521
4835
  if (kind === "blockquote") {
@@ -4543,6 +4857,26 @@
4543
4857
  return { text: chars.join(""), rawOffsets };
4544
4858
  }
4545
4859
 
4860
+ function findClosingUnescapedSequence(source, startIndex, sequence) {
4861
+ const text = String(source || "");
4862
+ const needle = String(sequence || "");
4863
+ if (!text || !needle) return -1;
4864
+ let searchIndex = Math.max(0, Number(startIndex) || 0);
4865
+ while (searchIndex <= text.length) {
4866
+ const matchIndex = text.indexOf(needle, searchIndex);
4867
+ if (matchIndex < 0) return -1;
4868
+ let backslashCount = 0;
4869
+ for (let i = matchIndex - 1; i >= 0 && text[i] === "\\"; i -= 1) {
4870
+ backslashCount += 1;
4871
+ }
4872
+ if (backslashCount % 2 === 0) {
4873
+ return matchIndex;
4874
+ }
4875
+ searchIndex = matchIndex + needle.length;
4876
+ }
4877
+ return -1;
4878
+ }
4879
+
4546
4880
  function buildPreviewInlineDisplayMap(text, rawOffsets) {
4547
4881
  const source = String(text || "");
4548
4882
  const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
@@ -4556,6 +4890,12 @@
4556
4890
  charEnds.push(rawEnd);
4557
4891
  }
4558
4892
 
4893
+ function appendRawRange(startIndex, endIndex) {
4894
+ for (let i = startIndex; i < endIndex; i += 1) {
4895
+ appendChar(source[i], rawMap[i], rawMap[i] + 1);
4896
+ }
4897
+ }
4898
+
4559
4899
  function appendNestedRange(startIndex, endIndex) {
4560
4900
  const nested = buildPreviewInlineDisplayMap(
4561
4901
  source.slice(startIndex, endIndex),
@@ -4584,15 +4924,86 @@
4584
4924
  const fence = "`".repeat(tickCount);
4585
4925
  const closeIndex = source.indexOf(fence, index + tickCount);
4586
4926
  if (closeIndex >= 0) {
4587
- for (let i = index + tickCount; i < closeIndex; i += 1) {
4588
- appendChar(source[i], rawMap[i], rawMap[i] + 1);
4589
- }
4927
+ appendRawRange(index + tickCount, closeIndex);
4590
4928
  index = closeIndex + tickCount;
4591
4929
  continue;
4592
4930
  }
4593
4931
  }
4594
4932
 
4933
+ if (remaining.startsWith("\\(")) {
4934
+ const closeIndex = source.indexOf("\\)", index + 2);
4935
+ if (closeIndex >= 0) {
4936
+ appendRawRange(index + 2, closeIndex);
4937
+ index = closeIndex + 2;
4938
+ continue;
4939
+ }
4940
+ }
4941
+
4942
+ if (remaining.startsWith("\\[")) {
4943
+ const closeIndex = source.indexOf("\\]", index + 2);
4944
+ if (closeIndex >= 0) {
4945
+ appendRawRange(index + 2, closeIndex);
4946
+ index = closeIndex + 2;
4947
+ continue;
4948
+ }
4949
+ }
4950
+
4951
+ if (remaining.startsWith("$$")) {
4952
+ const closeIndex = findClosingUnescapedSequence(source, index + 2, "$$");
4953
+ if (closeIndex >= 0) {
4954
+ appendRawRange(index + 2, closeIndex);
4955
+ index = closeIndex + 2;
4956
+ continue;
4957
+ }
4958
+ }
4959
+
4960
+ if (source[index] === "$") {
4961
+ const closeIndex = findClosingUnescapedSequence(source, index + 1, "$");
4962
+ if (closeIndex >= 0) {
4963
+ appendRawRange(index + 1, closeIndex);
4964
+ index = closeIndex + 1;
4965
+ continue;
4966
+ }
4967
+ }
4968
+
4595
4969
  if (source[index] === "\\" && index + 1 < source.length) {
4970
+ const latexCommand = parseLatexCommandAt(source, index);
4971
+ const normalizedCommandName = latexCommand && latexCommand.name
4972
+ ? String(latexCommand.name || "").replace(/\*$/, "").toLowerCase()
4973
+ : "";
4974
+ const isDroppedLatexCommand = Boolean(
4975
+ normalizedCommandName
4976
+ && (
4977
+ DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS.has(normalizedCommandName)
4978
+ || DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName)
4979
+ || DROPPED_MARKDOWN_RAW_TEX_STANDALONE_COMMANDS.has(normalizedCommandName)
4980
+ )
4981
+ );
4982
+ if (latexCommand && isDroppedLatexCommand) {
4983
+ let nextIndex = skipLatexWhitespace(source, latexCommand.end);
4984
+ if (source[nextIndex] === "[") {
4985
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4986
+ if (optionalGroup) {
4987
+ nextIndex = skipLatexWhitespace(source, optionalGroup.end);
4988
+ }
4989
+ }
4990
+ if (DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS.has(normalizedCommandName) || DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName)) {
4991
+ if (source[nextIndex] === "{") {
4992
+ const firstGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4993
+ if (firstGroup) {
4994
+ nextIndex = skipLatexWhitespace(source, firstGroup.end);
4995
+ }
4996
+ }
4997
+ }
4998
+ if (DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName) && source[nextIndex] === "{") {
4999
+ const secondGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
5000
+ if (secondGroup) {
5001
+ nextIndex = skipLatexWhitespace(source, secondGroup.end);
5002
+ }
5003
+ }
5004
+ index = Math.max(index + 1, nextIndex);
5005
+ continue;
5006
+ }
4596
5007
  appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4597
5008
  index += 2;
4598
5009
  continue;
@@ -4630,13 +5041,20 @@
4630
5041
  let pendingWhitespaceEnd = null;
4631
5042
 
4632
5043
  for (let i = 0; i < source.length; i += 1) {
4633
- const character = source[i];
5044
+ let character = normalizePreviewComparableCharacter(source[i]);
5045
+ let startRef = charStarts[i];
5046
+ let endRef = charEnds[i];
5047
+ if (source[i] === "." && source.slice(i, i + 3) === "...") {
5048
+ character = "…";
5049
+ endRef = charEnds[Math.min(i + 2, charEnds.length - 1)];
5050
+ i += 2;
5051
+ }
4634
5052
  if (/\s/.test(character)) {
4635
5053
  if (outChars.length === 0) continue;
4636
5054
  if (pendingWhitespaceStart == null) {
4637
- pendingWhitespaceStart = charStarts[i];
5055
+ pendingWhitespaceStart = startRef;
4638
5056
  }
4639
- pendingWhitespaceEnd = charEnds[i];
5057
+ pendingWhitespaceEnd = endRef;
4640
5058
  continue;
4641
5059
  }
4642
5060
 
@@ -4649,8 +5067,8 @@
4649
5067
  }
4650
5068
 
4651
5069
  outChars.push(character);
4652
- outStarts.push(charStarts[i]);
4653
- outEnds.push(charEnds[i]);
5070
+ outStarts.push(startRef);
5071
+ outEnds.push(endRef);
4654
5072
  }
4655
5073
 
4656
5074
  return {
@@ -4682,6 +5100,68 @@
4682
5100
  return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4683
5101
  }
4684
5102
 
5103
+ function getPreviewMathSearchText(element) {
5104
+ if (!element || !(element instanceof Element)) return null;
5105
+ const tag = element.tagName ? element.tagName.toUpperCase() : "";
5106
+ if (tag === "MATH") {
5107
+ const texSource = element.getAttribute("data-tex-source");
5108
+ if (texSource && texSource.trim()) {
5109
+ return texSource;
5110
+ }
5111
+ return typeof element.textContent === "string" ? element.textContent : "";
5112
+ }
5113
+ if (element.classList && element.classList.contains("math") && (element.classList.contains("inline") || element.classList.contains("display"))) {
5114
+ return extractMathFallbackTex(
5115
+ typeof element.textContent === "string" ? element.textContent : "",
5116
+ element.classList.contains("display"),
5117
+ );
5118
+ }
5119
+ return null;
5120
+ }
5121
+
5122
+ function buildNormalizedPreviewSearchText(rootNode) {
5123
+ if (!rootNode) return "";
5124
+ const parts = [];
5125
+
5126
+ function visit(node) {
5127
+ if (!node) return;
5128
+ if (node.nodeType === Node.TEXT_NODE) {
5129
+ parts.push(typeof node.nodeValue === "string" ? node.nodeValue : "");
5130
+ return;
5131
+ }
5132
+ if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
5133
+ return;
5134
+ }
5135
+ if (node.nodeType === Node.ELEMENT_NODE) {
5136
+ const element = node;
5137
+ const mathText = getPreviewMathSearchText(element);
5138
+ if (mathText != null) {
5139
+ parts.push(mathText);
5140
+ return;
5141
+ }
5142
+ if (element.tagName === "BR") {
5143
+ parts.push("\n");
5144
+ return;
5145
+ }
5146
+ }
5147
+ Array.from(node.childNodes || []).forEach(visit);
5148
+ }
5149
+
5150
+ visit(rootNode);
5151
+ return normalizeVisiblePreviewText(parts.join(""));
5152
+ }
5153
+
5154
+ function buildNormalizedPreviewRangeText(range) {
5155
+ if (!range || typeof range.cloneContents !== "function") {
5156
+ return "";
5157
+ }
5158
+ try {
5159
+ return buildNormalizedPreviewSearchText(range.cloneContents());
5160
+ } catch {
5161
+ return normalizeVisiblePreviewText(range.toString());
5162
+ }
5163
+ }
5164
+
4685
5165
  function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4686
5166
  const source = String(haystack || "");
4687
5167
  const query = String(needle || "");
@@ -4860,6 +5340,10 @@
4860
5340
  return /^\s*<!--/.test(getLine(index));
4861
5341
  }
4862
5342
 
5343
+ function isPageBreakLine(index) {
5344
+ return /^\\(?:newpage|pagebreak|clearpage)(?:\s*\[[^\]]*\])?\s*$/i.test(getLine(index));
5345
+ }
5346
+
4863
5347
  function makeBlock(kind, startLineIndex, endLineIndex) {
4864
5348
  const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4865
5349
  const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
@@ -4906,6 +5390,12 @@
4906
5390
  continue;
4907
5391
  }
4908
5392
 
5393
+ if (isPageBreakLine(index)) {
5394
+ blocks.push(makeBlock("page-break", index, index));
5395
+ index += 1;
5396
+ continue;
5397
+ }
5398
+
4909
5399
  const fenceMatch = lineStartsFence(index);
4910
5400
  if (fenceMatch) {
4911
5401
  const marker = fenceMatch[1] || "";
@@ -5004,11 +5494,83 @@
5004
5494
  index = endParagraph + 1;
5005
5495
  }
5006
5496
 
5007
- return blocks;
5497
+ return expandSourcePreviewCommentBlocksByDisplayMath(source, blocks);
5498
+ }
5499
+
5500
+ function isPreviewDisplayMathElement(element) {
5501
+ return Boolean(element && element instanceof Element && element.matches && element.matches("math[display='block'], .studio-mathjax-fallback-display"));
5502
+ }
5503
+
5504
+ function previewNodesHaveVisibleContent(nodes) {
5505
+ return (Array.isArray(nodes) ? nodes : []).some((node) => {
5506
+ if (!node) return false;
5507
+ if (node.nodeType === Node.TEXT_NODE) {
5508
+ return Boolean(normalizeVisiblePreviewText(node.nodeValue || ""));
5509
+ }
5510
+ return node instanceof Element && Boolean(buildNormalizedPreviewSearchText(node));
5511
+ });
5512
+ }
5513
+
5514
+ function splitMixedPreviewParagraphsAroundDisplayMath(targetEl) {
5515
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5516
+ Array.from(targetEl.querySelectorAll("p")).forEach((paragraphEl) => {
5517
+ if (!(paragraphEl instanceof Element) || !paragraphEl.parentNode) return;
5518
+ if (paragraphEl.closest && paragraphEl.closest(".preview-comment-block")) return;
5519
+ let ancestor = paragraphEl.parentElement;
5520
+ while (ancestor && ancestor !== targetEl) {
5521
+ if (getPreviewCommentTargetKind(ancestor)) return;
5522
+ ancestor = ancestor.parentElement;
5523
+ }
5524
+ const childNodes = Array.from(paragraphEl.childNodes || []);
5525
+ if (!childNodes.some((node) => isPreviewDisplayMathElement(node))) return;
5526
+
5527
+ const fragment = document.createDocumentFragment();
5528
+ let proseNodes = [];
5529
+ let segmentCount = 0;
5530
+
5531
+ function flushProse() {
5532
+ if (proseNodes.length === 0) return;
5533
+ if (!previewNodesHaveVisibleContent(proseNodes)) {
5534
+ proseNodes = [];
5535
+ return;
5536
+ }
5537
+ const proseEl = paragraphEl.cloneNode(false);
5538
+ if (proseEl instanceof Element) {
5539
+ proseEl.removeAttribute("id");
5540
+ }
5541
+ proseNodes.forEach((node) => {
5542
+ proseEl.appendChild(node);
5543
+ });
5544
+ fragment.appendChild(proseEl);
5545
+ proseNodes = [];
5546
+ segmentCount += 1;
5547
+ }
5548
+
5549
+ childNodes.forEach((node) => {
5550
+ if (isPreviewDisplayMathElement(node)) {
5551
+ flushProse();
5552
+ fragment.appendChild(node);
5553
+ segmentCount += 1;
5554
+ return;
5555
+ }
5556
+ proseNodes.push(node);
5557
+ });
5558
+ flushProse();
5559
+
5560
+ if (segmentCount > 0) {
5561
+ paragraphEl.replaceWith(fragment);
5562
+ }
5563
+ });
5008
5564
  }
5009
5565
 
5010
5566
  function getPreviewCommentTargetKind(element) {
5011
5567
  if (!element || !(element instanceof Element)) return "";
5568
+ if (element.classList && element.classList.contains("studio-mathjax-fallback-display")) {
5569
+ return "math";
5570
+ }
5571
+ if (element.classList && element.classList.contains("studio-page-break")) {
5572
+ return "page-break";
5573
+ }
5012
5574
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5013
5575
  if (/^H[1-6]$/.test(tag)) return "heading";
5014
5576
  if (tag === "P") return "paragraph";
@@ -5016,6 +5578,9 @@
5016
5578
  if (tag === "UL" || tag === "OL") return "list";
5017
5579
  if (tag === "TABLE") return "table";
5018
5580
  if (tag === "PRE") return "code";
5581
+ if (tag === "MATH") {
5582
+ return String(element.getAttribute("display") || "").toLowerCase() === "block" ? "math" : "";
5583
+ }
5019
5584
  if (element.classList) {
5020
5585
  if (
5021
5586
  element.classList.contains("sourceCode")
@@ -5042,7 +5607,7 @@
5042
5607
 
5043
5608
  function collectPreviewCommentTargetElements(targetEl) {
5044
5609
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
5045
- const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
5610
+ const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, math[display='block'], .studio-mathjax-fallback-display, .studio-page-break, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
5046
5611
  return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
5047
5612
  if (!isPreviewCommentTargetElement(element)) return false;
5048
5613
  let ancestor = element.parentElement;
@@ -5061,6 +5626,10 @@
5061
5626
  function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
5062
5627
  if (!sourceBlock) return "";
5063
5628
  const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
5629
+ if (sourceBlock.kind === "page-break") {
5630
+ const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
5631
+ return match ? String(match[1] || "").toLowerCase() : "page-break";
5632
+ }
5064
5633
  if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
5065
5634
  return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
5066
5635
  }
@@ -5084,14 +5653,26 @@
5084
5653
  function getNormalizedPreviewCommentTargetText(targetEntry) {
5085
5654
  if (!targetEntry) return "";
5086
5655
  if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
5087
- targetEntry.normalizedText = normalizeVisiblePreviewText(
5088
- targetEntry.element && typeof targetEntry.element.textContent === "string"
5089
- ? targetEntry.element.textContent
5090
- : "",
5091
- );
5656
+ if (targetEntry.kind === "page-break") {
5657
+ const element = targetEntry.element;
5658
+ targetEntry.normalizedText = String(element && element.getAttribute ? (element.getAttribute("data-page-break-kind") || "page-break") : "page-break").toLowerCase();
5659
+ return targetEntry.normalizedText;
5660
+ }
5661
+ targetEntry.normalizedText = buildNormalizedPreviewSearchText(targetEntry.element);
5092
5662
  return targetEntry.normalizedText;
5093
5663
  }
5094
5664
 
5665
+ function isHighConfidencePreviewTextContainmentMatch(leftText, rightText) {
5666
+ const left = String(leftText || "");
5667
+ const right = String(rightText || "");
5668
+ if (!left || !right || left === right) return false;
5669
+ const shorter = left.length <= right.length ? left : right;
5670
+ const longer = left.length <= right.length ? right : left;
5671
+ if (shorter.length < 12) return false;
5672
+ if (!/\s/.test(shorter)) return false;
5673
+ return longer.includes(shorter);
5674
+ }
5675
+
5095
5676
  function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5096
5677
  const desiredKind = sourceBlock ? sourceBlock.kind : "";
5097
5678
  const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
@@ -5107,7 +5688,7 @@
5107
5688
  if (targetText === desiredText) {
5108
5689
  return i;
5109
5690
  }
5110
- if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
5691
+ if (containsIndex < 0 && isHighConfidencePreviewTextContainmentMatch(targetText, desiredText)) {
5111
5692
  containsIndex = i;
5112
5693
  }
5113
5694
  }
@@ -5167,6 +5748,7 @@
5167
5748
 
5168
5749
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5169
5750
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5751
+ splitMixedPreviewParagraphsAroundDisplayMath(targetEl);
5170
5752
  const sourceBlocks = scanSourcePreviewCommentBlocks(sourceText);
5171
5753
  const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5172
5754
  if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
@@ -5250,6 +5832,19 @@
5250
5832
  const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5251
5833
  if (blockEnd <= blockStart) return null;
5252
5834
 
5835
+ if (kind === "math") {
5836
+ const selectedDisplayText = normalizeVisiblePreviewText(getPreviewMathSearchText(contentEl) || buildNormalizedPreviewSearchText(contentEl));
5837
+ if (!selectedDisplayText) return null;
5838
+ return {
5839
+ selectionStart: blockStart,
5840
+ selectionEnd: blockEnd,
5841
+ lineStart: getLineNumberAtOffset(source, blockStart),
5842
+ lineEnd: getLineNumberAtOffset(source, Math.max(blockStart, blockEnd - 1)),
5843
+ selectedText: source.slice(blockStart, blockEnd),
5844
+ selectedDisplayText,
5845
+ };
5846
+ }
5847
+
5253
5848
  const sourceBlockText = source.slice(blockStart, blockEnd);
5254
5849
  const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5255
5850
  if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
@@ -5257,8 +5852,8 @@
5257
5852
  const prefixRange = document.createRange();
5258
5853
  prefixRange.selectNodeContents(contentEl);
5259
5854
  prefixRange.setEnd(range.startContainer, range.startOffset);
5260
- const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5261
- const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
5855
+ const prefixText = buildNormalizedPreviewRangeText(prefixRange);
5856
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
5262
5857
  if (!selectedDisplayText) return null;
5263
5858
 
5264
5859
  const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
@@ -5376,7 +5971,7 @@
5376
5971
  let bestScore = Number.NEGATIVE_INFINITY;
5377
5972
  Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5378
5973
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5379
- const blockText = normalizeVisiblePreviewText(buildNormalizedDomTextMap(contentEl).text);
5974
+ const blockText = buildNormalizedPreviewSearchText(contentEl);
5380
5975
  if (!blockText) return;
5381
5976
  const matchIndex = blockText.indexOf(selectionText);
5382
5977
  if (matchIndex < 0) return;
@@ -5400,6 +5995,13 @@
5400
5995
  const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
5401
5996
  if (!blockEl) return false;
5402
5997
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5998
+ if (String(blockEl.dataset && blockEl.dataset.previewCommentKind || "") === "math") {
5999
+ if (typeof contentEl.scrollIntoView === "function") {
6000
+ contentEl.scrollIntoView({ block: "center", inline: "nearest" });
6001
+ }
6002
+ setPreviewJumpHighlight(targetEl, contentEl, null);
6003
+ return true;
6004
+ }
5403
6005
  const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5404
6006
  if (typeof blockEl.scrollIntoView === "function") {
5405
6007
  blockEl.scrollIntoView({ block: "center", inline: "nearest" });
package/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  replaceStudioInlineAnnotationMarkers,
20
20
  transformStudioMarkdownOutsideFences,
21
21
  } from "./shared/studio-annotation-scanner.js";
22
+ import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
22
23
  import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
23
24
 
24
25
  type Lens = "writing" | "code";
@@ -2893,114 +2894,6 @@ function normalizeMathDelimiters(markdown: string): string {
2893
2894
  return out.join("\n");
2894
2895
  }
2895
2896
 
2896
- function stripStudioMarkdownHtmlCommentsInSegment(markdown: string): string {
2897
- const source = String(markdown ?? "");
2898
- let out = "";
2899
- let i = 0;
2900
- let codeSpanFenceLength = 0;
2901
- let inHtmlComment = false;
2902
-
2903
- while (i < source.length) {
2904
- if (inHtmlComment) {
2905
- if (source.startsWith("-->", i)) {
2906
- inHtmlComment = false;
2907
- i += 3;
2908
- continue;
2909
- }
2910
- const ch = source[i]!;
2911
- if (ch === "\n" || ch === "\r") out += ch;
2912
- i += 1;
2913
- continue;
2914
- }
2915
-
2916
- if (codeSpanFenceLength > 0) {
2917
- const fence = "`".repeat(codeSpanFenceLength);
2918
- if (source.startsWith(fence, i)) {
2919
- out += fence;
2920
- i += codeSpanFenceLength;
2921
- codeSpanFenceLength = 0;
2922
- continue;
2923
- }
2924
- out += source[i]!;
2925
- i += 1;
2926
- continue;
2927
- }
2928
-
2929
- const backtickMatch = source.slice(i).match(/^`+/);
2930
- if (backtickMatch) {
2931
- const fence = backtickMatch[0]!;
2932
- codeSpanFenceLength = fence.length;
2933
- out += fence;
2934
- i += fence.length;
2935
- continue;
2936
- }
2937
-
2938
- if (source.startsWith("<!--", i)) {
2939
- inHtmlComment = true;
2940
- i += 4;
2941
- continue;
2942
- }
2943
-
2944
- out += source[i]!;
2945
- i += 1;
2946
- }
2947
-
2948
- return out;
2949
- }
2950
-
2951
- function stripStudioMarkdownHtmlComments(markdown: string): string {
2952
- const lines = String(markdown ?? "").split("\n");
2953
- const out: string[] = [];
2954
- let plainBuffer: string[] = [];
2955
- let inFence = false;
2956
- let fenceChar: "`" | "~" | undefined;
2957
- let fenceLength = 0;
2958
-
2959
- const flushPlain = () => {
2960
- if (plainBuffer.length === 0) return;
2961
- out.push(stripStudioMarkdownHtmlCommentsInSegment(plainBuffer.join("\n")));
2962
- plainBuffer = [];
2963
- };
2964
-
2965
- for (const line of lines) {
2966
- const trimmed = line.trimStart();
2967
- const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
2968
-
2969
- if (fenceMatch) {
2970
- const marker = fenceMatch[1]!;
2971
- const markerChar = marker[0] as "`" | "~";
2972
- const markerLength = marker.length;
2973
-
2974
- if (!inFence) {
2975
- flushPlain();
2976
- inFence = true;
2977
- fenceChar = markerChar;
2978
- fenceLength = markerLength;
2979
- out.push(line);
2980
- continue;
2981
- }
2982
-
2983
- if (fenceChar === markerChar && markerLength >= fenceLength) {
2984
- inFence = false;
2985
- fenceChar = undefined;
2986
- fenceLength = 0;
2987
- }
2988
-
2989
- out.push(line);
2990
- continue;
2991
- }
2992
-
2993
- if (inFence) {
2994
- out.push(line);
2995
- } else {
2996
- plainBuffer.push(line);
2997
- }
2998
- }
2999
-
3000
- flushPlain();
3001
- return out.join("\n");
3002
- }
3003
-
3004
2897
  const STUDIO_PREVIEW_PAGE_BREAK_SENTINEL_PREFIX = "PI_STUDIO_PAGE_BREAK__";
3005
2898
 
3006
2899
  function replaceStudioPreviewPageBreakCommands(markdown: string): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,111 @@
1
+ export function stripStudioMarkdownHtmlCommentsInSegment(markdown) {
2
+ const source = String(markdown ?? "");
3
+ let out = "";
4
+ let index = 0;
5
+ let codeSpanFenceLength = 0;
6
+ let inHtmlComment = false;
7
+
8
+ while (index < source.length) {
9
+ if (inHtmlComment) {
10
+ if (source.startsWith("-->", index)) {
11
+ inHtmlComment = false;
12
+ index += 3;
13
+ continue;
14
+ }
15
+ const ch = source[index];
16
+ if (ch === "\n" || ch === "\r") out += ch;
17
+ index += 1;
18
+ continue;
19
+ }
20
+
21
+ if (codeSpanFenceLength > 0) {
22
+ const fence = "`".repeat(codeSpanFenceLength);
23
+ if (source.startsWith(fence, index)) {
24
+ out += fence;
25
+ index += codeSpanFenceLength;
26
+ codeSpanFenceLength = 0;
27
+ continue;
28
+ }
29
+ const ch = source[index];
30
+ out += ch;
31
+ index += 1;
32
+ if (ch === "\n" || ch === "\r") {
33
+ codeSpanFenceLength = 0;
34
+ }
35
+ continue;
36
+ }
37
+
38
+ const backtickMatch = source.slice(index).match(/^`+/);
39
+ if (backtickMatch) {
40
+ const fence = backtickMatch[0];
41
+ codeSpanFenceLength = fence.length;
42
+ out += fence;
43
+ index += fence.length;
44
+ continue;
45
+ }
46
+
47
+ if (source.startsWith("<!--", index)) {
48
+ inHtmlComment = true;
49
+ index += 4;
50
+ continue;
51
+ }
52
+
53
+ out += source[index];
54
+ index += 1;
55
+ }
56
+
57
+ return out;
58
+ }
59
+
60
+ export function stripStudioMarkdownHtmlComments(markdown) {
61
+ const lines = String(markdown ?? "").split("\n");
62
+ const out = [];
63
+ let plainBuffer = [];
64
+ let inFence = false;
65
+ let fenceChar;
66
+ let fenceLength = 0;
67
+
68
+ const flushPlain = () => {
69
+ if (plainBuffer.length === 0) return;
70
+ out.push(stripStudioMarkdownHtmlCommentsInSegment(plainBuffer.join("\n")));
71
+ plainBuffer = [];
72
+ };
73
+
74
+ for (const line of lines) {
75
+ const trimmed = line.trimStart();
76
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
77
+
78
+ if (fenceMatch) {
79
+ const marker = fenceMatch[1];
80
+ const markerChar = marker[0];
81
+ const markerLength = marker.length;
82
+
83
+ if (!inFence) {
84
+ flushPlain();
85
+ inFence = true;
86
+ fenceChar = markerChar;
87
+ fenceLength = markerLength;
88
+ out.push(line);
89
+ continue;
90
+ }
91
+
92
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
93
+ inFence = false;
94
+ fenceChar = undefined;
95
+ fenceLength = 0;
96
+ }
97
+
98
+ out.push(line);
99
+ continue;
100
+ }
101
+
102
+ if (inFence) {
103
+ out.push(line);
104
+ } else {
105
+ plainBuffer.push(line);
106
+ }
107
+ }
108
+
109
+ flushPlain();
110
+ return out.join("\n");
111
+ }