pi-studio 0.5.48 → 0.5.50

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,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) {
@@ -1895,6 +1904,7 @@
1895
1904
 
1896
1905
  fallbackTargets.forEach((entry) => {
1897
1906
  entry.renderTarget.classList.add("studio-mathjax-fallback");
1907
+ entry.renderTarget.setAttribute("data-tex-source", entry.tex);
1898
1908
  if (entry.displayMode) {
1899
1909
  entry.renderTarget.classList.add("studio-mathjax-fallback-display");
1900
1910
  entry.renderTarget.textContent = "\\[\n" + entry.tex + "\n\\]";
@@ -2536,6 +2546,10 @@
2536
2546
  const previewPrepared = annotationsEnabled
2537
2547
  ? prepareMarkdownForPandocPreview(markdown)
2538
2548
  : { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
2549
+ const previewingEditorText = pane === "source" || rightView === "editor-preview";
2550
+ const previewFallbackOptions = {
2551
+ stripMarkdownHtmlComments: !previewingEditorText || editorLanguage !== "latex",
2552
+ };
2539
2553
 
2540
2554
  try {
2541
2555
  const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown, {
@@ -2550,7 +2564,7 @@
2550
2564
 
2551
2565
  clearPreviewJumpHighlight(targetEl);
2552
2566
  finishPreviewRender(targetEl);
2553
- targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2567
+ targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
2554
2568
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
2555
2569
  await renderAnnotationMathInElement(targetEl);
2556
2570
  decoratePdfEmbeds(targetEl);
@@ -2594,7 +2608,7 @@
2594
2608
  const detail = error && error.message ? error.message : String(error || "unknown error");
2595
2609
  clearPreviewJumpHighlight(targetEl);
2596
2610
  finishPreviewRender(targetEl);
2597
- targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2611
+ targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown, previewFallbackOptions);
2598
2612
  if (pane === "response") {
2599
2613
  applyPendingResponseScrollReset();
2600
2614
  scheduleResponsePaneRepaintNudge();
@@ -3847,7 +3861,8 @@
3847
3861
  + ">"
3848
3862
  + "<div class='preview-comment-controls'>"
3849
3863
  + "<button type='button' class='preview-comment-summary' hidden></button>"
3850
- + "<button type='button' class='preview-comment-add'>Comment</button>"
3864
+ + "<button type='button' class='preview-comment-add' data-preview-comment-action='comment'>Comment</button>"
3865
+ + "<button type='button' class='preview-comment-jump' data-preview-comment-action='jump'>Jump</button>"
3851
3866
  + "</div>"
3852
3867
  + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3853
3868
  + "</div>",
@@ -4434,16 +4449,19 @@
4434
4449
  }
4435
4450
 
4436
4451
  function supportsPreviewCommentsForCurrentEditor() {
4437
- // LaTeX preview comments are intentionally disabled for now.
4438
- // The initial client-side source/block heuristics were too unreliable on complex real documents.
4439
- // Keep the independent LaTeX rendering fixes, but only enable preview comments where mapping is robust.
4440
- return editorLanguage === "markdown" || supportsCodePreviewCommentsForCurrentEditor();
4452
+ return editorLanguage === "markdown"
4453
+ || editorLanguage === "latex"
4454
+ || supportsCodePreviewCommentsForCurrentEditor();
4441
4455
  }
4442
4456
 
4443
4457
  function getPreviewCommentBlockKindLabel(kind) {
4444
4458
  if (kind === "heading") return "heading";
4445
4459
  if (kind === "blockquote") return "quote block";
4446
4460
  if (kind === "list") return "list";
4461
+ if (kind === "math") return "equation";
4462
+ if (kind === "figure") return "figure";
4463
+ if (kind === "algorithm") return "algorithm block";
4464
+ if (kind === "page-break") return "page break";
4447
4465
  if (kind === "code") return "code block";
4448
4466
  if (kind === "table") return "table";
4449
4467
  if (kind === "code-line") return "code line";
@@ -4457,13 +4475,687 @@
4457
4475
  || kind === "heading"
4458
4476
  || kind === "blockquote"
4459
4477
  || kind === "list"
4478
+ || kind === "math"
4460
4479
  || kind === "code-line"
4461
4480
  || kind === "diff-line"
4462
4481
  || kind === "text-line";
4463
4482
  }
4464
4483
 
4484
+ const DISPLAY_MATH_ENV_NAMES = new Set([
4485
+ "displaymath",
4486
+ "equation",
4487
+ "equation*",
4488
+ "align",
4489
+ "align*",
4490
+ "aligned",
4491
+ "gather",
4492
+ "gather*",
4493
+ "multline",
4494
+ "multline*",
4495
+ "eqnarray",
4496
+ "eqnarray*",
4497
+ "split",
4498
+ ]);
4499
+
4500
+ function isEscapedAt(text, index) {
4501
+ let slashCount = 0;
4502
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
4503
+ slashCount += 1;
4504
+ }
4505
+ return (slashCount % 2) === 1;
4506
+ }
4507
+
4508
+ function readBalancedLatexGroup(source, startIndex, openChar, closeChar) {
4509
+ if (!source || source[startIndex] !== openChar) return null;
4510
+ let depth = 0;
4511
+ for (let index = startIndex; index < source.length; index += 1) {
4512
+ const ch = source[index];
4513
+ if (ch === "\\") {
4514
+ index += 1;
4515
+ continue;
4516
+ }
4517
+ if (ch === openChar) {
4518
+ depth += 1;
4519
+ continue;
4520
+ }
4521
+ if (ch === closeChar) {
4522
+ depth -= 1;
4523
+ if (depth === 0) {
4524
+ return {
4525
+ start: startIndex,
4526
+ contentStart: startIndex + 1,
4527
+ contentEnd: index,
4528
+ end: index + 1,
4529
+ };
4530
+ }
4531
+ }
4532
+ }
4533
+ return null;
4534
+ }
4535
+
4536
+ const DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS = new Set([
4537
+ "textbf",
4538
+ "textit",
4539
+ "emph",
4540
+ "underline",
4541
+ "texttt",
4542
+ "textrm",
4543
+ "textsf",
4544
+ "textsc",
4545
+ "mbox",
4546
+ "makebox",
4547
+ "framebox",
4548
+ "fbox",
4549
+ "url",
4550
+ "path",
4551
+ "nolinkurl",
4552
+ ]);
4553
+ const DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS = new Set([
4554
+ "href",
4555
+ "hyperref",
4556
+ ]);
4557
+ const DROPPED_MARKDOWN_RAW_TEX_STANDALONE_COMMANDS = new Set([
4558
+ "latex",
4559
+ "tex",
4560
+ "newpage",
4561
+ "pagebreak",
4562
+ "clearpage",
4563
+ ]);
4564
+
4565
+ function skipLatexWhitespace(source, startIndex) {
4566
+ let index = startIndex;
4567
+ while (index < source.length && /\s/.test(source[index])) index += 1;
4568
+ return index;
4569
+ }
4570
+
4571
+ function parseLatexCommandAt(source, startIndex) {
4572
+ if (!source || source[startIndex] !== "\\") return null;
4573
+ let index = startIndex + 1;
4574
+ if (index >= source.length) {
4575
+ return { name: "", end: index };
4576
+ }
4577
+ if (/[A-Za-z@]/.test(source[index])) {
4578
+ const nameStart = index;
4579
+ while (index < source.length && /[A-Za-z@]/.test(source[index])) index += 1;
4580
+ if (source[index] === "*") index += 1;
4581
+ return {
4582
+ name: source.slice(nameStart, index),
4583
+ end: index,
4584
+ };
4585
+ }
4586
+ return {
4587
+ name: source[index],
4588
+ end: index + 1,
4589
+ };
4590
+ }
4591
+
4592
+ function collectDisplayMathRanges(text) {
4593
+ const source = String(text || "");
4594
+ const ranges = [];
4595
+ let index = 0;
4596
+
4597
+ while (index < source.length) {
4598
+ if (source[index] === "%" && !isEscapedAt(source, index)) {
4599
+ while (index < source.length && source[index] !== "\n") index += 1;
4600
+ continue;
4601
+ }
4602
+ if (source.startsWith("$$", index)) {
4603
+ const close = source.indexOf("$$", index + 2);
4604
+ if (close >= 0) {
4605
+ ranges.push({
4606
+ start: index,
4607
+ end: close + 2,
4608
+ bodyStart: index + 2,
4609
+ bodyEnd: close,
4610
+ bodyText: source.slice(index + 2, close),
4611
+ });
4612
+ index = close + 2;
4613
+ continue;
4614
+ }
4615
+ }
4616
+ if (source.startsWith("\\[", index)) {
4617
+ const close = source.indexOf("\\]", index + 2);
4618
+ if (close >= 0) {
4619
+ ranges.push({
4620
+ start: index,
4621
+ end: close + 2,
4622
+ bodyStart: index + 2,
4623
+ bodyEnd: close,
4624
+ bodyText: source.slice(index + 2, close),
4625
+ });
4626
+ index = close + 2;
4627
+ continue;
4628
+ }
4629
+ }
4630
+ if (source.startsWith("\\begin{", index)) {
4631
+ const envGroup = readBalancedLatexGroup(source, index + 6, "{", "}");
4632
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim() : "";
4633
+ if (envName && DISPLAY_MATH_ENV_NAMES.has(envName)) {
4634
+ const closeToken = "\\end{" + envName + "}";
4635
+ const close = source.indexOf(closeToken, envGroup.end);
4636
+ if (close >= 0) {
4637
+ ranges.push({
4638
+ start: index,
4639
+ end: close + closeToken.length,
4640
+ bodyStart: envGroup.end,
4641
+ bodyEnd: close,
4642
+ bodyText: source.slice(envGroup.end, close),
4643
+ });
4644
+ index = close + closeToken.length;
4645
+ continue;
4646
+ }
4647
+ }
4648
+ }
4649
+ index += 1;
4650
+ }
4651
+
4652
+ return ranges;
4653
+ }
4654
+
4655
+ function getStandaloneDisplayMathRange(text) {
4656
+ const source = String(text || "");
4657
+ const leadingMatch = source.match(/^\s*/);
4658
+ const trailingMatch = source.match(/\s*$/);
4659
+ const leadingLength = leadingMatch ? leadingMatch[0].length : 0;
4660
+ const trailingLength = trailingMatch ? trailingMatch[0].length : 0;
4661
+ const trimmedEnd = Math.max(leadingLength, source.length - trailingLength);
4662
+ const trimmed = source.slice(leadingLength, trimmedEnd);
4663
+ if (!trimmed) return null;
4664
+ const ranges = collectDisplayMathRanges(trimmed);
4665
+ if (ranges.length !== 1) return null;
4666
+ const range = ranges[0];
4667
+ if (!range || range.start !== 0 || range.end !== trimmed.length) return null;
4668
+ return {
4669
+ start: leadingLength + range.start,
4670
+ end: leadingLength + range.end,
4671
+ bodyStart: leadingLength + range.bodyStart,
4672
+ bodyEnd: leadingLength + range.bodyEnd,
4673
+ bodyText: String(range.bodyText || ""),
4674
+ };
4675
+ }
4676
+
4677
+ const LATEX_PREVIEW_HEADING_COMMANDS = new Set([
4678
+ "part",
4679
+ "chapter",
4680
+ "section",
4681
+ "subsection",
4682
+ "subsubsection",
4683
+ "paragraph",
4684
+ "subparagraph",
4685
+ ]);
4686
+ const LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS = new Set([
4687
+ "part",
4688
+ "chapter",
4689
+ "section",
4690
+ "subsection",
4691
+ "subsubsection",
4692
+ "paragraph",
4693
+ "subparagraph",
4694
+ "title",
4695
+ "author",
4696
+ "caption",
4697
+ "text",
4698
+ "textbf",
4699
+ "textit",
4700
+ "emph",
4701
+ "underline",
4702
+ "texttt",
4703
+ "textrm",
4704
+ "textsf",
4705
+ "textsc",
4706
+ "mbox",
4707
+ "makebox",
4708
+ "framebox",
4709
+ "fbox",
4710
+ "url",
4711
+ "path",
4712
+ "nolinkurl",
4713
+ ]);
4714
+ const LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS = new Set([
4715
+ "href",
4716
+ "hyperref",
4717
+ ]);
4718
+ const LATEX_PREVIEW_HIDDEN_COMMANDS = new Set([
4719
+ "label",
4720
+ "ref",
4721
+ "eqref",
4722
+ "autoref",
4723
+ "pageref",
4724
+ "cite",
4725
+ "citet",
4726
+ "citep",
4727
+ "citealt",
4728
+ "citeauthor",
4729
+ "nocite",
4730
+ "footnote",
4731
+ "marginpar",
4732
+ "index",
4733
+ "includegraphics",
4734
+ "addbibresource",
4735
+ ]);
4736
+ const LATEX_PREVIEW_SKIPPED_ENV_NAMES = new Set([
4737
+ "document",
4738
+ "thebibliography",
4739
+ "itemize",
4740
+ "enumerate",
4741
+ "description",
4742
+ "figure",
4743
+ "figure*",
4744
+ "table",
4745
+ "table*",
4746
+ "tabular",
4747
+ "tabular*",
4748
+ "theorem",
4749
+ "lemma",
4750
+ "proposition",
4751
+ "corollary",
4752
+ "definition",
4753
+ "proof",
4754
+ "remark",
4755
+ "example",
4756
+ "verbatim",
4757
+ "lstlisting",
4758
+ "minted",
4759
+ "algorithm",
4760
+ "algorithm*",
4761
+ "algorithmic",
4762
+ ]);
4763
+ const LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME = new Map([
4764
+ ["figure", "figure"],
4765
+ ["figure*", "figure"],
4766
+ ["table", "table"],
4767
+ ["table*", "table"],
4768
+ ["algorithm", "algorithm"],
4769
+ ["algorithm*", "algorithm"],
4770
+ ]);
4771
+
4772
+ function stripLatexPreviewComments(text) {
4773
+ const source = String(text || "");
4774
+ let out = "";
4775
+ for (let index = 0; index < source.length; index += 1) {
4776
+ const ch = source[index];
4777
+ if (ch === "%" && !isEscapedAt(source, index)) {
4778
+ while (index < source.length && source[index] !== "\n") index += 1;
4779
+ if (index < source.length && source[index] === "\n") {
4780
+ out += "\n";
4781
+ }
4782
+ continue;
4783
+ }
4784
+ out += ch;
4785
+ }
4786
+ return out;
4787
+ }
4788
+
4789
+ function skipLatexPreviewCommentSpace(source, startIndex) {
4790
+ let index = Math.max(0, Number(startIndex) || 0);
4791
+ while (index < source.length) {
4792
+ const ch = source[index];
4793
+ if (/\s/.test(ch)) {
4794
+ index += 1;
4795
+ continue;
4796
+ }
4797
+ if (ch === "%" && !isEscapedAt(source, index)) {
4798
+ while (index < source.length && source[index] !== "\n") index += 1;
4799
+ continue;
4800
+ }
4801
+ break;
4802
+ }
4803
+ return index;
4804
+ }
4805
+
4806
+ function readLatexHeadingChunk(chunkText) {
4807
+ const source = String(chunkText || "");
4808
+ let index = skipLatexPreviewCommentSpace(source, 0);
4809
+ const command = parseLatexCommandAt(source, index);
4810
+ const commandName = command && command.name
4811
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4812
+ : "";
4813
+ if (!command || !LATEX_PREVIEW_HEADING_COMMANDS.has(commandName)) return null;
4814
+ index = skipLatexPreviewCommentSpace(source, command.end);
4815
+ if (source[index] === "[") {
4816
+ const optionalGroup = readBalancedLatexGroup(source, index, "[", "]");
4817
+ if (optionalGroup) {
4818
+ index = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4819
+ }
4820
+ }
4821
+ if (source[index] !== "{") return null;
4822
+ const titleGroup = readBalancedLatexGroup(source, index, "{", "}");
4823
+ if (!titleGroup) return null;
4824
+ index = skipLatexPreviewCommentSpace(source, titleGroup.end);
4825
+ while (index < source.length) {
4826
+ const trailingCommand = parseLatexCommandAt(source, index);
4827
+ const trailingName = trailingCommand && trailingCommand.name
4828
+ ? String(trailingCommand.name || "").replace(/\*$/, "").toLowerCase()
4829
+ : "";
4830
+ if (!trailingCommand || !LATEX_PREVIEW_HIDDEN_COMMANDS.has(trailingName)) {
4831
+ break;
4832
+ }
4833
+ let nextIndex = skipLatexPreviewCommentSpace(source, trailingCommand.end);
4834
+ if (source[nextIndex] === "[") {
4835
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4836
+ if (optionalGroup) {
4837
+ nextIndex = skipLatexPreviewCommentSpace(source, optionalGroup.end);
4838
+ }
4839
+ }
4840
+ if (source[nextIndex] === "{") {
4841
+ const argGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4842
+ if (argGroup) {
4843
+ nextIndex = skipLatexPreviewCommentSpace(source, argGroup.end);
4844
+ }
4845
+ }
4846
+ index = nextIndex;
4847
+ }
4848
+ if (skipLatexPreviewCommentSpace(source, index) < source.length) return null;
4849
+ return {
4850
+ commandName,
4851
+ titleText: source.slice(titleGroup.contentStart, titleGroup.contentEnd),
4852
+ };
4853
+ }
4854
+
4855
+ function extractLatexPreviewVisibleText(text) {
4856
+ const source = String(text || "");
4857
+ let out = "";
4858
+ let index = 0;
4859
+
4860
+ while (index < source.length) {
4861
+ const ch = source[index];
4862
+ if (ch === "%" && !isEscapedAt(source, index)) {
4863
+ while (index < source.length && source[index] !== "\n") index += 1;
4864
+ continue;
4865
+ }
4866
+ if (source.startsWith("$$", index)) {
4867
+ const close = source.indexOf("$$", index + 2);
4868
+ if (close >= 0) {
4869
+ out += " " + source.slice(index + 2, close) + " ";
4870
+ index = close + 2;
4871
+ continue;
4872
+ }
4873
+ }
4874
+ if (ch === "$" && !isEscapedAt(source, index)) {
4875
+ const close = findClosingUnescapedSequence(source, index + 1, "$", true);
4876
+ if (close >= 0) {
4877
+ out += " " + source.slice(index + 1, close) + " ";
4878
+ index = close + 1;
4879
+ continue;
4880
+ }
4881
+ }
4882
+ if (source.startsWith("\\(", index)) {
4883
+ const close = source.indexOf("\\)", index + 2);
4884
+ if (close >= 0) {
4885
+ out += " " + source.slice(index + 2, close) + " ";
4886
+ index = close + 2;
4887
+ continue;
4888
+ }
4889
+ }
4890
+ if (source.startsWith("\\[", index)) {
4891
+ const close = source.indexOf("\\]", index + 2);
4892
+ if (close >= 0) {
4893
+ out += " " + source.slice(index + 2, close) + " ";
4894
+ index = close + 2;
4895
+ continue;
4896
+ }
4897
+ }
4898
+ if (source.startsWith("\\begin{", index)) {
4899
+ const envGroup = readBalancedLatexGroup(source, index + 6, "{", "}");
4900
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim() : "";
4901
+ if (envName && DISPLAY_MATH_ENV_NAMES.has(envName)) {
4902
+ const closeToken = "\\end{" + envName + "}";
4903
+ const close = source.indexOf(closeToken, envGroup.end);
4904
+ if (close >= 0) {
4905
+ out += " " + source.slice(envGroup.end, close) + " ";
4906
+ index = close + closeToken.length;
4907
+ continue;
4908
+ }
4909
+ }
4910
+ }
4911
+ if (source.startsWith("\\end{", index)) {
4912
+ const envGroup = readBalancedLatexGroup(source, index + 4, "{", "}");
4913
+ if (envGroup) {
4914
+ index = envGroup.end;
4915
+ continue;
4916
+ }
4917
+ }
4918
+ if (ch === "\\") {
4919
+ const command = parseLatexCommandAt(source, index);
4920
+ const commandName = command && command.name
4921
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
4922
+ : "";
4923
+ if (!command) {
4924
+ index += 1;
4925
+ continue;
4926
+ }
4927
+ if (commandName === "begin" || commandName === "end") {
4928
+ let nextIndex = skipLatexWhitespace(source, command.end);
4929
+ if (source[nextIndex] === "{") {
4930
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4931
+ if (group) {
4932
+ index = group.end;
4933
+ continue;
4934
+ }
4935
+ }
4936
+ }
4937
+ if (commandName === "latex") {
4938
+ out += "LaTeX";
4939
+ index = command.end;
4940
+ continue;
4941
+ }
4942
+ if (commandName === "tex") {
4943
+ out += "TeX";
4944
+ index = command.end;
4945
+ continue;
4946
+ }
4947
+ if (commandName === "item") {
4948
+ out += " ";
4949
+ index = command.end;
4950
+ continue;
4951
+ }
4952
+ let nextIndex = skipLatexWhitespace(source, command.end);
4953
+ if (source[nextIndex] === "[") {
4954
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
4955
+ if (optionalGroup) {
4956
+ nextIndex = skipLatexWhitespace(source, optionalGroup.end);
4957
+ }
4958
+ }
4959
+ if (LATEX_PREVIEW_VISIBLE_GROUP_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4960
+ const group = readBalancedLatexGroup(source, nextIndex, "{", "}");
4961
+ if (group) {
4962
+ out += " " + extractLatexPreviewVisibleText(source.slice(group.contentStart, group.contentEnd)) + " ";
4963
+ index = group.end;
4964
+ continue;
4965
+ }
4966
+ }
4967
+ if (LATEX_PREVIEW_SECOND_ARG_VISIBLE_COMMANDS.has(commandName) && source[nextIndex] === "{") {
4968
+ const firstGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
4969
+ if (firstGroup) {
4970
+ let secondIndex = skipLatexWhitespace(source, firstGroup.end);
4971
+ if (source[secondIndex] === "{") {
4972
+ const secondGroup = readBalancedLatexGroup(source, secondIndex, "{", "}");
4973
+ if (secondGroup) {
4974
+ out += " " + extractLatexPreviewVisibleText(source.slice(secondGroup.contentStart, secondGroup.contentEnd)) + " ";
4975
+ index = secondGroup.end;
4976
+ continue;
4977
+ }
4978
+ }
4979
+ }
4980
+ }
4981
+ if (LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) {
4982
+ index = nextIndex;
4983
+ if (source[index] === "{") {
4984
+ const group = readBalancedLatexGroup(source, index, "{", "}");
4985
+ if (group) {
4986
+ index = group.end;
4987
+ continue;
4988
+ }
4989
+ }
4990
+ index = command.end;
4991
+ continue;
4992
+ }
4993
+ index = command.end;
4994
+ continue;
4995
+ }
4996
+ if (ch === "{" || ch === "}") {
4997
+ index += 1;
4998
+ continue;
4999
+ }
5000
+ if (ch === "~") {
5001
+ out += " ";
5002
+ index += 1;
5003
+ continue;
5004
+ }
5005
+ out += ch;
5006
+ index += 1;
5007
+ }
5008
+
5009
+ return normalizeVisiblePreviewText(out);
5010
+ }
5011
+
5012
+ function findLatexDocumentBodyRange(text) {
5013
+ const source = String(text || "");
5014
+ const beginMatch = source.match(/\\begin\{document\}/);
5015
+ if (!beginMatch || beginMatch.index == null) {
5016
+ return { start: 0, end: source.length };
5017
+ }
5018
+ const start = beginMatch.index + beginMatch[0].length;
5019
+ const endMatch = source.slice(start).match(/\\end\{document\}/);
5020
+ return {
5021
+ start,
5022
+ end: endMatch && endMatch.index != null ? (start + endMatch.index) : source.length,
5023
+ };
5024
+ }
5025
+
5026
+ function normalizeLatexPreviewBlockText(blockText, kind) {
5027
+ const source = String(blockText || "");
5028
+ if (/\\(?:bibliography|printbibliography)\b/i.test(source)) {
5029
+ return kind === "heading" ? "References" : "references";
5030
+ }
5031
+ if (kind === "math") {
5032
+ const mathRange = getStandaloneDisplayMathRange(stripLatexPreviewComments(source));
5033
+ return mathRange ? normalizeVisiblePreviewText(mathRange.bodyText) : normalizeVisiblePreviewText(source);
5034
+ }
5035
+ if (kind === "heading") {
5036
+ const heading = readLatexHeadingChunk(stripLatexPreviewComments(source));
5037
+ return heading ? extractLatexPreviewVisibleText(heading.titleText) : extractLatexPreviewVisibleText(source);
5038
+ }
5039
+ return extractLatexPreviewVisibleText(source);
5040
+ }
5041
+
5042
+ function isLatexPreviewSkippableChunk(chunkText) {
5043
+ const source = stripLatexPreviewComments(chunkText).trim();
5044
+ if (!source) return true;
5045
+ const command = parseLatexCommandAt(source, 0);
5046
+ const commandName = command && command.name
5047
+ ? String(command.name || "").replace(/\*$/, "").toLowerCase()
5048
+ : "";
5049
+ if (command && LATEX_PREVIEW_HIDDEN_COMMANDS.has(commandName)) return true;
5050
+ if (command && /^(?:documentclass|usepackage|newtheorem|title|author|date|maketitle|tableofcontents)$/i.test(commandName)) return true;
5051
+ if (source.startsWith("\\begin{")) {
5052
+ const envGroup = readBalancedLatexGroup(source, 6, "{", "}");
5053
+ const envName = envGroup ? source.slice(envGroup.contentStart, envGroup.contentEnd).trim().toLowerCase() : "";
5054
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName)) return true;
5055
+ }
5056
+ return false;
5057
+ }
5058
+
5059
+ function normalizePreviewComparableCharacter(character) {
5060
+ switch (String(character || "")) {
5061
+ case "\u2018":
5062
+ case "\u2019":
5063
+ case "\u201A":
5064
+ case "\u201B":
5065
+ return "'";
5066
+ case "\u201C":
5067
+ case "\u201D":
5068
+ case "\u201E":
5069
+ case "\u201F":
5070
+ return '"';
5071
+ case "\u2013":
5072
+ case "\u2014":
5073
+ case "\u2212":
5074
+ return "-";
5075
+ case "\u2026":
5076
+ return "…";
5077
+ default:
5078
+ return String(character || "");
5079
+ }
5080
+ }
5081
+
4465
5082
  function normalizeVisiblePreviewText(text) {
4466
- return String(text || "").replace(/\s+/g, " ").trim();
5083
+ const source = String(text || "");
5084
+ let normalized = "";
5085
+ let pendingWhitespace = false;
5086
+ for (let i = 0; i < source.length; i += 1) {
5087
+ let character = source[i] === "." && source.slice(i, i + 3) === "..."
5088
+ ? "…"
5089
+ : normalizePreviewComparableCharacter(source[i]);
5090
+ if (character === "…" && source[i] === "." && source.slice(i, i + 3) === "...") {
5091
+ i += 2;
5092
+ }
5093
+ if (/\s/.test(character)) {
5094
+ if (normalized) {
5095
+ pendingWhitespace = true;
5096
+ }
5097
+ continue;
5098
+ }
5099
+ if (pendingWhitespace && normalized) {
5100
+ normalized += " ";
5101
+ pendingWhitespace = false;
5102
+ }
5103
+ normalized += character;
5104
+ }
5105
+ return normalized.trim();
5106
+ }
5107
+
5108
+ function splitSourcePreviewCommentBlockByDisplayMath(sourceText, block) {
5109
+ if (!block || block.kind !== "paragraph") {
5110
+ return block ? [block] : [];
5111
+ }
5112
+ const source = String(sourceText || "");
5113
+ const blockStart = Math.max(0, Math.min(Number(block.start) || 0, source.length));
5114
+ const blockEnd = Math.max(blockStart, Math.min(Number(block.end) || blockStart, source.length));
5115
+ const blockText = source.slice(blockStart, blockEnd);
5116
+ const mathRanges = collectDisplayMathRanges(blockText);
5117
+ if (mathRanges.length === 0) {
5118
+ return [block];
5119
+ }
5120
+
5121
+ const segments = [];
5122
+ function pushSegment(kind, relativeStart, relativeEnd) {
5123
+ const safeRelativeStart = Math.max(0, Math.min(relativeStart, blockText.length));
5124
+ const safeRelativeEnd = Math.max(safeRelativeStart, Math.min(relativeEnd, blockText.length));
5125
+ if (safeRelativeEnd <= safeRelativeStart) return;
5126
+ const absoluteStart = blockStart + safeRelativeStart;
5127
+ const absoluteEnd = blockStart + safeRelativeEnd;
5128
+ const segmentText = source.slice(absoluteStart, absoluteEnd);
5129
+ if (kind === "paragraph" && !normalizeVisiblePreviewText(segmentText)) {
5130
+ return;
5131
+ }
5132
+ segments.push({
5133
+ kind,
5134
+ start: absoluteStart,
5135
+ end: absoluteEnd,
5136
+ lineStart: getLineNumberAtOffset(source, absoluteStart),
5137
+ lineEnd: getLineNumberAtOffset(source, Math.max(absoluteStart, absoluteEnd - 1)),
5138
+ });
5139
+ }
5140
+
5141
+ let cursor = 0;
5142
+ mathRanges.forEach((mathRange) => {
5143
+ if (!mathRange) return;
5144
+ pushSegment("paragraph", cursor, mathRange.start);
5145
+ pushSegment("math", mathRange.start, mathRange.end);
5146
+ cursor = mathRange.end;
5147
+ });
5148
+ pushSegment("paragraph", cursor, blockText.length);
5149
+
5150
+ return segments.length > 0 ? segments : [block];
5151
+ }
5152
+
5153
+ function expandSourcePreviewCommentBlocksByDisplayMath(sourceText, blocks) {
5154
+ const expanded = [];
5155
+ (Array.isArray(blocks) ? blocks : []).forEach((block) => {
5156
+ expanded.push(...splitSourcePreviewCommentBlockByDisplayMath(sourceText, block));
5157
+ });
5158
+ return expanded;
4467
5159
  }
4468
5160
 
4469
5161
  function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
@@ -4516,6 +5208,14 @@
4516
5208
  }
4517
5209
  }
4518
5210
 
5211
+ if (kind === "math") {
5212
+ const mathRange = getStandaloneDisplayMathRange(source);
5213
+ if (mathRange) {
5214
+ appendMappedPreviewSlice(chars, rawOffsets, source, 0, mathRange.bodyStart, mathRange.bodyEnd);
5215
+ return { text: chars.join(""), rawOffsets };
5216
+ }
5217
+ }
5218
+
4519
5219
  for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4520
5220
  const line = lines[lineIndex] || "";
4521
5221
  if (kind === "blockquote") {
@@ -4543,6 +5243,26 @@
4543
5243
  return { text: chars.join(""), rawOffsets };
4544
5244
  }
4545
5245
 
5246
+ function findClosingUnescapedSequence(source, startIndex, sequence) {
5247
+ const text = String(source || "");
5248
+ const needle = String(sequence || "");
5249
+ if (!text || !needle) return -1;
5250
+ let searchIndex = Math.max(0, Number(startIndex) || 0);
5251
+ while (searchIndex <= text.length) {
5252
+ const matchIndex = text.indexOf(needle, searchIndex);
5253
+ if (matchIndex < 0) return -1;
5254
+ let backslashCount = 0;
5255
+ for (let i = matchIndex - 1; i >= 0 && text[i] === "\\"; i -= 1) {
5256
+ backslashCount += 1;
5257
+ }
5258
+ if (backslashCount % 2 === 0) {
5259
+ return matchIndex;
5260
+ }
5261
+ searchIndex = matchIndex + needle.length;
5262
+ }
5263
+ return -1;
5264
+ }
5265
+
4546
5266
  function buildPreviewInlineDisplayMap(text, rawOffsets) {
4547
5267
  const source = String(text || "");
4548
5268
  const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
@@ -4556,6 +5276,12 @@
4556
5276
  charEnds.push(rawEnd);
4557
5277
  }
4558
5278
 
5279
+ function appendRawRange(startIndex, endIndex) {
5280
+ for (let i = startIndex; i < endIndex; i += 1) {
5281
+ appendChar(source[i], rawMap[i], rawMap[i] + 1);
5282
+ }
5283
+ }
5284
+
4559
5285
  function appendNestedRange(startIndex, endIndex) {
4560
5286
  const nested = buildPreviewInlineDisplayMap(
4561
5287
  source.slice(startIndex, endIndex),
@@ -4584,15 +5310,86 @@
4584
5310
  const fence = "`".repeat(tickCount);
4585
5311
  const closeIndex = source.indexOf(fence, index + tickCount);
4586
5312
  if (closeIndex >= 0) {
4587
- for (let i = index + tickCount; i < closeIndex; i += 1) {
4588
- appendChar(source[i], rawMap[i], rawMap[i] + 1);
4589
- }
5313
+ appendRawRange(index + tickCount, closeIndex);
4590
5314
  index = closeIndex + tickCount;
4591
5315
  continue;
4592
5316
  }
4593
5317
  }
4594
5318
 
5319
+ if (remaining.startsWith("\\(")) {
5320
+ const closeIndex = source.indexOf("\\)", index + 2);
5321
+ if (closeIndex >= 0) {
5322
+ appendRawRange(index + 2, closeIndex);
5323
+ index = closeIndex + 2;
5324
+ continue;
5325
+ }
5326
+ }
5327
+
5328
+ if (remaining.startsWith("\\[")) {
5329
+ const closeIndex = source.indexOf("\\]", index + 2);
5330
+ if (closeIndex >= 0) {
5331
+ appendRawRange(index + 2, closeIndex);
5332
+ index = closeIndex + 2;
5333
+ continue;
5334
+ }
5335
+ }
5336
+
5337
+ if (remaining.startsWith("$$")) {
5338
+ const closeIndex = findClosingUnescapedSequence(source, index + 2, "$$");
5339
+ if (closeIndex >= 0) {
5340
+ appendRawRange(index + 2, closeIndex);
5341
+ index = closeIndex + 2;
5342
+ continue;
5343
+ }
5344
+ }
5345
+
5346
+ if (source[index] === "$") {
5347
+ const closeIndex = findClosingUnescapedSequence(source, index + 1, "$");
5348
+ if (closeIndex >= 0) {
5349
+ appendRawRange(index + 1, closeIndex);
5350
+ index = closeIndex + 1;
5351
+ continue;
5352
+ }
5353
+ }
5354
+
4595
5355
  if (source[index] === "\\" && index + 1 < source.length) {
5356
+ const latexCommand = parseLatexCommandAt(source, index);
5357
+ const normalizedCommandName = latexCommand && latexCommand.name
5358
+ ? String(latexCommand.name || "").replace(/\*$/, "").toLowerCase()
5359
+ : "";
5360
+ const isDroppedLatexCommand = Boolean(
5361
+ normalizedCommandName
5362
+ && (
5363
+ DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS.has(normalizedCommandName)
5364
+ || DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName)
5365
+ || DROPPED_MARKDOWN_RAW_TEX_STANDALONE_COMMANDS.has(normalizedCommandName)
5366
+ )
5367
+ );
5368
+ if (latexCommand && isDroppedLatexCommand) {
5369
+ let nextIndex = skipLatexWhitespace(source, latexCommand.end);
5370
+ if (source[nextIndex] === "[") {
5371
+ const optionalGroup = readBalancedLatexGroup(source, nextIndex, "[", "]");
5372
+ if (optionalGroup) {
5373
+ nextIndex = skipLatexWhitespace(source, optionalGroup.end);
5374
+ }
5375
+ }
5376
+ if (DROPPED_MARKDOWN_RAW_TEX_GROUP_COMMANDS.has(normalizedCommandName) || DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName)) {
5377
+ if (source[nextIndex] === "{") {
5378
+ const firstGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
5379
+ if (firstGroup) {
5380
+ nextIndex = skipLatexWhitespace(source, firstGroup.end);
5381
+ }
5382
+ }
5383
+ }
5384
+ if (DROPPED_MARKDOWN_RAW_TEX_DOUBLE_GROUP_COMMANDS.has(normalizedCommandName) && source[nextIndex] === "{") {
5385
+ const secondGroup = readBalancedLatexGroup(source, nextIndex, "{", "}");
5386
+ if (secondGroup) {
5387
+ nextIndex = skipLatexWhitespace(source, secondGroup.end);
5388
+ }
5389
+ }
5390
+ index = Math.max(index + 1, nextIndex);
5391
+ continue;
5392
+ }
4596
5393
  appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4597
5394
  index += 2;
4598
5395
  continue;
@@ -4630,13 +5427,20 @@
4630
5427
  let pendingWhitespaceEnd = null;
4631
5428
 
4632
5429
  for (let i = 0; i < source.length; i += 1) {
4633
- const character = source[i];
5430
+ let character = normalizePreviewComparableCharacter(source[i]);
5431
+ let startRef = charStarts[i];
5432
+ let endRef = charEnds[i];
5433
+ if (source[i] === "." && source.slice(i, i + 3) === "...") {
5434
+ character = "…";
5435
+ endRef = charEnds[Math.min(i + 2, charEnds.length - 1)];
5436
+ i += 2;
5437
+ }
4634
5438
  if (/\s/.test(character)) {
4635
5439
  if (outChars.length === 0) continue;
4636
5440
  if (pendingWhitespaceStart == null) {
4637
- pendingWhitespaceStart = charStarts[i];
5441
+ pendingWhitespaceStart = startRef;
4638
5442
  }
4639
- pendingWhitespaceEnd = charEnds[i];
5443
+ pendingWhitespaceEnd = endRef;
4640
5444
  continue;
4641
5445
  }
4642
5446
 
@@ -4649,8 +5453,8 @@
4649
5453
  }
4650
5454
 
4651
5455
  outChars.push(character);
4652
- outStarts.push(charStarts[i]);
4653
- outEnds.push(charEnds[i]);
5456
+ outStarts.push(startRef);
5457
+ outEnds.push(endRef);
4654
5458
  }
