pi-studio 0.5.56 → 0.5.58
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 +17 -0
- package/WORKFLOW.md +5 -3
- package/client/studio-client.js +234 -28
- package/client/studio.css +39 -1
- package/index.ts +156 -7
- package/package.json +1 -1
- package/shared/studio-markdown-latex-literals.js +86 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.58] — 2026-04-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Rendered Markdown/editor previews now add small copy controls to code/fenced-content blocks so block contents can be copied directly from the preview.
|
|
11
|
+
- The local comments rail now includes a **Delete all** action for clearing all comments attached to the current document or draft without removing inline `[an: ...]` annotations from the editor text.
|
|
12
|
+
|
|
13
|
+
## [0.5.57] — 2026-04-20
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- The inserted annotated-reply scaffold now renders more cleanly in Markdown preview, using `annotated reply: below` plus a short metadata list instead of relying on hard line-break spacing.
|
|
17
|
+
- Local review-note comment boxes now use normal multiline textarea behavior, so `Enter` inserts a newline while edits continue saving automatically as you type.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Markdown preview now preserves standalone LaTeX math definition lines such as `\newcommand`, `\def`, and `\DeclareMathOperator` so custom math macros/operators can work in Markdown documents instead of showing up as literal preview text.
|
|
21
|
+
- Markdown PDF export now moves standalone LaTeX math definition lines into the generated PDF preamble when needed, avoiding LaTeX errors like `Can be used only in preamble` for commands such as `\DeclareMathOperator`.
|
|
22
|
+
- Annotated-reply header detection now accepts both the older `annotated reply below:` form and the newer `annotated reply: below` form when removing/reapplying the scaffold.
|
|
23
|
+
|
|
7
24
|
## [0.5.56] — 2026-04-15
|
|
8
25
|
|
|
9
26
|
### Removed
|
package/WORKFLOW.md
CHANGED
|
@@ -20,9 +20,11 @@ Studio uses a **single workspace**:
|
|
|
20
20
|
Adds/updates an `annotated-reply` compatible scaffold in the editor:
|
|
21
21
|
|
|
22
22
|
```md
|
|
23
|
-
annotated reply below
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
annotated reply: below
|
|
24
|
+
|
|
25
|
+
- original source: <last model response | file <path> | studio editor>
|
|
26
|
+
- user annotation syntax: [an: your note]
|
|
27
|
+
- precedence: later messages supersede these annotations unless user explicitly references them
|
|
26
28
|
|
|
27
29
|
---
|
|
28
30
|
|
package/client/studio-client.js
CHANGED
|
@@ -133,6 +133,7 @@
|
|
|
133
133
|
const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
|
|
134
134
|
const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
|
|
135
135
|
const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
|
|
136
|
+
const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
|
|
136
137
|
const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
|
|
137
138
|
const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
|
|
138
139
|
|
|
@@ -459,16 +460,92 @@
|
|
|
459
460
|
return "working";
|
|
460
461
|
}
|
|
461
462
|
|
|
463
|
+
async function writeTextToClipboard(text) {
|
|
464
|
+
const content = String(text || "");
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
await fetchStudioJson("/clipboard", {
|
|
468
|
+
method: "POST",
|
|
469
|
+
body: JSON.stringify({ text: content }),
|
|
470
|
+
});
|
|
471
|
+
return true;
|
|
472
|
+
} catch {
|
|
473
|
+
// Fall back to browser clipboard APIs. The server-side clipboard path
|
|
474
|
+
// is most reliable for local Studio, but may be unavailable over SSH
|
|
475
|
+
// or on systems without a clipboard command.
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Prefer a copy-event payload first. It runs synchronously inside the
|
|
479
|
+
// user's click gesture and avoids browser quirks where copying a hidden
|
|
480
|
+
// textarea reports success but leaves the system clipboard unchanged.
|
|
481
|
+
if (document.execCommand && typeof document.addEventListener === "function") {
|
|
482
|
+
let handled = false;
|
|
483
|
+
const handleCopy = (event) => {
|
|
484
|
+
if (!event || !event.clipboardData) return;
|
|
485
|
+
event.clipboardData.setData("text/plain", content);
|
|
486
|
+
event.preventDefault();
|
|
487
|
+
handled = true;
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
document.addEventListener("copy", handleCopy, true);
|
|
491
|
+
const ok = document.execCommand("copy");
|
|
492
|
+
if (ok && handled) return true;
|
|
493
|
+
} catch {
|
|
494
|
+
// Fall through to the other clipboard paths.
|
|
495
|
+
} finally {
|
|
496
|
+
document.removeEventListener("copy", handleCopy, true);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
501
|
+
try {
|
|
502
|
+
await navigator.clipboard.writeText(content);
|
|
503
|
+
return true;
|
|
504
|
+
} catch {
|
|
505
|
+
// Fall through to the selection-based legacy path.
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const textarea = document.createElement("textarea");
|
|
510
|
+
textarea.value = content;
|
|
511
|
+
textarea.setAttribute("readonly", "");
|
|
512
|
+
textarea.style.position = "fixed";
|
|
513
|
+
textarea.style.top = "0";
|
|
514
|
+
textarea.style.left = "0";
|
|
515
|
+
textarea.style.width = "1px";
|
|
516
|
+
textarea.style.height = "1px";
|
|
517
|
+
textarea.style.opacity = "0";
|
|
518
|
+
document.body.appendChild(textarea);
|
|
519
|
+
const activeEl = document.activeElement;
|
|
520
|
+
textarea.focus();
|
|
521
|
+
textarea.select();
|
|
522
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
523
|
+
let ok = false;
|
|
524
|
+
try {
|
|
525
|
+
ok = document.execCommand && document.execCommand("copy");
|
|
526
|
+
} catch {
|
|
527
|
+
ok = false;
|
|
528
|
+
}
|
|
529
|
+
textarea.remove();
|
|
530
|
+
if (activeEl && typeof activeEl.focus === "function") {
|
|
531
|
+
try {
|
|
532
|
+
activeEl.focus();
|
|
533
|
+
} catch {
|
|
534
|
+
// Ignore focus restore failures.
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return Boolean(ok);
|
|
538
|
+
}
|
|
539
|
+
|
|
462
540
|
async function copyVisibleWorkingToClipboard() {
|
|
463
541
|
const content = buildVisibleWorkingText();
|
|
464
542
|
if (!content.trim()) {
|
|
465
543
|
setStatus("No visible working details to copy yet.", "warning");
|
|
466
544
|
return;
|
|
467
545
|
}
|
|
468
|
-
|
|
469
|
-
await navigator.clipboard.writeText(content);
|
|
546
|
+
if (await writeTextToClipboard(content)) {
|
|
470
547
|
setStatus("Copied visible working text.", "success");
|
|
471
|
-
}
|
|
548
|
+
} else {
|
|
472
549
|
setStatus("Clipboard write failed.", "warning");
|
|
473
550
|
}
|
|
474
551
|
}
|
|
@@ -2837,6 +2914,107 @@
|
|
|
2837
2914
|
}
|
|
2838
2915
|
}
|
|
2839
2916
|
|
|
2917
|
+
function normalizeCopyableBlockText(text) {
|
|
2918
|
+
return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
function getCopyablePreviewBlockText(blockEl) {
|
|
2922
|
+
if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
|
|
2923
|
+
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
2924
|
+
return normalizeCopyableBlockText(
|
|
2925
|
+
Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
|
|
2926
|
+
.map((lineEl) => lineEl && typeof lineEl.textContent === "string" ? lineEl.textContent : "")
|
|
2927
|
+
.join("\n"),
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
const codeEl = typeof blockEl.querySelector === "function"
|
|
2932
|
+
? blockEl.querySelector("pre code, code")
|
|
2933
|
+
: null;
|
|
2934
|
+
if (codeEl && typeof codeEl.textContent === "string") {
|
|
2935
|
+
return normalizeCopyableBlockText(codeEl.textContent);
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const clone = typeof blockEl.cloneNode === "function" ? blockEl.cloneNode(true) : null;
|
|
2939
|
+
if (clone && typeof clone.querySelectorAll === "function") {
|
|
2940
|
+
Array.from(clone.querySelectorAll(".studio-copy-block-btn")).forEach((buttonEl) => {
|
|
2941
|
+
if (buttonEl && buttonEl.parentNode) buttonEl.parentNode.removeChild(buttonEl);
|
|
2942
|
+
});
|
|
2943
|
+
return normalizeCopyableBlockText(clone.textContent || "");
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
return normalizeCopyableBlockText(blockEl.textContent || "");
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
async function handleCopyPreviewBlockButtonClick(event) {
|
|
2950
|
+
const target = event && event.target;
|
|
2951
|
+
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
2952
|
+
if (!copyBtn) return;
|
|
2953
|
+
event.preventDefault();
|
|
2954
|
+
event.stopPropagation();
|
|
2955
|
+
if (typeof event.stopImmediatePropagation === "function") {
|
|
2956
|
+
event.stopImmediatePropagation();
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const blockEl = copyBtn.closest(".studio-copyable-block");
|
|
2960
|
+
if (!blockEl) {
|
|
2961
|
+
setStatus("Could not find the block to copy.", "warning");
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
const text = getCopyablePreviewBlockText(blockEl);
|
|
2966
|
+
if (!text.trim()) {
|
|
2967
|
+
setStatus("Nothing to copy from this block.", "warning");
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
if (copyBtn.dataset && copyBtn.dataset.studioCopyBusy === "1") return;
|
|
2972
|
+
if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "1";
|
|
2973
|
+
const ok = await writeTextToClipboard(text);
|
|
2974
|
+
if (ok) {
|
|
2975
|
+
setStatus("Copied block to clipboard.", "success");
|
|
2976
|
+
} else {
|
|
2977
|
+
setStatus("Clipboard write failed.", "warning");
|
|
2978
|
+
}
|
|
2979
|
+
if (copyBtn.dataset) {
|
|
2980
|
+
window.setTimeout(() => {
|
|
2981
|
+
if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "0";
|
|
2982
|
+
}, 150);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
function decorateCopyablePreviewBlocks(targetEl) {
|
|
2987
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
2988
|
+
const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines"));
|
|
2989
|
+
blocks.forEach((blockEl) => {
|
|
2990
|
+
if (!blockEl || !(blockEl instanceof Element)) return;
|
|
2991
|
+
if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
|
|
2992
|
+
if (blockEl.matches && blockEl.matches("pre") && blockEl.closest("div.sourceCode")) return;
|
|
2993
|
+
if (blockEl.closest && blockEl.closest("button, .studio-copy-block-btn")) return;
|
|
2994
|
+
|
|
2995
|
+
const initialText = getCopyablePreviewBlockText(blockEl);
|
|
2996
|
+
if (!initialText.trim()) return;
|
|
2997
|
+
|
|
2998
|
+
blockEl.classList.add("studio-copyable-block");
|
|
2999
|
+
if (blockEl.dataset) blockEl.dataset.studioCopyDecorated = "1";
|
|
3000
|
+
|
|
3001
|
+
const copyBtn = document.createElement("button");
|
|
3002
|
+
copyBtn.type = "button";
|
|
3003
|
+
copyBtn.className = "studio-copy-block-btn";
|
|
3004
|
+
copyBtn.textContent = "Copy";
|
|
3005
|
+
copyBtn.title = "Copy this block to the clipboard.";
|
|
3006
|
+
copyBtn.setAttribute("aria-label", "Copy this block to the clipboard");
|
|
3007
|
+
copyBtn.addEventListener("pointerdown", (event) => {
|
|
3008
|
+
event.stopPropagation();
|
|
3009
|
+
});
|
|
3010
|
+
copyBtn.addEventListener("mousedown", (event) => {
|
|
3011
|
+
event.stopPropagation();
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
blockEl.appendChild(copyBtn);
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
|
|
2840
3018
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
2841
3019
|
const previewPrepared = annotationsEnabled
|
|
2842
3020
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -2879,6 +3057,7 @@
|
|
|
2879
3057
|
if (shouldDecoratePreviewComments) {
|
|
2880
3058
|
decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
|
|
2881
3059
|
}
|
|
3060
|
+
decorateCopyablePreviewBlocks(targetEl);
|
|
2882
3061
|
|
|
2883
3062
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
2884
3063
|
if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
|
|
@@ -4281,6 +4460,7 @@
|
|
|
4281
4460
|
targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
|
|
4282
4461
|
ensurePreviewSelectionActions(targetEl);
|
|
4283
4462
|
updatePreviewCommentBlocksForElement(targetEl);
|
|
4463
|
+
decorateCopyablePreviewBlocks(targetEl);
|
|
4284
4464
|
if (pane === "response") {
|
|
4285
4465
|
applyPendingResponseScrollReset();
|
|
4286
4466
|
scheduleResponsePaneRepaintNudge();
|
|
@@ -8066,6 +8246,12 @@
|
|
|
8066
8246
|
? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
|
|
8067
8247
|
: "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
|
|
8068
8248
|
}
|
|
8249
|
+
if (reviewNotesDeleteAllBtn) {
|
|
8250
|
+
reviewNotesDeleteAllBtn.disabled = uiBusy || !hasNotes;
|
|
8251
|
+
reviewNotesDeleteAllBtn.title = hasNotes
|
|
8252
|
+
? "Delete all local comments for this document or draft. Existing inline [an: ...] annotations in the editor text are left unchanged."
|
|
8253
|
+
: "No local comments to delete.";
|
|
8254
|
+
}
|
|
8069
8255
|
if (reviewNotesDoneBtn) {
|
|
8070
8256
|
reviewNotesDoneBtn.disabled = !isOpen;
|
|
8071
8257
|
}
|
|
@@ -8103,7 +8289,7 @@
|
|
|
8103
8289
|
const textarea = document.createElement("textarea");
|
|
8104
8290
|
textarea.value = String(note.text || "");
|
|
8105
8291
|
textarea.placeholder = "Write a local comment here…";
|
|
8106
|
-
textarea.title = "Write a local comment.
|
|
8292
|
+
textarea.title = "Write a local comment. Enter inserts a new line; changes save automatically as you type.";
|
|
8107
8293
|
card.appendChild(textarea);
|
|
8108
8294
|
|
|
8109
8295
|
const footer = document.createElement("div");
|
|
@@ -8169,22 +8355,6 @@
|
|
|
8169
8355
|
updateReviewNotesUi();
|
|
8170
8356
|
});
|
|
8171
8357
|
|
|
8172
|
-
textarea.addEventListener("keydown", (event) => {
|
|
8173
|
-
if (
|
|
8174
|
-
event.key === "Enter"
|
|
8175
|
-
&& !event.shiftKey
|
|
8176
|
-
&& !event.altKey
|
|
8177
|
-
&& !event.ctrlKey
|
|
8178
|
-
&& !event.metaKey
|
|
8179
|
-
) {
|
|
8180
|
-
event.preventDefault();
|
|
8181
|
-
textarea.blur();
|
|
8182
|
-
if (!convertBtn.disabled) {
|
|
8183
|
-
convertBtn.focus();
|
|
8184
|
-
}
|
|
8185
|
-
}
|
|
8186
|
-
});
|
|
8187
|
-
|
|
8188
8358
|
reviewNotesListEl.appendChild(card);
|
|
8189
8359
|
|
|
8190
8360
|
if (pendingReviewNoteInlineFocusId && pendingReviewNoteInlineFocusId === note.id && isReviewNotesOpen()) {
|
|
@@ -8433,6 +8603,21 @@
|
|
|
8433
8603
|
setStatus("Deleted local comment.", "success");
|
|
8434
8604
|
}
|
|
8435
8605
|
|
|
8606
|
+
function deleteAllReviewNotes() {
|
|
8607
|
+
if (!reviewNotes.length) {
|
|
8608
|
+
setStatus("No local comments to delete.", "warning");
|
|
8609
|
+
return;
|
|
8610
|
+
}
|
|
8611
|
+
const count = reviewNotes.length;
|
|
8612
|
+
const confirmed = window.confirm(
|
|
8613
|
+
"Delete all " + count + " local comment" + (count === 1 ? "" : "s") + " for this document?\n\n"
|
|
8614
|
+
+ "Existing inline [an: ...] annotations in the editor text will not be removed.",
|
|
8615
|
+
);
|
|
8616
|
+
if (!confirmed) return;
|
|
8617
|
+
setReviewNotes([]);
|
|
8618
|
+
setStatus("Deleted all local comments.", "success");
|
|
8619
|
+
}
|
|
8620
|
+
|
|
8436
8621
|
function convertReviewNoteToAnnotation(noteId) {
|
|
8437
8622
|
if (uiBusy) {
|
|
8438
8623
|
setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
|
|
@@ -9684,10 +9869,10 @@
|
|
|
9684
9869
|
|
|
9685
9870
|
function buildAnnotationHeader() {
|
|
9686
9871
|
const sourceDescriptor = describeSourceForAnnotation();
|
|
9687
|
-
let header = "annotated reply below
|
|
9688
|
-
header += "original source: " + sourceDescriptor + "\n";
|
|
9689
|
-
header += "user annotation syntax: [an: note]\n";
|
|
9690
|
-
header += "precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
|
|
9872
|
+
let header = "annotated reply: below\n\n";
|
|
9873
|
+
header += "- original source: " + sourceDescriptor + "\n";
|
|
9874
|
+
header += "- user annotation syntax: [an: note]\n";
|
|
9875
|
+
header += "- precedence: later messages supersede these annotations unless user explicitly references them\n\n---\n\n";
|
|
9691
9876
|
return header;
|
|
9692
9877
|
}
|
|
9693
9878
|
|
|
@@ -9697,7 +9882,8 @@
|
|
|
9697
9882
|
|
|
9698
9883
|
function stripAnnotationHeader(text) {
|
|
9699
9884
|
const normalized = String(text || "").replace(/\r\n/g, "\n");
|
|
9700
|
-
|
|
9885
|
+
const lower = normalized.toLowerCase();
|
|
9886
|
+
if (!lower.startsWith("annotated reply: below") && !lower.startsWith("annotated reply below:")) {
|
|
9701
9887
|
return { hadHeader: false, body: normalized };
|
|
9702
9888
|
}
|
|
9703
9889
|
|
|
@@ -10093,7 +10279,7 @@
|
|
|
10093
10279
|
}
|
|
10094
10280
|
|
|
10095
10281
|
try {
|
|
10096
|
-
await
|
|
10282
|
+
await writeTextToClipboard(content);
|
|
10097
10283
|
setStatus("Copied response text.", "success");
|
|
10098
10284
|
} catch (error) {
|
|
10099
10285
|
setStatus("Clipboard write failed.", "warning");
|
|
@@ -10317,7 +10503,7 @@
|
|
|
10317
10503
|
}
|
|
10318
10504
|
|
|
10319
10505
|
try {
|
|
10320
|
-
await
|
|
10506
|
+
await writeTextToClipboard(content);
|
|
10321
10507
|
setStatus("Copied editor text.", "success");
|
|
10322
10508
|
} catch (error) {
|
|
10323
10509
|
setStatus("Clipboard write failed.", "warning");
|
|
@@ -10401,6 +10587,12 @@
|
|
|
10401
10587
|
});
|
|
10402
10588
|
}
|
|
10403
10589
|
|
|
10590
|
+
if (reviewNotesDeleteAllBtn) {
|
|
10591
|
+
reviewNotesDeleteAllBtn.addEventListener("click", () => {
|
|
10592
|
+
deleteAllReviewNotes();
|
|
10593
|
+
});
|
|
10594
|
+
}
|
|
10595
|
+
|
|
10404
10596
|
if (reviewNoteGutterContentEl) {
|
|
10405
10597
|
reviewNoteGutterContentEl.addEventListener("click", (event) => {
|
|
10406
10598
|
const target = event.target;
|
|
@@ -10412,6 +10604,20 @@
|
|
|
10412
10604
|
});
|
|
10413
10605
|
}
|
|
10414
10606
|
|
|
10607
|
+
document.addEventListener("click", (event) => {
|
|
10608
|
+
const target = event.target;
|
|
10609
|
+
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
10610
|
+
if (!copyBtn) return;
|
|
10611
|
+
void handleCopyPreviewBlockButtonClick(event);
|
|
10612
|
+
}, true);
|
|
10613
|
+
|
|
10614
|
+
document.addEventListener("pointerup", (event) => {
|
|
10615
|
+
const target = event.target;
|
|
10616
|
+
const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
|
|
10617
|
+
if (!copyBtn) return;
|
|
10618
|
+
void handleCopyPreviewBlockButtonClick(event);
|
|
10619
|
+
}, true);
|
|
10620
|
+
|
|
10415
10621
|
function handlePreviewCommentActionMouseDown(event) {
|
|
10416
10622
|
const target = event.target;
|
|
10417
10623
|
const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
|
|
@@ -10498,7 +10704,7 @@
|
|
|
10498
10704
|
}
|
|
10499
10705
|
|
|
10500
10706
|
try {
|
|
10501
|
-
await
|
|
10707
|
+
await writeTextToClipboard(String(scratchpadText || ""));
|
|
10502
10708
|
setStatus("Copied scratchpad text.", "success");
|
|
10503
10709
|
} catch (error) {
|
|
10504
10710
|
setStatus("Clipboard write failed.", "warning");
|
package/client/studio.css
CHANGED
|
@@ -1133,6 +1133,43 @@
|
|
|
1133
1133
|
white-space: inherit;
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
|
+
.rendered-markdown .studio-copyable-block {
|
|
1137
|
+
position: relative;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.rendered-markdown .studio-copy-block-btn {
|
|
1141
|
+
position: absolute;
|
|
1142
|
+
top: 8px;
|
|
1143
|
+
right: 8px;
|
|
1144
|
+
z-index: 4;
|
|
1145
|
+
padding: 3px 8px;
|
|
1146
|
+
border-radius: 999px;
|
|
1147
|
+
border: 1px solid var(--border-muted);
|
|
1148
|
+
background: var(--panel);
|
|
1149
|
+
color: var(--muted);
|
|
1150
|
+
font-family: var(--font-ui);
|
|
1151
|
+
font-size: 11px;
|
|
1152
|
+
font-weight: 700;
|
|
1153
|
+
line-height: 1.4;
|
|
1154
|
+
opacity: 0.25;
|
|
1155
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
|
|
1156
|
+
cursor: pointer;
|
|
1157
|
+
pointer-events: auto;
|
|
1158
|
+
user-select: none;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.rendered-markdown .studio-copy-block-btn:active {
|
|
1162
|
+
transform: translateY(1px);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.rendered-markdown .studio-copyable-block:hover > .studio-copy-block-btn,
|
|
1166
|
+
.rendered-markdown .studio-copy-block-btn:focus-visible {
|
|
1167
|
+
opacity: 1;
|
|
1168
|
+
color: var(--text);
|
|
1169
|
+
border-color: var(--accent-soft-strong);
|
|
1170
|
+
background: var(--panel);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1136
1173
|
.rendered-markdown :not(pre) > code {
|
|
1137
1174
|
background: rgba(127, 127, 127, 0.13);
|
|
1138
1175
|
border: 1px solid var(--md-codeblock-border);
|
|
@@ -2226,7 +2263,8 @@
|
|
|
2226
2263
|
background: var(--panel-2);
|
|
2227
2264
|
}
|
|
2228
2265
|
|
|
2229
|
-
.review-note-delete-btn:not(:disabled)
|
|
2266
|
+
.review-note-delete-btn:not(:disabled),
|
|
2267
|
+
#reviewNotesDeleteAllBtn:not(:disabled) {
|
|
2230
2268
|
border-color: var(--warn-border);
|
|
2231
2269
|
color: var(--warn);
|
|
2232
2270
|
}
|
package/index.ts
CHANGED
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
transformStudioMarkdownOutsideFences,
|
|
21
21
|
} from "./shared/studio-annotation-scanner.js";
|
|
22
22
|
import { stripStudioMarkdownHtmlComments } from "./shared/studio-markdown-html-comments.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
extractStandaloneLatexDefinitionsFromMarkdown,
|
|
25
|
+
preserveLiteralLatexCommandsInMarkdown,
|
|
26
|
+
} from "./shared/studio-markdown-latex-literals.js";
|
|
24
27
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
25
28
|
|
|
26
29
|
type Lens = "writing" | "code";
|
|
@@ -477,7 +480,7 @@ function buildStudioPdfCalloutTitleSizeCommand(options?: StudioPdfRenderOptions)
|
|
|
477
480
|
return "\\footnotesize";
|
|
478
481
|
}
|
|
479
482
|
|
|
480
|
-
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
|
|
483
|
+
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble = ""): string {
|
|
481
484
|
const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
|
|
482
485
|
const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
|
|
483
486
|
const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
|
|
@@ -543,7 +546,7 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
|
|
|
543
546
|
\\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere,bgcolor=StudioCodeBlockBg,framesep=2mm}%
|
|
544
547
|
}
|
|
545
548
|
\\makeatother
|
|
546
|
-
`;
|
|
549
|
+
${extraPreamble ? `${extraPreamble.trim()}\n` : ""}`;
|
|
547
550
|
}
|
|
548
551
|
|
|
549
552
|
type StudioThemeMode = "dark" | "light";
|
|
@@ -4075,6 +4078,86 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
|
|
|
4075
4078
|
};
|
|
4076
4079
|
}
|
|
4077
4080
|
|
|
4081
|
+
interface StudioClipboardCommand {
|
|
4082
|
+
command: string;
|
|
4083
|
+
args: string[];
|
|
4084
|
+
label: string;
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function getStudioClipboardCommands(): StudioClipboardCommand[] {
|
|
4088
|
+
if (process.platform === "darwin") {
|
|
4089
|
+
return [{ command: "pbcopy", args: [], label: "pbcopy" }];
|
|
4090
|
+
}
|
|
4091
|
+
if (process.platform === "win32") {
|
|
4092
|
+
return [{ command: "cmd.exe", args: ["/c", "clip"], label: "clip" }];
|
|
4093
|
+
}
|
|
4094
|
+
return [
|
|
4095
|
+
{ command: "wl-copy", args: [], label: "wl-copy" },
|
|
4096
|
+
{ command: "xclip", args: ["-selection", "clipboard"], label: "xclip" },
|
|
4097
|
+
{ command: "xsel", args: ["--clipboard", "--input"], label: "xsel" },
|
|
4098
|
+
];
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
function writeStudioClipboardWithCommand(spec: StudioClipboardCommand, text: string): Promise<void> {
|
|
4102
|
+
return new Promise((resolve, reject) => {
|
|
4103
|
+
const child = spawn(spec.command, spec.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
4104
|
+
const stderrChunks: Buffer[] = [];
|
|
4105
|
+
let settled = false;
|
|
4106
|
+
const timer = setTimeout(() => {
|
|
4107
|
+
if (settled) return;
|
|
4108
|
+
settled = true;
|
|
4109
|
+
try {
|
|
4110
|
+
child.kill();
|
|
4111
|
+
} catch {
|
|
4112
|
+
// Ignore kill failures.
|
|
4113
|
+
}
|
|
4114
|
+
reject(new Error(`${spec.label} timed out.`));
|
|
4115
|
+
}, 3000);
|
|
4116
|
+
|
|
4117
|
+
const fail = (error: Error) => {
|
|
4118
|
+
if (settled) return;
|
|
4119
|
+
settled = true;
|
|
4120
|
+
clearTimeout(timer);
|
|
4121
|
+
reject(error);
|
|
4122
|
+
};
|
|
4123
|
+
|
|
4124
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
4125
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
4126
|
+
});
|
|
4127
|
+
|
|
4128
|
+
child.once("error", (error) => {
|
|
4129
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
4130
|
+
});
|
|
4131
|
+
|
|
4132
|
+
child.once("close", (code) => {
|
|
4133
|
+
if (settled) return;
|
|
4134
|
+
settled = true;
|
|
4135
|
+
clearTimeout(timer);
|
|
4136
|
+
if (code === 0) {
|
|
4137
|
+
resolve();
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
4141
|
+
reject(new Error(`${spec.label} exited with code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
4142
|
+
});
|
|
4143
|
+
|
|
4144
|
+
child.stdin.end(text, "utf-8");
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
async function writeStudioSystemClipboard(text: string): Promise<{ ok: true; method: string } | { ok: false; error: string }> {
|
|
4149
|
+
const errors: string[] = [];
|
|
4150
|
+
for (const spec of getStudioClipboardCommands()) {
|
|
4151
|
+
try {
|
|
4152
|
+
await writeStudioClipboardWithCommand(spec, text);
|
|
4153
|
+
return { ok: true, method: spec.label };
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
errors.push(`${spec.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return { ok: false, error: errors.join("; ") || "No system clipboard command is available." };
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4078
4161
|
async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
|
|
4079
4162
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
4080
4163
|
const markdownWithoutHtmlComments = isLatex ? markdown : stripStudioMarkdownHtmlComments(markdown);
|
|
@@ -4478,6 +4561,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
4478
4561
|
calloutBlocks: StudioPdfMarkdownCalloutBlock[] = [],
|
|
4479
4562
|
alignedImageBlocks: StudioPdfAlignedImageBlock[] = [],
|
|
4480
4563
|
pdfOptions?: StudioPdfRenderOptions,
|
|
4564
|
+
extraPreamble = "",
|
|
4481
4565
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
4482
4566
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
4483
4567
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
@@ -4485,7 +4569,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
4485
4569
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
4486
4570
|
|
|
4487
4571
|
await mkdir(tempDir, { recursive: true });
|
|
4488
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
4572
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPreamble), "utf-8");
|
|
4489
4573
|
|
|
4490
4574
|
const pandocArgs = [
|
|
4491
4575
|
"-f", inputFormat,
|
|
@@ -4767,17 +4851,22 @@ async function renderStudioPdfWithPandoc(
|
|
|
4767
4851
|
? "latex"
|
|
4768
4852
|
: "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
|
|
4769
4853
|
const normalizedMarkdown = prepareStudioPdfMarkdown(pdfAlignedImageTransform.markdown, isLatex, effectiveEditorLanguage);
|
|
4854
|
+
const markdownPreambleSplit = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
|
|
4855
|
+
? extractStandaloneLatexDefinitionsFromMarkdown(normalizedMarkdown)
|
|
4856
|
+
: { body: normalizedMarkdown, definitions: [], preamble: "" };
|
|
4857
|
+
const normalizedMarkdownBody = markdownPreambleSplit.body;
|
|
4858
|
+
const extraPdfPreamble = markdownPreambleSplit.preamble;
|
|
4770
4859
|
|
|
4771
4860
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
4772
4861
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
4773
4862
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
4774
4863
|
|
|
4775
4864
|
await mkdir(tempDir, { recursive: true });
|
|
4776
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
4865
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPdfPreamble), "utf-8");
|
|
4777
4866
|
|
|
4778
4867
|
const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
|
|
4779
|
-
? { markdown:
|
|
4780
|
-
: await preprocessStudioMermaidForPdf(
|
|
4868
|
+
? { markdown: normalizedMarkdownBody, found: 0, replaced: 0, failed: 0, missingCli: false }
|
|
4869
|
+
: await preprocessStudioMermaidForPdf(normalizedMarkdownBody, tempDir);
|
|
4781
4870
|
const markdownForPdf = mermaidPrepared.markdown;
|
|
4782
4871
|
const hasDiffBlocks = !isLatex && hasStudioMarkdownDiffFence(markdownForPdf);
|
|
4783
4872
|
|
|
@@ -4795,6 +4884,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
4795
4884
|
pdfCalloutTransform.blocks,
|
|
4796
4885
|
pdfAlignedImageTransform.blocks,
|
|
4797
4886
|
pdfOptions,
|
|
4887
|
+
extraPdfPreamble,
|
|
4798
4888
|
);
|
|
4799
4889
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
4800
4890
|
return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
|
|
@@ -6172,6 +6262,7 @@ ${cssVarsBlock}
|
|
|
6172
6262
|
<div class="scratchpad-actions">
|
|
6173
6263
|
<button id="reviewNotesAddBtn" type="button" title="Create a new local comment on the current editor line.">Line comment</button>
|
|
6174
6264
|
<button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">All inline: Off</button>
|
|
6265
|
+
<button id="reviewNotesDeleteAllBtn" type="button" title="Delete all local comments for this document or draft.">Delete all</button>
|
|
6175
6266
|
<button id="reviewNotesDoneBtn" type="button" title="Hide the comments rail.">Hide</button>
|
|
6176
6267
|
</div>
|
|
6177
6268
|
</div>
|
|
@@ -7892,6 +7983,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
7892
7983
|
respondJson(res, 200, { ok: true });
|
|
7893
7984
|
};
|
|
7894
7985
|
|
|
7986
|
+
const handleClipboardRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
7987
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7988
|
+
if (method !== "POST") {
|
|
7989
|
+
res.setHeader("Allow", "POST");
|
|
7990
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
7991
|
+
return;
|
|
7992
|
+
}
|
|
7993
|
+
|
|
7994
|
+
let rawBody = "";
|
|
7995
|
+
try {
|
|
7996
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7997
|
+
} catch (error) {
|
|
7998
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7999
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
8000
|
+
respondJson(res, status, { ok: false, error: message });
|
|
8001
|
+
return;
|
|
8002
|
+
}
|
|
8003
|
+
|
|
8004
|
+
let parsedBody: unknown;
|
|
8005
|
+
try {
|
|
8006
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
8007
|
+
} catch {
|
|
8008
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
8009
|
+
return;
|
|
8010
|
+
}
|
|
8011
|
+
|
|
8012
|
+
const text =
|
|
8013
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { text?: unknown }).text === "string"
|
|
8014
|
+
? (parsedBody as { text: string }).text
|
|
8015
|
+
: null;
|
|
8016
|
+
if (text === null) {
|
|
8017
|
+
respondJson(res, 400, { ok: false, error: "Missing clipboard text in request body." });
|
|
8018
|
+
return;
|
|
8019
|
+
}
|
|
8020
|
+
|
|
8021
|
+
const result = await writeStudioSystemClipboard(text);
|
|
8022
|
+
if (result.ok) {
|
|
8023
|
+
respondJson(res, 200, { ok: true, method: result.method });
|
|
8024
|
+
return;
|
|
8025
|
+
}
|
|
8026
|
+
respondJson(res, 500, { ok: false, error: result.error });
|
|
8027
|
+
};
|
|
8028
|
+
|
|
7895
8029
|
const handleReviewNotesRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
7896
8030
|
const method = (req.method ?? "GET").toUpperCase();
|
|
7897
8031
|
if (method === "GET") {
|
|
@@ -8230,6 +8364,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
8230
8364
|
return;
|
|
8231
8365
|
}
|
|
8232
8366
|
|
|
8367
|
+
if (requestUrl.pathname === "/clipboard") {
|
|
8368
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8369
|
+
if (token !== serverState.token) {
|
|
8370
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
8371
|
+
return;
|
|
8372
|
+
}
|
|
8373
|
+
void handleClipboardRequest(req, res).catch((error) => {
|
|
8374
|
+
respondJson(res, 500, {
|
|
8375
|
+
ok: false,
|
|
8376
|
+
error: `Clipboard write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8377
|
+
});
|
|
8378
|
+
});
|
|
8379
|
+
return;
|
|
8380
|
+
}
|
|
8381
|
+
|
|
8233
8382
|
if (requestUrl.pathname === "/render-preview") {
|
|
8234
8383
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8235
8384
|
if (token !== serverState.token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.58",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,6 +51,18 @@ const STUDIO_LITERAL_MARKDOWN_LATEX_COMMANDS = new Set([
|
|
|
51
51
|
"tex",
|
|
52
52
|
]);
|
|
53
53
|
|
|
54
|
+
const STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS = new Set([
|
|
55
|
+
"newcommand",
|
|
56
|
+
"renewcommand",
|
|
57
|
+
"providecommand",
|
|
58
|
+
"declaremathoperator",
|
|
59
|
+
"def",
|
|
60
|
+
"gdef",
|
|
61
|
+
"edef",
|
|
62
|
+
"xdef",
|
|
63
|
+
"let",
|
|
64
|
+
]);
|
|
65
|
+
|
|
54
66
|
function isEscapedAt(text, index) {
|
|
55
67
|
let slashCount = 0;
|
|
56
68
|
for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
@@ -70,12 +82,40 @@ function findClosingUnescapedDelimiter(text, startIndex, delimiter) {
|
|
|
70
82
|
return -1;
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
function isStandaloneLatexDefinitionLine(line) {
|
|
86
|
+
const commandMatch = String(line || "").match(/^[ \t]{0,3}\\([A-Za-z@]+)\*?(?=\s|\\|\{|\[|$)/);
|
|
87
|
+
if (!commandMatch) return false;
|
|
88
|
+
|
|
89
|
+
const commandName = String(commandMatch[1] || "").toLowerCase();
|
|
90
|
+
return STUDIO_STANDALONE_MARKDOWN_LATEX_DEFINITION_COMMANDS.has(commandName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readStandaloneLatexDefinitionLine(text, startIndex) {
|
|
94
|
+
if (startIndex > 0 && text[startIndex - 1] !== "\n") return null;
|
|
95
|
+
|
|
96
|
+
const lineEndIndex = text.indexOf("\n", startIndex);
|
|
97
|
+
const sliceEnd = lineEndIndex >= 0 ? lineEndIndex : text.length;
|
|
98
|
+
const line = text.slice(startIndex, sliceEnd);
|
|
99
|
+
if (!isStandaloneLatexDefinitionLine(line)) return null;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
text: lineEndIndex >= 0 ? text.slice(startIndex, lineEndIndex + 1) : line,
|
|
103
|
+
nextIndex: lineEndIndex >= 0 ? lineEndIndex + 1 : text.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
function preserveLiteralLatexCommandsInMarkdownSegment(markdown) {
|
|
74
108
|
const source = String(markdown || "");
|
|
75
109
|
let out = "";
|
|
76
110
|
let index = 0;
|
|
77
111
|
|
|
78
112
|
while (index < source.length) {
|
|
113
|
+
const standaloneDefinitionLine = readStandaloneLatexDefinitionLine(source, index);
|
|
114
|
+
if (standaloneDefinitionLine) {
|
|
115
|
+
out += standaloneDefinitionLine.text;
|
|
116
|
+
index = standaloneDefinitionLine.nextIndex;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
79
119
|
if (source[index] === "`") {
|
|
80
120
|
let tickCount = 1;
|
|
81
121
|
while (source[index + tickCount] === "`") tickCount += 1;
|
|
@@ -201,3 +241,49 @@ export function preserveLiteralLatexCommandsInMarkdown(markdown) {
|
|
|
201
241
|
flushPlain();
|
|
202
242
|
return out.join("\n");
|
|
203
243
|
}
|
|
244
|
+
|
|
245
|
+
export function extractStandaloneLatexDefinitionsFromMarkdown(markdown) {
|
|
246
|
+
const lines = String(markdown || "").split("\n");
|
|
247
|
+
const bodyLines = [];
|
|
248
|
+
const definitions = [];
|
|
249
|
+
let inFence = false;
|
|
250
|
+
let fenceChar;
|
|
251
|
+
let fenceLength = 0;
|
|
252
|
+
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const trimmed = line.trimStart();
|
|
255
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
256
|
+
|
|
257
|
+
if (fenceMatch) {
|
|
258
|
+
const marker = fenceMatch[1];
|
|
259
|
+
const markerChar = marker[0];
|
|
260
|
+
const markerLength = marker.length;
|
|
261
|
+
|
|
262
|
+
if (!inFence) {
|
|
263
|
+
inFence = true;
|
|
264
|
+
fenceChar = markerChar;
|
|
265
|
+
fenceLength = markerLength;
|
|
266
|
+
} else if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
267
|
+
inFence = false;
|
|
268
|
+
fenceChar = undefined;
|
|
269
|
+
fenceLength = 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
bodyLines.push(line);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!inFence && isStandaloneLatexDefinitionLine(line)) {
|
|
277
|
+
definitions.push(line);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
bodyLines.push(line);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
body: bodyLines.join("\n"),
|
|
286
|
+
definitions,
|
|
287
|
+
preamble: definitions.join("\n"),
|
|
288
|
+
};
|
|
289
|
+
}
|