pi-studio 0.5.47 → 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.
@@ -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,33 +1531,41 @@
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
- const mathAnnotationStripped = rawHtml
1538
- .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
1539
- .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
1546
+ const mathAnnotationPreserved = rawHtml.replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (match, attrs, inner) => {
1547
+ const texAnnotationMatch = String(inner || "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
1548
+ const texSource = texAnnotationMatch ? String(texAnnotationMatch[1] || "").trim() : "";
1549
+ const cleanedInner = String(inner || "")
1550
+ .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
1551
+ .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
1552
+ const texAttr = texSource ? (" data-tex-source=\"" + escapeHtml(texSource) + "\"") : "";
1553
+ return "<math" + attrs + texAttr + ">" + cleanedInner + "</math>";
1554
+ });
1540
1555
 
1541
1556
  if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") {
1542
- return window.DOMPurify.sanitize(mathAnnotationStripped, {
1557
+ return window.DOMPurify.sanitize(mathAnnotationPreserved, {
1543
1558
  USE_PROFILES: {
1544
1559
  html: true,
1545
1560
  mathMl: true,
1546
1561
  svg: true,
1547
1562
  },
1548
1563
  ADD_TAGS: ["embed"],
1549
- ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
1564
+ ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align", "data-tex-source"],
1550
1565
  ADD_DATA_URI_TAGS: ["embed"],
1551
1566
  });
1552
1567
  }
1553
- return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1568
+ return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown, options);
1554
1569
  }
1555
1570
 
1556
1571
  function isPdfPreviewSource(src) {
@@ -2275,7 +2290,7 @@
2275
2290
  }
2276
2291
  }
2277
2292
 