4655
5459
 
4656
5460
  return {
@@ -4682,6 +5486,78 @@
4682
5486
  return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4683
5487
  }
4684
5488
 
5489
+ function getPreviewMathSearchText(element) {
5490
+ if (!element || !(element instanceof Element)) return null;
5491
+ const texSourceAttr = element.getAttribute("data-tex-source");
5492
+ if (texSourceAttr && texSourceAttr.trim()) {
5493
+ return texSourceAttr;
5494
+ }
5495
+ const tag = element.tagName ? element.tagName.toUpperCase() : "";
5496
+ if (tag === "MATH") {
5497
+ return typeof element.textContent === "string" ? element.textContent : "";
5498
+ }
5499
+ if (element.classList && element.classList.contains("math") && (element.classList.contains("inline") || element.classList.contains("display"))) {
5500
+ return extractMathFallbackTex(
5501
+ typeof element.textContent === "string" ? element.textContent : "",
5502
+ element.classList.contains("display"),
5503
+ );
5504
+ }
5505
+ if (
5506
+ element.classList
5507
+ && (element.classList.contains("studio-display-equation") || element.classList.contains("studio-display-equation-body"))
5508
+ && typeof element.querySelector === "function"
5509
+ ) {
5510
+ const innerMathEl = element.querySelector("[data-tex-source], math[display='block'], .studio-mathjax-fallback-display");
5511
+ if (innerMathEl && innerMathEl !== element) {
5512
+ return getPreviewMathSearchText(innerMathEl);
5513
+ }
5514
+ }
5515
+ return null;
5516
+ }
5517
+
5518
+ function buildNormalizedPreviewSearchText(rootNode) {
5519
+ if (!rootNode) return "";
5520
+ const parts = [];
5521
+
5522
+ function visit(node) {
5523
+ if (!node) return;
5524
+ if (node.nodeType === Node.TEXT_NODE) {
5525
+ parts.push(typeof node.nodeValue === "string" ? node.nodeValue : "");
5526
+ return;
5527
+ }
5528
+ if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
5529
+ return;
5530
+ }
5531
+ if (node.nodeType === Node.ELEMENT_NODE) {
5532
+ const element = node;
5533
+ const mathText = getPreviewMathSearchText(element);
5534
+ if (mathText != null) {
5535
+ parts.push(mathText);
5536
+ return;
5537
+ }
5538
+ if (element.tagName === "BR") {
5539
+ parts.push("\n");
5540
+ return;
5541
+ }
5542
+ }
5543
+ Array.from(node.childNodes || []).forEach(visit);
5544
+ }
5545
+
5546
+ visit(rootNode);
5547
+ return normalizeVisiblePreviewText(parts.join(""));
5548
+ }
5549
+
5550
+ function buildNormalizedPreviewRangeText(range) {
5551
+ if (!range || typeof range.cloneContents !== "function") {
5552
+ return "";
5553
+ }
5554
+ try {
5555
+ return buildNormalizedPreviewSearchText(range.cloneContents());
5556
+ } catch {
5557
+ return normalizeVisiblePreviewText(range.toString());
5558
+ }
5559
+ }
5560
+
4685
5561
  function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4686
