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.
- package/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/client/studio-annotation-helpers.js +66 -0
- package/client/studio-client.js +775 -51
- package/client/studio.css +18 -0
- package/index.ts +41 -112
- package/package.json +1 -1
- package/shared/studio-markdown-html-comments.js +111 -0
package/client/studio-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1538
|
-
.
|
|
1539
|
-
|
|
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(
|
|
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 (
|
|
2595
|
-
|
|
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 (
|
|
2664
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
5055
|
+
pendingWhitespaceStart = startRef;
|
|
4553
5056
|
}
|
|
4554
|
-
pendingWhitespaceEnd =
|
|
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(
|
|
4568
|
-
outEnds.push(
|
|
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.
|
|
4998
|
-
|
|
4999
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
5171
|
-
const selectedDisplayText =
|
|
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
|
|
8416
|
+
if (!mode || !mode.startsWith("selection")) return;
|
|
7693
8417
|
addReviewNoteFromPreviewSelection(blockEl);
|
|
7694
8418
|
}
|
|
7695
8419
|
|