2278
- async function renderMarkdownWithPandoc(markdown) {
2293
+ async function renderMarkdownWithPandoc(markdown, options) {
2279
2294
  const token = getToken();
2280
2295
  if (!token) {
2281
2296
  throw new Error("Missing Studio token in URL.");
@@ -2288,20 +2303,26 @@
2288
2303
  const controller = typeof AbortController === "function" ? new AbortController() : null;
2289
2304
  const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
2290
2305
 
2306
+ const previewOptions = options && typeof options === "object" ? options : {};
2307
+
2291
2308
  let response;
2292
2309
  try {
2293
2310
  const effectivePath = getEffectiveSavePath();
2294
2311
  const sourcePath = effectivePath || sourceState.path || "";
2312
+ const payload = {
2313
+ markdown: String(markdown || ""),
2314
+ sourcePath: sourcePath,
2315
+ resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
2316
+ };
2317
+ if (previewOptions.includeEditorLanguage) {
2318
+ payload.editorLanguage = String(editorLanguage || "");
2319
+ }
2295
2320
  response = await fetch("/render-preview?token=" + encodeURIComponent(token), {
2296
2321
  method: "POST",
2297
2322
  headers: {
2298
2323
  "Content-Type": "application/json",
2299
2324
  },
2300
- body: JSON.stringify({
2301
- markdown: String(markdown || ""),
2302
- sourcePath: sourcePath,
2303
- resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
2304
- }),
2325
+ body: JSON.stringify(payload),
2305
2326
  signal: controller ? controller.signal : undefined,
2306
2327
  });
2307
2328
  } catch (error) {
@@ -2524,9 +2545,15 @@
2524
2545
  const previewPrepared = annotationsEnabled
2525
2546
  ? prepareMarkdownForPandocPreview(markdown)
2526
2547
  : { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
2548
+ const previewingEditorText = pane === "source" || rightView === "editor-preview";
2549
+ const previewFallbackOptions = {
2550
+ stripMarkdownHtmlComments: !previewingEditorText || editorLanguage !== "latex",
2551
+ };
2527
2552
 
2528
2553
  try {
2529
- const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown);
2554
+ const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown, {
2555
+ includeEditorLanguage: pane === "source" || rightView === "editor-preview",
2556
+ });
2530
2557
 
2531
2558
  if (pane === "source") {
2532
2559
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -2536,7 +2563,7 @@
2536
2563
 
2537
2564
  clearPreviewJumpHighlight(targetEl);
2538
2565
  finishPreviewRender(targetEl);
2539
- targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2566
+ targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
2540
2567
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
2541
2568
  await renderAnnotationMathInElement(targetEl);
2542
2569
  decoratePdfEmbeds(targetEl);
@@ -2580,7 +2607,7 @@
2580
2607
  const detail = error && error.message ? error.message : String(error || "unknown error");
2581
2608
  clearPreviewJumpHighlight(targetEl);
2582
2609
  finishPreviewRender(targetEl);
2583
- targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2610
+ targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
2584
2611
  if (pane === "response") {
2585
2612
  applyPendingResponseScrollReset();
2586
2613
  scheduleResponsePaneRepaintNudge();
@@ -2591,9 +2618,8 @@
2591
2618
  function renderSourcePreviewNow() {
2592
2619
  if (editorView !== "preview") return;
2593
2620
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
2594
- if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
2595
- finishPreviewRender(sourcePreviewEl);
2596
- sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
2621
+ if (supportsCodePreviewCommentsForCurrentEditor()) {
2622
+ renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
2597
2623
  return;
2598
2624
  }
2599
2625
  const nonce = ++sourcePreviewRenderNonce;
@@ -2660,10 +2686,8 @@
2660
2686
  scheduleResponsePaneRepaintNudge();
2661
2687
  return;
2662
2688
  }
2663
- if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
2664
- finishPreviewRender(critiqueViewEl);
2665
- critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
2666
- scheduleResponsePaneRepaintNudge();
2689
+ if (supportsCodePreviewCommentsForCurrentEditor()) {
2690
+ renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
2667
2691
  return;
2668
2692
  }
2669
2693
  const nonce = ++responsePreviewRenderNonce;
@@ -3798,6 +3822,68 @@
3798
3822
  return out.join("<br>");
3799
3823
  }
3800
3824
 
3825
+ function supportsCodePreviewCommentsForCurrentEditor() {
3826
+ return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
3827
+ }
3828
+
3829
+ function getCodePreviewCommentKind(language) {
3830
+ const lang = normalizeFenceLanguage(language || "");
3831
+ if (lang === "diff") return "diff-line";
3832
+ if (lang === "text") return "text-line";
3833
+ return "code-line";
3834
+ }
3835
+
3836
+ function buildCodePreviewHtmlWithCommentBlocks(text, language) {
3837
+ const source = String(text || "").replace(/\r\n/g, "\n");
3838
+ const lines = source.split("\n");
3839
+ const lang = normalizeFenceLanguage(language || "");
3840
+ const kind = getCodePreviewCommentKind(lang);
3841
+ const html = [];
3842
+ let offset = 0;
3843
+
3844
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3845
+ const line = String(lines[lineIndex] || "");
3846
+ const start = offset;
3847
+ const end = start + line.length;
3848
+ const lineNumber = lineIndex + 1;
3849
+ const lineHtml = line.length === 0
3850
+ ? "<span class='hl-code'>" + EMPTY_OVERLAY_LINE + "</span>"
3851
+ : (lang ? highlightCodeLine(line, lang, "preview") : escapeHtml(line));
3852
+
3853
+ html.push(
3854
+ "<div class='preview-comment-block preview-comment-line-block'"
3855
+ + " data-review-note-start='" + String(start) + "'"
3856
+ + " data-review-note-end='" + String(end) + "'"
3857
+ + " data-review-note-line-start='" + String(lineNumber) + "'"
3858
+ + " data-review-note-line-end='" + String(lineNumber) + "'"
3859
+ + " data-preview-comment-kind='" + escapeHtml(kind) + "'"
3860
+ + ">"
3861
+ + "<div class='preview-comment-controls'>"
3862
+ + "<button type='button' class='preview-comment-summary' hidden></button>"
3863
+ + "<button type='button' class='preview-comment-add'>Comment</button>"
3864
+ + "</div>"
3865
+ + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3866
+ + "</div>",
3867
+ );
3868
+
3869
+ offset = end + 1;
3870
+ }
3871
+
3872
+ return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
3873
+ }
3874
+
3875
+ function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
3876
+ if (!targetEl) return;
3877
+ clearPreviewJumpHighlight(targetEl);
3878
+ finishPreviewRender(targetEl);
3879
+ targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
3880
+ updatePreviewCommentBlocksForElement(targetEl);
3881
+ if (pane === "response") {
3882
+ applyPendingResponseScrollReset();
3883
+ scheduleResponsePaneRepaintNudge();
3884
+ }
3885
+ }
3886
+
3801
3887
  function detectLanguageFromName(name) {
3802
3888
  if (!name) return "";
3803
3889
  var dot = name.lastIndexOf(".");
@@ -4361,24 +4447,329 @@
4361
4447
  }
4362
4448
 
4363
4449
  function supportsPreviewCommentsForCurrentEditor() {
4364
- return editorLanguage === "markdown";
4450
+ // LaTeX preview comments are intentionally disabled for now.
4451
+ // The initial client-side source/block heuristics were too unreliable on complex real documents.
4452
+ // Keep the independent LaTeX rendering fixes, but only enable preview comments where mapping is robust.
4453
+ return editorLanguage === "markdown" || supportsCodePreviewCommentsForCurrentEditor();
4365
4454
  }
4366
4455
 
4367
4456
  function getPreviewCommentBlockKindLabel(kind) {
4368
4457
  if (kind === "heading") return "heading";
4369
4458
  if (kind === "blockquote") return "quote block";
4370
4459
  if (kind === "list") return "list";
4460
+ if (kind === "math") return "equation";
4461
+ if (kind === "page-break") return "page break";
4371
4462
  if (kind === "code") return "code block";
4372
4463
  if (kind === "table") return "table";
4464
+ if (kind === "code-line") return "code line";
4465
+ if (kind === "diff-line") return "diff line";
4466
+ if (kind === "text-line") return "text line";
4373
4467
  return "paragraph";
4374
4468
  }
4375
4469
 
4376
4470
  function supportsPreviewSelectionCommentsForBlockKind(kind) {
4377
- return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
4471
+ return kind === "paragraph"
4472
+ || kind === "heading"
4473
+ || kind === "blockquote"
4474
+ || kind === "list"
4475
+ || kind === "math"
4476
+ || kind === "code-line"
4477
+ || kind === "diff-line"
4478
+ || kind === "text-line";
4479
+ }
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
+ }
4378
4694
  }
4379
4695
 
4380
4696
  function normalizeVisiblePreviewText(text) {
4381
- 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;
4382
4773
  }
4383
4774
 
4384
4775
  function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
@@ -4431,6 +4822,14 @@
4431
4822
  }