5562
  const source = String(haystack || "");
4687
5563
  const query = String(needle || "");
@@ -4797,8 +5673,9 @@
4797
5673
  }
4798
5674
 
4799
5675
  function scanSourcePreviewCommentBlocks(markdown) {
4800
- if (editorLanguage !== "markdown") return [];
4801
- return scanMarkdownPreviewCommentBlocks(markdown);
5676
+ if (editorLanguage === "markdown") return scanMarkdownPreviewCommentBlocks(markdown);
5677
+ if (editorLanguage === "latex") return scanLatexPreviewCommentBlocks(markdown);
5678
+ return [];
4802
5679
  }
4803
5680
 
4804
5681
  function scanMarkdownPreviewCommentBlocks(markdown) {
@@ -4860,6 +5737,10 @@
4860
5737
  return /^\s*<!--/.test(getLine(index));
4861
5738
  }
4862
5739
 
5740
+ function isPageBreakLine(index) {
5741
+ return /^\\(?:newpage|pagebreak|clearpage)(?:\s*\[[^\]]*\])?\s*$/i.test(getLine(index));
5742
+ }
5743
+
4863
5744
  function makeBlock(kind, startLineIndex, endLineIndex) {
4864
5745
  const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4865
5746
  const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
@@ -4906,6 +5787,12 @@
4906
5787
  continue;
4907
5788
  }
4908
5789
 
5790
+ if (isPageBreakLine(index)) {
5791
+ blocks.push(makeBlock("page-break", index, index));
5792
+ index += 1;
5793
+ continue;
5794
+ }
5795
+
4909
5796
  const fenceMatch = lineStartsFence(index);
4910
5797
  if (fenceMatch) {
4911
5798
  const marker = fenceMatch[1] || "";
@@ -5004,18 +5891,353 @@
5004
5891
  index = endParagraph + 1;
5005
5892
  }
5006
5893
 
5894
+ return expandSourcePreviewCommentBlocksByDisplayMath(source, blocks);
5895
+ }
5896
+
5897
+ function scanLatexPreviewCommentBlocks(markdown) {
5898
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
5899
+ if (!source) return [];
5900
+ const bodyRange = findLatexDocumentBodyRange(source);
5901
+ const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
5902
+ const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
5903
+ const bodyText = source.slice(bodyStart, bodyEnd);
5904
+ const lines = bodyText.split("\n");
5905
+ const lineOffsets = [];
5906
+ let runningOffset = 0;
5907
+ for (const line of lines) {
5908
+ lineOffsets.push(runningOffset);
5909
+ runningOffset += line.length + 1;
5910
+ }
5911
+
5912
+ function getLine(index) {
5913
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
5914
+ }
5915
+
5916
+ function getStrippedLine(index) {
5917
+ return stripLatexPreviewComments(getLine(index)).trim();
5918
+ }
5919
+
5920
+ function isBlankLine(index) {
5921
+ return !getStrippedLine(index);
5922
+ }
5923
+
5924
+ function isBibliographyCommandLine(index) {
5925
+ return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
5926
+ }
5927
+
5928
+ function makeBlock(kind, startLineIndex, endLineIndex) {
5929
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
5930
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
5931
+ const start = bodyStart + (lineOffsets[safeStartLine] || 0);
5932
+ const end = bodyStart + (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
5933
+ return {
5934
+ kind,
5935
+ start,
5936
+ end,
5937
+ lineStart: getLineNumberAtOffset(source, start),
5938
+ lineEnd: getLineNumberAtOffset(source, Math.max(start, end - 1)),
5939
+ };
5940
+ }
5941
+
5942
+ function getChunkText(startLineIndex, endLineIndex) {
5943
+ return bodyText.slice(
5944
+ lineOffsets[startLineIndex] || 0,
5945
+ (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
5946
+ );
5947
+ }
5948
+
5949
+ function getEnvironmentStartName(index) {
5950
+ const line = getStrippedLine(index);
5951
+ const match = line.match(/^\\begin\{([^}]+)\}/);
5952
+ return match ? String(match[1] || "").trim().toLowerCase() : "";
5953
+ }
5954
+
5955
+ function findEnvironmentEndLine(startLineIndex, envName) {
5956
+ const openToken = "\\begin{" + envName + "}";
5957
+ const closeToken = "\\end{" + envName + "}";
5958
+ let depth = 0;
5959
+ for (let lineIndex = startLineIndex; lineIndex < lines.length; lineIndex += 1) {
5960
+ const line = getStrippedLine(lineIndex);
5961
+ if (line.includes(openToken)) depth += 1;
5962
+ if (line.includes(closeToken)) {
5963
+ depth -= 1;
5964
+ if (depth <= 0) return lineIndex;
5965
+ }
5966
+ }
5967
+ return startLineIndex;
5968
+ }
5969
+
5970
+ function isHeadingLine(index) {
5971
+ return Boolean(readLatexHeadingChunk(getLine(index)));
5972
+ }
5973
+
5974
+ function findBibliographyCommandEndLine(startLineIndex) {
5975
+ let endLineIndex = startLineIndex;
5976
+ for (let lineIndex = startLineIndex + 1; lineIndex < lines.length; lineIndex += 1) {
5977
+ if (!isBibliographyCommandLine(lineIndex)) break;
5978
+ endLineIndex = lineIndex;
5979
+ }
5980
+ return endLineIndex;
5981
+ }
5982
+
5983
+ function isMathStartLine(index) {
5984
+ const line = getStrippedLine(index);
5985
+ if (!line) return false;
5986
+ if (line.startsWith("$$") || line.startsWith("\\[")) return true;
5987
+ const envName = getEnvironmentStartName(index);
5988
+ return Boolean(envName && DISPLAY_MATH_ENV_NAMES.has(envName));
5989
+ }
5990
+
5991
+ function findMathEndLine(startLineIndex) {
5992
+ for (let endLineIndex = startLineIndex; endLineIndex < lines.length; endLineIndex += 1) {
5993
+ const chunkText = getChunkText(startLineIndex, endLineIndex);
5994
+ if (getStandaloneDisplayMathRange(stripLatexPreviewComments(chunkText))) {
5995
+ return endLineIndex;
5996
+ }
5997
+ }
5998
+ return startLineIndex;
5999
+ }
6000
+
6001
+ const blocks = [];
6002
+ let lineIndex = 0;
6003
+ while (lineIndex < lines.length) {
6004
+ if (isBlankLine(lineIndex)) {
6005
+ lineIndex += 1;
6006
+ continue;
6007
+ }
6008
+
6009
+ const strippedLine = getStrippedLine(lineIndex);
6010
+ const envName = getEnvironmentStartName(lineIndex);
6011
+
6012
+ if (isHeadingLine(lineIndex)) {
6013
+ blocks.push(makeBlock("heading", lineIndex, lineIndex));
6014
+ lineIndex += 1;
6015
+ continue;
6016
+ }
6017
+
6018
+ if (envName === "abstract" || envName === "keywords") {
6019
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6020
+ const chunkText = getChunkText(lineIndex, endLineIndex);
6021
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph")) {
6022
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6023
+ }
6024
+ lineIndex = endLineIndex + 1;
6025
+ continue;
6026
+ }
6027
+
6028
+ if (envName && LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.has(envName)) {
6029
+ const endLineIndex = findEnvironmentEndLine(lineIndex, envName);
6030
+ blocks.push(makeBlock(LATEX_PREVIEW_STRUCTURAL_ENV_KIND_BY_NAME.get(envName) || "paragraph", lineIndex, endLineIndex));
6031
+ lineIndex = endLineIndex + 1;
6032
+ continue;
6033
+ }
6034
+
6035
+ if (isBibliographyCommandLine(lineIndex)) {
6036
+ const endLineIndex = findBibliographyCommandEndLine(lineIndex);
6037
+ blocks.push(makeBlock("heading", lineIndex, endLineIndex));
6038
+ blocks.push(makeBlock("paragraph", lineIndex, endLineIndex));
6039
+ lineIndex = endLineIndex + 1;
6040
+ continue;
6041
+ }
6042
+
6043
+ if (envName && LATEX_PREVIEW_SKIPPED_ENV_NAMES.has(envName) && !DISPLAY_MATH_ENV_NAMES.has(envName)) {
6044
+ lineIndex = findEnvironmentEndLine(lineIndex, envName) + 1;
6045
+ continue;
6046
+ }
6047
+
6048
+ if (isMathStartLine(lineIndex)) {
6049
+ const endLineIndex = findMathEndLine(lineIndex);
6050
+ blocks.push(makeBlock("math", lineIndex, endLineIndex));
6051
+ lineIndex = endLineIndex + 1;
6052
+ continue;
6053
+ }
6054
+
6055
+ if (isLatexPreviewSkippableChunk(strippedLine)) {
6056
+ lineIndex += 1;
6057
+ continue;
6058
+ }
6059
+
6060
+ const paragraphStartLine = lineIndex;
6061
+ let paragraphEndLine = lineIndex;
6062
+ for (let nextLineIndex = lineIndex + 1; nextLineIndex < lines.length; nextLineIndex += 1) {
6063
+ if (isBlankLine(nextLineIndex) || isHeadingLine(nextLineIndex) || isMathStartLine(nextLineIndex)) {
6064
+ break;
6065
+ }
6066
+ const nextEnvName = getEnvironmentStartName(nextLineIndex);
6067
+ if (nextEnvName) {
6068
+ break;
6069
+ }
6070
+ paragraphEndLine = nextLineIndex;
6071
+ }
6072
+
6073
+ const chunkText = getChunkText(paragraphStartLine, paragraphEndLine);
6074
+ if (normalizeLatexPreviewBlockText(chunkText, "paragraph") && !isLatexPreviewSkippableChunk(chunkText)) {
6075
+ blocks.push(makeBlock("paragraph", paragraphStartLine, paragraphEndLine));
6076
+ }
6077
+ lineIndex = paragraphEndLine + 1;
6078
+ }
6079
+
5007
6080
  return blocks;
5008
6081
  }