4432
4823
  }
4433
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
+
4434
4833
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4435
4834
  const line = lines[lineIndex] || "";
4436
4835
  if (kind === "blockquote") {
@@ -4458,6 +4857,26 @@
4458
4857
  return { text: chars.join(""), rawOffsets };
4459
4858
  }
4460
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
+
4461
4880
  function buildPreviewInlineDisplayMap(text, rawOffsets) {
4462
4881
  const source = String(text || "");
4463
4882
  const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
@@ -4471,6 +4890,12 @@
4471
4890
  charEnds.push(rawEnd);
4472
4891
  }
4473
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
+
4474
4899
  function appendNestedRange(startIndex, endIndex) {
4475
4900
  const nested = buildPreviewInlineDisplayMap(
4476
4901
  source.slice(startIndex, endIndex),
@@ -4499,15 +4924,86 @@
4499
4924
  const fence = "`".repeat(tickCount);
4500
4925
  const closeIndex = source.indexOf(fence, index + tickCount);
4501
4926
  if (closeIndex >= 0) {
4502
- for (let i = index + tickCount; i < closeIndex; i += 1) {
4503
- appendChar(source[i], rawMap[i], rawMap[i] + 1);
4504
- }
4927
+ appendRawRange(index + tickCount, closeIndex);
4505
4928
  index = closeIndex + tickCount;
4506
4929
  continue;
4507
4930
  }
4508
4931
  }
4509
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
+
4510
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
+ }
4511
5007
  appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4512
5008
  index += 2;
4513
5009
  continue;
@@ -4545,13 +5041,20 @@
4545
5041
  let pendingWhitespaceEnd = null;
4546
5042
 
4547
5043
  for (let i = 0; i < source.length; i += 1) {
4548
- 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
+ }
4549
5052
  if (/\s/.test(character)) {
4550
5053
  if (outChars.length === 0) continue;
4551
5054
  if (pendingWhitespaceStart == null) {
4552
- pendingWhitespaceStart = charStarts[i];
5055
+ pendingWhitespaceStart = startRef;
4553
5056
  }
4554
- pendingWhitespaceEnd = charEnds[i];
5057
+ pendingWhitespaceEnd = endRef;
4555
5058
  continue;
4556
5059
  }
4557
5060
 
@@ -4564,8 +5067,8 @@
4564
5067
  }
4565
5068
 
4566
5069
  outChars.push(character);