5009
6082
 
6083
+ function isPreviewDisplayMathElement(element) {
6084
+ return Boolean(
6085
+ element
6086
+ && element instanceof Element
6087
+ && element.matches
6088
+ && element.matches("math[display='block'], .studio-mathjax-fallback-display, .studio-display-equation, .studio-display-equation-body")
6089
+ );
6090
+ }
6091
+
6092
+ function previewNodesHaveVisibleContent(nodes) {
6093
+ return (Array.isArray(nodes) ? nodes : []).some((node) => {
6094
+ if (!node) return false;
6095
+ if (node.nodeType === Node.TEXT_NODE) {
6096
+ return Boolean(normalizeVisiblePreviewText(node.nodeValue || ""));
6097
+ }
6098
+ return node instanceof Element && Boolean(buildNormalizedPreviewSearchText(node));
6099
+ });
6100
+ }
6101
+
6102
+ function wrapLoosePreviewInlineRunsAsParagraphs(targetEl) {
6103
+ if (!targetEl || !targetEl.childNodes || typeof document.createElement !== "function") return;
6104
+ const childNodes = Array.from(targetEl.childNodes || []);
6105
+ if (childNodes.length === 0) return;
6106
+
6107
+ function isDirectBlockChild(node) {
6108
+ if (!(node instanceof Element) || node.parentElement !== targetEl) return false;
6109
+ const tag = node.tagName ? node.tagName.toUpperCase() : "";
6110
+ if (/^H[1-6]$/.test(tag)) return true;
6111
+ if (tag === "P" || tag === "BLOCKQUOTE" || tag === "UL" || tag === "OL" || tag === "TABLE" || tag === "PRE" || tag === "HEADER" || tag === "FIGURE") {
6112
+ return true;
6113
+ }
6114
+ if (tag === "MATH") {
6115
+ return String(node.getAttribute("display") || "").toLowerCase() === "block";
6116
+ }
6117
+ if (tag === "DIV") return true;
6118
+ return false;
6119
+ }
6120
+
6121
+ let runNodes = [];
6122
+
6123
+ function flushRun(referenceNode) {
6124
+ if (runNodes.length === 0) return;
6125
+ if (!previewNodesHaveVisibleContent(runNodes)) {
6126
+ runNodes.forEach((node) => {
6127
+ if (node && node.parentNode === targetEl) {
6128
+ targetEl.removeChild(node);
6129
+ }
6130
+ });
6131
+ runNodes = [];
6132
+ return;
6133
+ }
6134
+ const paragraphEl = document.createElement("p");
6135
+ runNodes.forEach((node) => {
6136
+ paragraphEl.appendChild(node);
6137
+ });
6138
+ targetEl.insertBefore(paragraphEl, referenceNode || null);
6139
+ runNodes = [];
6140
+ }
6141
+
6142
+ childNodes.forEach((node) => {
6143
+ if (node instanceof Element && isDirectBlockChild(node)) {
6144
+ flushRun(node);
6145
+ return;
6146
+ }
6147
+ if (node.parentNode === targetEl) {
6148
+ runNodes.push(node);
6149
+ }
6150
+ });
6151
+ flushRun(null);
6152
+ }
6153
+
6154
+ function splitMixedPreviewParagraphsAroundDisplayMath(targetEl) {
6155
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6156
+ if (editorLanguage === "latex") {
6157
+ wrapLoosePreviewInlineRunsAsParagraphs(targetEl);
6158
+ }
6159
+ Array.from(targetEl.querySelectorAll("p")).forEach((paragraphEl) => {
6160
+ if (!(paragraphEl instanceof Element) || !paragraphEl.parentNode) return;
6161
+ if (paragraphEl.closest && paragraphEl.closest(".preview-comment-block")) return;
6162
+ let ancestor = paragraphEl.parentElement;
6163
+ while (ancestor && ancestor !== targetEl) {
6164
+ if (getPreviewCommentTargetKind(ancestor)) return;
6165
+ ancestor = ancestor.parentElement;
6166
+ }
6167
+ const childNodes = Array.from(paragraphEl.childNodes || []);
6168
+ if (!childNodes.some((node) => isPreviewDisplayMathElement(node))) return;
6169
+
6170
+ const fragment = document.createDocumentFragment();
6171
+ let proseNodes = [];
6172
+ let segmentCount = 0;
6173
+
6174
+ function flushProse() {
6175
+ if (proseNodes.length === 0) return;
6176
+ if (!previewNodesHaveVisibleContent(proseNodes)) {
6177
+ proseNodes = [];
6178
+ return;
6179
+ }
6180
+ const proseEl = paragraphEl.cloneNode(false);
6181
+ if (proseEl instanceof Element) {
6182
+ proseEl.removeAttribute("id");
6183
+ }
6184
+ proseNodes.forEach((node) => {
6185
+ proseEl.appendChild(node);
6186
+ });
6187
+ fragment.appendChild(proseEl);
6188
+ proseNodes = [];
6189
+ segmentCount += 1;
6190
+ }
6191
+
6192
+ childNodes.forEach((node) => {
6193
+ if (isPreviewDisplayMathElement(node)) {
6194
+ flushProse();
6195
+ fragment.appendChild(node);
6196
+ segmentCount += 1;
6197
+ return;
6198
+ }
6199
+ proseNodes.push(node);
6200
+ });
6201
+ flushProse();
6202
+
6203
+ if (segmentCount > 0) {
6204
+ paragraphEl.replaceWith(fragment);
6205
+ }
6206
+ });
6207
+ }
6208
+
5010
6209
  function getPreviewCommentTargetKind(element) {
5011
6210
  if (!element || !(element instanceof Element)) return "";
6211
+ if (element.classList && element.classList.contains("studio-mathjax-fallback-display")) {
6212
+ return "math";
6213
+ }
6214
+ if (element.classList && element.classList.contains("studio-page-break")) {
6215
+ return "page-break";
6216
+ }
5012
6217
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
5013
6218
  if (/^H[1-6]$/.test(tag)) return "heading";
5014
6219
  if (tag === "P") return "paragraph";
6220
+ if (tag === "FIGURE") {
6221
+ if (element.classList && element.classList.contains("studio-algorithm-block")) {
6222
+ return "algorithm";
6223
+ }
6224
+ return editorLanguage === "latex" ? "figure" : "";
6225
+ }
6226
+ if (tag === "DIV" && element.classList) {
6227
+ if (element.classList.contains("studio-display-equation")) {
6228
+ return "math";
6229
+ }
6230
+ if (element.classList.contains("abstract") || element.classList.contains("keywords") || element.classList.contains("references")) {
6231
+ return "paragraph";
6232
+ }
6233
+ }
5015
6234
  if (tag === "BLOCKQUOTE") return "blockquote";
5016
6235
  if (tag === "UL" || tag === "OL") return "list";
5017
6236
  if (tag === "TABLE") return "table";
5018
6237
  if (tag === "PRE") return "code";
6238
+ if (tag === "MATH") {
6239
+ return String(element.getAttribute("display") || "").toLowerCase() === "block" ? "math" : "";
6240
+ }
5019
6241
  if (element.classList) {
5020
6242
  if (
5021
6243
  element.classList.contains("sourceCode")
@@ -5040,11 +6262,49 @@
5040
6262
  return Boolean(getPreviewCommentTargetKind(element));
5041
6263
  }
5042
6264
 
6265
+ function isLatexPreviewCommentTargetElement(element, targetEl) {
6266
+ if (!element || !(element instanceof Element) || !targetEl) return false;
6267
+ const kind = getPreviewCommentTargetKind(element);
6268
+ if (kind === "heading" || kind === "paragraph" || kind === "figure" || kind === "algorithm" || kind === "table") {
6269
+ if (element.parentElement === targetEl) return true;
6270
+ if (
6271
+ kind === "paragraph"
6272
+ && element.classList
6273
+ && element.classList.contains("abstract")
6274
+ && element.parentElement
6275
+ && element.parentElement.tagName === "HEADER"
6276
+ && element.parentElement.id === "title-block-header"
6277
+ && element.parentElement.parentElement === targetEl
6278
+ ) {
6279
+ return true;
6280
+ }
6281
+ return false;
6282
+ }
6283
+ if (kind === "math") {
6284
+ if (element.parentElement === targetEl) return true;
6285
+ const bodyEl = element.parentElement;
6286
+ const frameEl = bodyEl && bodyEl.parentElement;
6287
+ return Boolean(
6288
+ bodyEl
6289
+ && bodyEl.classList
6290
+ && bodyEl.classList.contains("studio-display-equation-body")
6291
+ && frameEl
6292
+ && frameEl.classList
6293
+ && frameEl.classList.contains("studio-display-equation")
6294
+ && frameEl.parentElement === targetEl
6295
+ );
6296
+ }
6297
+ return false;
6298
+ }
6299
+
5043
6300
  function collectPreviewCommentTargetElements(targetEl) {
5044
6301
  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";
6302
+ const selector = "h1, h2, h3, h4, h5, h6, p, figure, blockquote, ul, ol, table, div.sourceCode, pre, math[display='block'], .studio-display-equation, .studio-mathjax-fallback-display, .studio-page-break, .abstract, .keywords, .references, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
5046
6303
  return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
5047
6304
  if (!isPreviewCommentTargetElement(element)) return false;
6305
+ if (editorLanguage === "latex" && !isLatexPreviewCommentTargetElement(element, targetEl)) {
6306
+ return false;
6307
+ }
5048
6308
  let ancestor = element.parentElement;
5049
6309
  while (ancestor && ancestor !== targetEl) {
5050
6310
  if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
@@ -5061,6 +6321,13 @@
5061
6321
  function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
5062
6322
  if (!sourceBlock) return "";
5063
6323
  const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
6324
+ if (editorLanguage === "latex") {
6325
+ return normalizeLatexPreviewBlockText(blockText, sourceBlock.kind);
6326
+ }
6327
+ if (sourceBlock.kind === "page-break") {
6328
+ const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
6329
+ return match ? String(match[1] || "").toLowerCase() : "page-break";
6330
+ }
5064
6331
  if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
5065
6332
  return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
5066
6333
  }
@@ -5084,21 +6351,66 @@
5084
6351
  function getNormalizedPreviewCommentTargetText(targetEntry) {
5085
6352
  if (!targetEntry) return "";
5086
6353
  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
- );
6354
+ if (targetEntry.kind === "page-break") {
6355
+ const element = targetEntry.element;
6356
+ targetEntry.normalizedText = String(element && element.getAttribute ? (element.getAttribute("data-page-break-kind") || "page-break") : "page-break").toLowerCase();
6357
+ return targetEntry.normalizedText;
6358
+ }
6359
+ targetEntry.normalizedText = buildNormalizedPreviewSearchText(targetEntry.element);
5092
6360
  return targetEntry.normalizedText;
5093
6361
  }
5094
6362
 
6363
+ function isHighConfidencePreviewTextContainmentMatch(leftText, rightText) {
6364
+ const left = String(leftText || "");
6365
+ const right = String(rightText || "");
6366
+ if (!left || !right || left === right) return false;
6367
+ const shorter = left.length <= right.length ? left : right;
6368
+ const longer = left.length <= right.length ? right : left;
6369
+ if (shorter.length < 12) return false;
6370
+ if (!/\s/.test(shorter)) return false;
6371
+ return longer.includes(shorter);
6372
+ }
6373
+
6374
+ function tokenizePreviewComparableText(text) {
6375
+ return normalizeVisiblePreviewText(text)
6376
+ .toLowerCase()
6377
+ .split(/\s+/)
6378
+ .map((token) => token.replace(/^[^0-9A-Za-z\u00C0-\uFFFF]+|[^0-9A-Za-z\u00C0-\uFFFF]+$/g, ""))
6379
+ .filter((token) => token && (token.length >= 4 || /[A-Za-z\u00C0-\uFFFF]/.test(token)));
6380
+ }
6381
+
6382
+ function getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText) {
6383
+ if (editorLanguage !== "latex") return -1;
6384
+ const targetTokens = tokenizePreviewComparableText(targetText);
6385
+ const desiredTokens = tokenizePreviewComparableText(desiredText);
6386
+ if (targetTokens.length === 0 || desiredTokens.length < 5) return -1;
6387
+
6388
+ let targetTokenIndex = 0;
6389
+ let matchedCount = 0;
6390
+ for (const token of desiredTokens) {
6391
+ while (targetTokenIndex < targetTokens.length && targetTokens[targetTokenIndex] !== token) {
6392
+ targetTokenIndex += 1;
6393
+ }
6394
+ if (targetTokenIndex >= targetTokens.length) break;
6395
+ matchedCount += 1;
6396
+ targetTokenIndex += 1;
6397
+ }
6398
+
6399
+ const matchRatio = matchedCount / desiredTokens.length;
6400
+ if (matchedCount < 5 || matchRatio < 0.6) return -1;
6401
+ return matchedCount * 1000 + Math.round(matchRatio * 100);
6402
+ }
6403
+
5095
6404
  function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5096
6405
  const desiredKind = sourceBlock ? sourceBlock.kind : "";
5097
6406
  const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
6407
+ const preferredStartIndex = Math.max(0, startIndex || 0);
5098
6408
  let fallbackIndex = -1;
5099
6409
  let containsIndex = -1;
6410
+ let orderedTokenIndex = -1;
6411
+ let orderedTokenScore = Number.NEGATIVE_INFINITY;
5100
6412
 
5101
- for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
6413
+ for (let i = preferredStartIndex; i < targetBlocks.length; i += 1) {
5102
6414
  const targetEntry = targetBlocks[i];
5103
6415
  if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5104
6416
  if (fallbackIndex < 0) fallbackIndex = i;
@@ -5107,13 +6419,22 @@
5107
6419
  if (targetText === desiredText) {
5108
6420
  return i;
5109
6421
  }
5110
- if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
6422
+ if (containsIndex < 0 && isHighConfidencePreviewTextContainmentMatch(targetText, desiredText)) {
5111
6423
  containsIndex = i;
5112
6424
  }
6425
+ const latexTokenScore = getHighConfidenceLatexOrderedTokenMatchScore(targetText, desiredText);
6426
+ if (latexTokenScore >= 0) {
6427
+ const score = latexTokenScore - (Math.abs(i - preferredStartIndex) * 4);
6428
+ if (score > orderedTokenScore) {
6429
+ orderedTokenScore = score;
6430
+ orderedTokenIndex = i;
6431
+ }
6432
+ }
5113
6433
  }
5114
6434
  }
5115
6435
 
5116
6436
  if (containsIndex >= 0) return containsIndex;
6437
+ if (orderedTokenIndex >= 0) return orderedTokenIndex;
5117
6438
  return fallbackIndex;
5118
6439
  }
5119
6440
 
@@ -5132,6 +6453,7 @@
5132
6453
  const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5133
6454
  const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5134
6455
  const addBtn = blockEl.querySelector(".preview-comment-add");
6456
+ const jumpBtn = blockEl.querySelector(".preview-comment-jump");
5135
6457
  const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5136
6458
  const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5137
6459
  const blockKey = getPreviewCommentBlockKey(blockEl);
@@ -5155,6 +6477,16 @@
5155
6477
  : "";
5156
6478
  addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5157
6479
  }
6480
+
6481
+ if (jumpBtn) {
6482
+ jumpBtn.hidden = !hasSelection;
6483
+ jumpBtn.textContent = "Jump";
6484
+ jumpBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
6485
+ jumpBtn.title = hasSelection
6486
+ ? ("Jump to the current preview selection on this " + blockKindLabel + " in the raw editor (" + lineLabel + ").")
6487
+ : "";
6488
+ jumpBtn.setAttribute("aria-label", jumpBtn.title || "Jump");
6489
+ }
5158
6490
  }