4567
- outStarts.push(charStarts[i]);
4568
- outEnds.push(charEnds[i]);
5070
+ outStarts.push(startRef);
5071
+ outEnds.push(endRef);
4569
5072
  }
4570
5073
 
4571
5074
  return {
@@ -4597,6 +5100,68 @@
4597
5100
  return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4598
5101
  }
4599
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
+
4600
5165
  function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4601
5166
  const source = String(haystack || "");
4602
5167
  const query = String(needle || "");
@@ -4711,6 +5276,11 @@
4711
5276
  return safeStartA < safeEndB && safeStartB < safeEndA;
4712
5277
  }
4713
5278
 
5279
+ function scanSourcePreviewCommentBlocks(markdown) {
5280
+ if (editorLanguage !== "markdown") return [];
5281
+ return scanMarkdownPreviewCommentBlocks(markdown);
5282
+ }
5283
+
4714
5284
  function scanMarkdownPreviewCommentBlocks(markdown) {
4715
5285
  const source = String(markdown || "").replace(/\r\n/g, "\n");
4716
5286
  const lines = source.split("\n");
@@ -4770,6 +5340,10 @@
4770
5340
  return /^\s*<!--/.test(getLine(index));
4771
5341
  }
4772
5342
 
5343
+ function isPageBreakLine(index) {
5344
+ return /^\\(?:newpage|pagebreak|clearpage)(?:\s*\[[^\]]*\])?\s*$/i.test(getLine(index));
5345
+ }
5346
+
4773
5347
  function makeBlock(kind, startLineIndex, endLineIndex) {
4774
5348
  const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4775
5349
  const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
@@ -4816,6 +5390,12 @@
4816
5390
  continue;
4817
5391
  }
4818
5392
 
5393
+ if (isPageBreakLine(index)) {
5394
+ blocks.push(makeBlock("page-break", index, index));
5395
+ index += 1;
5396
+ continue;
5397
+ }
5398
+
4819
5399
  const fenceMatch = lineStartsFence(index);
4820
5400
  if (fenceMatch) {
4821
5401
  const marker = fenceMatch[1] || "";
@@ -4914,11 +5494,83 @@
4914
5494
  index = endParagraph + 1;
4915
5495
  }
4916
5496
 
4917
- 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
+ });
4918
5564
  }
4919
5565
 
4920
5566
  function getPreviewCommentTargetKind(element) {
4921
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
+ }
4922
5574
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
4923
5575
  if (/^H[1-6]$/.test(tag)) return "heading";
4924
5576
  if (tag === "P") return "paragraph";
@@ -4926,6 +5578,9 @@
4926
5578
  if (tag === "UL" || tag === "OL") return "list";
4927
5579
  if (tag === "TABLE") return "table";
4928
5580
  if (tag === "PRE") return "code";
5581
+ if (tag === "MATH") {
5582
+ return String(element.getAttribute("display") || "").toLowerCase() === "block" ? "math" : "";
5583
+ }
4929
5584
  if (element.classList) {
4930
5585
  if (
4931
5586
  element.classList.contains("sourceCode")
@@ -4952,7 +5607,7 @@
4952
5607
 
4953
5608
  function collectPreviewCommentTargetElements(targetEl) {
4954
5609
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
4955
- 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";
4956
5611
  return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
4957
5612
  if (!isPreviewCommentTargetElement(element)) return false;
4958
5613
  let ancestor = element.parentElement;
@@ -4971,6 +5626,10 @@
4971
5626
  function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
4972
5627
  if (!sourceBlock) return "";
4973
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
+ }
4974
5633
  if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
4975
5634
  return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
4976
5635
  }
@@ -4994,14 +5653,26 @@
4994
5653
  function getNormalizedPreviewCommentTargetText(targetEntry) {
4995
5654
  if (!targetEntry) return "";
4996
5655
  if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
4997
- targetEntry.normalizedText = normalizeVisiblePreviewText(
4998
- targetEntry.element && typeof targetEntry.element.textContent === "string"
4999
- ? targetEntry.element.textContent
5000
- : "",
5001
- );
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);
5002
5662
  return targetEntry.normalizedText;
5003
5663
  }
5004
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
+
5005
5676
  function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5006
5677
  const desiredKind = sourceBlock ? sourceBlock.kind : "";
5007
5678
  const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
@@ -5017,7 +5688,7 @@
5017
5688
  if (targetText === desiredText) {
5018
5689
  return i;
5019
5690
  }