5159
6491
 
5160
6492
  function updatePreviewCommentBlocksForElement(targetEl) {
@@ -5167,6 +6499,7 @@
5167
6499
 
5168
6500
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5169
6501
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
6502
+ splitMixedPreviewParagraphsAroundDisplayMath(targetEl);
5170
6503
  const sourceBlocks = scanSourcePreviewCommentBlocks(sourceText);
5171
6504
  const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5172
6505
  if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
@@ -5201,9 +6534,17 @@
5201
6534
  const addBtn = document.createElement("button");
5202
6535
  addBtn.type = "button";
5203
6536
  addBtn.className = "preview-comment-add";
6537
+ addBtn.dataset.previewCommentAction = "comment";
5204
6538
  addBtn.textContent = "Comment";
5205
6539
  controls.appendChild(addBtn);
5206
6540
 
6541
+ const jumpBtn = document.createElement("button");
6542
+ jumpBtn.type = "button";
6543
+ jumpBtn.className = "preview-comment-jump";
6544
+ jumpBtn.dataset.previewCommentAction = "jump";
6545
+ jumpBtn.textContent = "Jump";
6546
+ controls.appendChild(jumpBtn);
6547
+
5207
6548
  originalElement.replaceWith(wrapper);
5208
6549
  wrapper.appendChild(controls);
5209
6550
  originalElement.classList.add("preview-comment-block-content");
@@ -5250,6 +6591,32 @@
5250
6591
  const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5251
6592
  if (blockEnd <= blockStart) return null;
5252
6593
 
6594
+ if (kind === "math") {
6595
+ const selectedDisplayText = normalizeVisiblePreviewText(getPreviewMathSearchText(contentEl) || buildNormalizedPreviewSearchText(contentEl));
6596
+ if (!selectedDisplayText) return null;
6597
+ return {
6598
+ selectionStart: blockStart,
6599
+ selectionEnd: blockEnd,
6600
+ lineStart: getLineNumberAtOffset(source, blockStart),
6601
+ lineEnd: getLineNumberAtOffset(source, Math.max(blockStart, blockEnd - 1)),
6602
+ selectedText: source.slice(blockStart, blockEnd),
6603
+ selectedDisplayText,
6604
+ };
6605
+ }
6606
+
6607
+ if (editorLanguage === "latex") {
6608
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
6609
+ if (!selectedDisplayText) return null;
6610
+ return {
6611
+ selectionStart: blockStart,
6612
+ selectionEnd: blockEnd,
6613
+ lineStart: getLineNumberAtOffset(source, blockStart),
6614
+ lineEnd: getLineNumberAtOffset(source, Math.max(blockStart, blockEnd - 1)),
6615
+ selectedText: source.slice(blockStart, blockEnd),
6616
+ selectedDisplayText,
6617
+ };
6618
+ }
6619
+
5253
6620
  const sourceBlockText = source.slice(blockStart, blockEnd);
5254
6621
  const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5255
6622
  if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
@@ -5257,8 +6624,8 @@
5257
6624
  const prefixRange = document.createRange();
5258
6625
  prefixRange.selectNodeContents(contentEl);
5259
6626
  prefixRange.setEnd(range.startContainer, range.startOffset);
5260
- const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5261
- const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
6627
+ const prefixText = buildNormalizedPreviewRangeText(prefixRange);
6628
+ const selectedDisplayText = buildNormalizedPreviewRangeText(range);
5262
6629
  if (!selectedDisplayText) return null;
5263
6630
 
5264
6631
  const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
@@ -5376,7 +6743,7 @@
5376
6743
  let bestScore = Number.NEGATIVE_INFINITY;
5377
6744
  Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5378
6745
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5379
- const blockText = normalizeVisiblePreviewText(buildNormalizedDomTextMap(contentEl).text);
6746
+ const blockText = buildNormalizedPreviewSearchText(contentEl);
5380
6747
  if (!blockText) return;
5381
6748
  const matchIndex = blockText.indexOf(selectionText);
5382
6749
  if (matchIndex < 0) return;
@@ -5397,9 +6764,25 @@
5397
6764
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5398
6765
  const range = resolveReviewNoteRange(note, source);
5399
6766
  if (!range) return false;
5400
- const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
6767
+ const rangeBlock = findPreviewCommentBlockForRange(targetEl, range);
6768
+ const selectionText = getPreviewNoteNormalizedSelectionText(note);
6769
+ let blockEl = rangeBlock;
6770
+ if (selectionText) {
6771
+ const rangeContentEl = rangeBlock ? (rangeBlock.querySelector(".preview-comment-block-content") || rangeBlock) : null;
6772
+ const rangeText = rangeContentEl ? buildNormalizedPreviewSearchText(rangeContentEl) : "";
6773
+ if (!rangeText || !rangeText.includes(selectionText)) {
6774
+ blockEl = findPreviewCommentBlockForNoteText(targetEl, note) || rangeBlock;
6775
+ }
6776
+ }
5401
6777
  if (!blockEl) return false;
5402
6778
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
6779
+ if (String(blockEl.dataset && blockEl.dataset.previewCommentKind || "") === "math") {
6780
+ if (typeof contentEl.scrollIntoView === "function") {
6781
+ contentEl.scrollIntoView({ block: "center", inline: "nearest" });
6782
+ }
6783
+ setPreviewJumpHighlight(targetEl, contentEl, null);
6784
+ return true;
6785
+ }
5403
6786
  const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5404
6787
  if (typeof blockEl.scrollIntoView === "function") {
5405
6788
  blockEl.scrollIntoView({ block: "center", inline: "nearest" });
@@ -5760,12 +7143,17 @@
5760
7143
  });