5020
- if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
5691
+ if (containsIndex < 0 && isHighConfidencePreviewTextContainmentMatch(targetText, desiredText)) {
5021
5692
  containsIndex = i;
5022
5693
  }
5023
5694
  }
@@ -5077,7 +5748,8 @@
5077
5748
 
5078
5749
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5079
5750
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5080
- const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
5751
+ splitMixedPreviewParagraphsAroundDisplayMath(targetEl);
5752
+ const sourceBlocks = scanSourcePreviewCommentBlocks(sourceText);
5081
5753
  const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5082
5754
  if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
5083
5755
 
@@ -5160,6 +5832,19 @@
5160
5832
  const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5161
5833
  if (blockEnd <= blockStart) return null;
5162
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
+
5163
5848
  const sourceBlockText = source.slice(blockStart, blockEnd);
5164
5849
  const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5165
5850
  if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
@@ -5167,8 +5852,8 @@
5167
5852
  const prefixRange = document.createRange();
5168
5853
  prefixRange.selectNodeContents(contentEl);
5169
5854
  prefixRange.setEnd(range.startContainer, range.startOffset);
5170
- const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5171
- const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
5855
+ const prefixText = buildNormalizedPreviewRangeText(prefixRange);
5856
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
5172
5857
  if (!selectedDisplayText) return null;
5173
5858
 
5174
5859
  const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
@@ -5271,14 +5956,52 @@
5271
5956
  return bestBlock;
5272
5957
  }
5273
5958
 
5959
+ function getPreviewNoteNormalizedSelectionText(note) {
5960
+ const direct = normalizeVisiblePreviewText(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "");
5961
+ if (direct) return direct;
5962
+ return "";
5963
+ }
5964
+
5965
+ function findPreviewCommentBlockForNoteText(targetEl, note) {
5966
+ if (!targetEl || !note || typeof targetEl.querySelectorAll !== "function") return null;
5967
+ const selectionText = getPreviewNoteNormalizedSelectionText(note);
5968
+ if (!selectionText) return null;
5969
+
5970
+ let bestBlock = null;
5971
+ let bestScore = Number.NEGATIVE_INFINITY;
5972
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5973
+ const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5974
+ const blockText = buildNormalizedPreviewSearchText(contentEl);
5975
+ if (!blockText) return;
5976
+ const matchIndex = blockText.indexOf(selectionText);
5977
+ if (matchIndex < 0) return;
5978
+ const lineStart = Math.max(1, Number(blockEl.dataset && blockEl.dataset.reviewNoteLineStart) || 1);
5979
+ const desiredLine = Math.max(1, Number(note && note.lineStart) || 1);
5980
+ const proximityPenalty = Math.abs(lineStart - desiredLine);
5981
+ const score = 1000000 - (matchIndex * 4) - proximityPenalty - Math.max(0, blockText.length - selectionText.length);
5982
+ if (score > bestScore) {
5983
+ bestScore = score;
5984
+ bestBlock = blockEl;
5985
+ }
5986
+ });
5987
+ return bestBlock;
5988
+ }
5989
+
5274
5990
  function revealReviewNoteInPreviewElement(targetEl, note) {
5275
5991
  if (!targetEl || !note) return false;
5276
5992
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5277
5993
  const range = resolveReviewNoteRange(note, source);
5278
5994
  if (!range) return false;
5279
- const blockEl = findPreviewCommentBlockForRange(targetEl, range);
5995
+ const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
5280
5996
  if (!blockEl) return false;
5281
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
+ }
5282
6005
  const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5283
6006
  if (typeof blockEl.scrollIntoView === "function") {
5284
6007
  blockEl.scrollIntoView({ block: "center", inline: "nearest" });
@@ -5288,6 +6011,7 @@
5288
6011
  }
5289
6012
 
5290
6013
  function revealReviewNoteInPreview(note) {
6014
+ if (!supportsPreviewCommentsForCurrentEditor()) return;
5291
6015
  if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
5292
6016
  revealReviewNoteInPreviewElement(critiqueViewEl, note);
5293
6017
  }
@@ -7689,7 +8413,7 @@
7689
8413
  event.preventDefault();
7690
8414
  event.stopPropagation();
7691
8415
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7692
- if (mode !== "selection") return;
8416
+ if (!mode || !mode.startsWith("selection")) return;
7693
8417
  addReviewNoteFromPreviewSelection(blockEl);
7694
8418
  }
7695
8419