5761
7144
  }
5762
7145
 
5763
- function addReviewNoteFromPreviewSelection(blockEl) {
7146
+ function getActivePreviewSelectionAnchorForBlock(blockEl) {
5764
7147
  if (!blockEl) return null;
5765
7148
  const blockKey = getPreviewCommentBlockKey(blockEl);
5766
- const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
7149
+ return activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
5767
7150
  ? activePreviewCommentSelection
5768
7151
  : null;
7152
+ }
7153
+
7154
+ function addReviewNoteFromPreviewSelection(blockEl) {
7155
+ if (!blockEl) return null;
7156
+ const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
5769
7157
  if (!anchor) {
5770
7158
  setStatus("Select some preview text within a single block first.", "warning");
5771
7159
  return null;
@@ -5840,14 +7228,13 @@
5840
7228
  });
5841
7229
  }
5842
7230
 
5843
- function jumpToReviewNote(noteId) {
5844
- const note = reviewNotes.find((entry) => entry && entry.id === noteId);
5845
- if (!note) return;
7231
+ function jumpToReviewAnchor(anchor, options) {
7232
+ if (!anchor) return false;
5846
7233
  const current = String(sourceTextEl.value || "");
5847
- const range = resolveReviewNoteRange(note, current);
7234
+ const range = resolveReviewNoteRange(anchor, current);
5848
7235
  if (!range) {
5849
- setStatus("Could not find the anchored location for this comment.", "warning");
5850
- return;
7236
+ setStatus((options && options.notFoundStatusMessage) || "Could not find the anchored location.", "warning");
7237
+ return false;
5851
7238
  }
5852
7239
  suppressEditorSelectionComment = true;
5853
7240
  suppressedEditorSelectionStart = range.start;
@@ -5862,9 +7249,47 @@
5862
7249
  : (cb) => window.setTimeout(cb, 16);
5863
7250
  schedule(() => {
5864
7251
  scrollEditorRangeIntoView(range);
5865
- revealReviewNoteInPreview(note);
7252
+ if (options && typeof options.afterJump === "function") {
7253
+ options.afterJump(range);
7254
+ }
5866
7255
  updateEditorSelectionCommentUi();
5867
7256
  });
7257
+ if (!options || options.status !== false) {
7258
+ setStatus((options && options.statusMessage) || "Jumped to anchored location in the editor.", "success");
7259
+ }
7260
+ return true;
7261
+ }
7262
+
7263
+ function jumpToPreviewSelection(blockEl) {
7264
+ if (!blockEl) return false;
7265
+ const anchor = getActivePreviewSelectionAnchorForBlock(blockEl);
7266
+ if (!anchor) {
7267
+ setStatus("Select some preview text within a single block first.", "warning");
7268
+ return false;
7269
+ }
7270
+ const jumped = jumpToReviewAnchor(anchor, {
7271
+ statusMessage: "Jumped to preview selection in the raw editor.",
7272
+ });
7273
+ if (jumped) {
7274
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
7275
+ if (selection && typeof selection.removeAllRanges === "function") {
7276
+ selection.removeAllRanges();
7277
+ }
7278
+ clearPreviewCommentSelection();
7279
+ }
7280
+ return jumped;
7281
+ }
7282
+
7283
+ function jumpToReviewNote(noteId) {
7284
+ const note = reviewNotes.find((entry) => entry && entry.id === noteId);
7285
+ if (!note) return;
7286
+ jumpToReviewAnchor(note, {
7287
+ status: false,
7288
+ notFoundStatusMessage: "Could not find the anchored location for this comment.",
7289
+ afterJump: () => {
7290
+ revealReviewNoteInPreview(note);
7291
+ },
7292
+ });
5868
7293
  }
5869
7294
 
5870
7295
  function deleteReviewNote(noteId) {
@@ -7797,14 +9222,14 @@
7797
9222
 
7798
9223
  function handlePreviewCommentActionMouseDown(event) {
7799
9224
  const target = event.target;
7800
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9225
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
7801
9226
  if (!actionBtn) return;
7802
9227
  event.preventDefault();
7803
9228
  }
7804
9229
 
7805
9230
  function handlePreviewCommentActionClick(event) {
7806
9231
  const target = event.target;
7807
- const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
9232
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
7808
9233
  if (!actionBtn) return;
7809
9234
  const blockEl = actionBtn.closest(".preview-comment-block");
7810
9235
  if (!blockEl) return;
@@ -7812,6 +9237,11 @@
7812
9237
  event.stopPropagation();
7813
9238
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7814
9239
  if (!mode || !mode.startsWith("selection")) return;
9240
+ const action = String(actionBtn.dataset && actionBtn.dataset.previewCommentAction ? actionBtn.dataset.previewCommentAction : "comment");
9241
+ if (action === "jump") {
9242
+ jumpToPreviewSelection(blockEl);
9243
+ return;
9244
+ }
7815
9245
  addReviewNoteFromPreviewSelection(blockEl);
7816
9246
  }
7817
9247