pi-studio 0.5.29 → 0.5.31
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 +13 -0
- package/client/studio-client.js +122 -20
- package/client/studio.css +13 -3
- package/index.ts +134 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.31] — 2026-03-24
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- The right-pane response view now nudges the browser to repaint after response renders complete, reducing cases where freshly rendered response content stayed visually blank until the user scrolled or interacted with the pane.
|
|
11
|
+
- Newly selected or newly arrived responses now reset the right-pane scroll position to the top by default, while **Editor (Preview)** continues to preserve scroll position so in-place edit/preview workflows still feel natural.
|
|
12
|
+
|
|
13
|
+
## [0.5.30] — 2026-03-24
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- LaTeX preview now preserves structured display-math environments such as `bmatrix` inside `\[ ... \]` instead of flattening their rows during Markdown math normalization, and preview display equations now center more robustly across browser engines.
|
|
17
|
+
- Studio now highlights custom `[an: ...]` markers in LaTeX editor syntax-highlighting mode, and PDF export renders those markers as styled annotation badges for both Markdown and LaTeX documents instead of leaving the raw bracket syntax in the final PDF.
|
|
18
|
+
- Right-pane response PDF export now also respects the current annotation-visibility mode, so hidden annotations do not leak into exported PDFs as raw `[an: ...]` text.
|
|
19
|
+
|
|
7
20
|
## [0.5.29] — 2026-03-21
|
|
8
21
|
|
|
9
22
|
### Changed
|
package/client/studio-client.js
CHANGED
|
@@ -234,6 +234,7 @@
|
|
|
234
234
|
let sourcePreviewRenderNonce = 0;
|
|
235
235
|
let responsePreviewRenderNonce = 0;
|
|
236
236
|
let responseEditorPreviewTimer = null;
|
|
237
|
+
let pendingResponseScrollReset = false;
|
|
237
238
|
let editorMetaUpdateRaf = null;
|
|
238
239
|
let editorHighlightEnabled = false;
|
|
239
240
|
let editorLanguage = "markdown";
|
|
@@ -971,6 +972,7 @@
|
|
|
971
972
|
}
|
|
972
973
|
|
|
973
974
|
function clearActiveResponseView() {
|
|
975
|
+
pendingResponseScrollReset = false;
|
|
974
976
|
latestResponseMarkdown = "";
|
|
975
977
|
latestResponseThinking = "";
|
|
976
978
|
latestResponseKind = "annotation";
|
|
@@ -1016,13 +1018,13 @@
|
|
|
1016
1018
|
}
|
|
1017
1019
|
}
|
|
1018
1020
|
|
|
1019
|
-
function applySelectedHistoryItem() {
|
|
1021
|
+
function applySelectedHistoryItem(options) {
|
|
1020
1022
|
const item = getSelectedHistoryItem();
|
|
1021
1023
|
if (!item) {
|
|
1022
1024
|
clearActiveResponseView();
|
|
1023
1025
|
return false;
|
|
1024
1026
|
}
|
|
1025
|
-
handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
|
|
1027
|
+
handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking, options);
|
|
1026
1028
|
return true;
|
|
1027
1029
|
}
|
|
1028
1030
|
|
|
@@ -1035,9 +1037,13 @@
|
|
|
1035
1037
|
return false;
|
|
1036
1038
|
}
|
|
1037
1039
|
|
|
1040
|
+
const previousItem = getSelectedHistoryItem();
|
|
1041
|
+
const previousId = previousItem && typeof previousItem.id === "string" ? previousItem.id : null;
|
|
1038
1042
|
const nextIndex = Math.max(0, Math.min(total - 1, Number(index) || 0));
|
|
1039
1043
|
responseHistoryIndex = nextIndex;
|
|
1040
|
-
const
|
|
1044
|
+
const nextItem = getSelectedHistoryItem();
|
|
1045
|
+
const nextId = nextItem && typeof nextItem.id === "string" ? nextItem.id : null;
|
|
1046
|
+
const applied = applySelectedHistoryItem({ resetScroll: previousId !== nextId });
|
|
1041
1047
|
updateHistoryControls();
|
|
1042
1048
|
|
|
1043
1049
|
if (applied && !(options && options.silent)) {
|
|
@@ -1539,6 +1545,33 @@
|
|
|
1539
1545
|
targetEl.classList.remove("preview-pending");
|
|
1540
1546
|
}
|
|
1541
1547
|
|
|
1548
|
+
function scheduleResponsePaneRepaintNudge() {
|
|
1549
|
+
if (!critiqueViewEl || typeof critiqueViewEl.getBoundingClientRect !== "function") return;
|
|
1550
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
1551
|
+
? window.requestAnimationFrame.bind(window)
|
|
1552
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
1553
|
+
|
|
1554
|
+
schedule(() => {
|
|
1555
|
+
if (!critiqueViewEl || !critiqueViewEl.isConnected) return;
|
|
1556
|
+
void critiqueViewEl.getBoundingClientRect();
|
|
1557
|
+
if (!critiqueViewEl.classList) return;
|
|
1558
|
+
critiqueViewEl.classList.add("response-repaint-nudge");
|
|
1559
|
+
schedule(() => {
|
|
1560
|
+
if (!critiqueViewEl || !critiqueViewEl.classList) return;
|
|
1561
|
+
critiqueViewEl.classList.remove("response-repaint-nudge");
|
|
1562
|
+
});
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function applyPendingResponseScrollReset() {
|
|
1567
|
+
if (!pendingResponseScrollReset || !critiqueViewEl) return false;
|
|
1568
|
+
if (rightView === "editor-preview") return false;
|
|
1569
|
+
critiqueViewEl.scrollTop = 0;
|
|
1570
|
+
critiqueViewEl.scrollLeft = 0;
|
|
1571
|
+
pendingResponseScrollReset = false;
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1542
1575
|
async function getMermaidApi() {
|
|
1543
1576
|
if (mermaidModulePromise) {
|
|
1544
1577
|
return mermaidModulePromise;
|
|
@@ -1715,7 +1748,9 @@
|
|
|
1715
1748
|
return;
|
|
1716
1749
|
}
|
|
1717
1750
|
|
|
1718
|
-
const markdown = rightView === "editor-preview"
|
|
1751
|
+
const markdown = rightView === "editor-preview"
|
|
1752
|
+
? prepareEditorTextForPdfExport(sourceTextEl.value)
|
|
1753
|
+
: prepareEditorTextForPreview(latestResponseMarkdown);
|
|
1719
1754
|
if (!markdown || !markdown.trim()) {
|
|
1720
1755
|
setStatus("Nothing to export yet.", "warning");
|
|
1721
1756
|
return;
|
|
@@ -1881,6 +1916,11 @@
|
|
|
1881
1916
|
appendPreviewNotice(targetEl, "Images not displaying? Set working dir in the editor pane or open via /studio <path>.");
|
|
1882
1917
|
}
|
|
1883
1918
|
}
|
|
1919
|
+
|
|
1920
|
+
if (pane === "response") {
|
|
1921
|
+
applyPendingResponseScrollReset();
|
|
1922
|
+
scheduleResponsePaneRepaintNudge();
|
|
1923
|
+
}
|
|
1884
1924
|
} catch (error) {
|
|
1885
1925
|
if (pane === "source") {
|
|
1886
1926
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -1891,6 +1931,10 @@
|
|
|
1891
1931
|
const detail = error && error.message ? error.message : String(error || "unknown error");
|
|
1892
1932
|
finishPreviewRender(targetEl);
|
|
1893
1933
|
targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
|
|
1934
|
+
if (pane === "response") {
|
|
1935
|
+
applyPendingResponseScrollReset();
|
|
1936
|
+
scheduleResponsePaneRepaintNudge();
|
|
1937
|
+
}
|
|
1894
1938
|
}
|
|
1895
1939
|
}
|
|
1896
1940
|
|
|
@@ -1960,11 +2004,13 @@
|
|
|
1960
2004
|
if (!editorText.trim()) {
|
|
1961
2005
|
finishPreviewRender(critiqueViewEl);
|
|
1962
2006
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>Editor is empty.</pre>";
|
|
2007
|
+
scheduleResponsePaneRepaintNudge();
|
|
1963
2008
|
return;
|
|
1964
2009
|
}
|
|
1965
2010
|
if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
|
|
1966
2011
|
finishPreviewRender(critiqueViewEl);
|
|
1967
2012
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
|
|
2013
|
+
scheduleResponsePaneRepaintNudge();
|
|
1968
2014
|
return;
|
|
1969
2015
|
}
|
|
1970
2016
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -1979,6 +2025,8 @@
|
|
|
1979
2025
|
critiqueViewEl.innerHTML = thinking && thinking.trim()
|
|
1980
2026
|
? buildPlainMarkdownHtml(thinking)
|
|
1981
2027
|
: "<pre class='plain-markdown'>No thinking available for this response.</pre>";
|
|
2028
|
+
applyPendingResponseScrollReset();
|
|
2029
|
+
scheduleResponsePaneRepaintNudge();
|
|
1982
2030
|
return;
|
|
1983
2031
|
}
|
|
1984
2032
|
|
|
@@ -1986,6 +2034,8 @@
|
|
|
1986
2034
|
if (!markdown || !markdown.trim()) {
|
|
1987
2035
|
finishPreviewRender(critiqueViewEl);
|
|
1988
2036
|
critiqueViewEl.innerHTML = "<pre class='plain-markdown'>No response yet. Run editor text or critique editor text.</pre>";
|
|
2037
|
+
applyPendingResponseScrollReset();
|
|
2038
|
+
scheduleResponsePaneRepaintNudge();
|
|
1989
2039
|
return;
|
|
1990
2040
|
}
|
|
1991
2041
|
|
|
@@ -2003,16 +2053,22 @@
|
|
|
2003
2053
|
"Response is too large for markdown highlighting. Showing plain markdown.",
|
|
2004
2054
|
markdown,
|
|
2005
2055
|
);
|
|
2056
|
+
applyPendingResponseScrollReset();
|
|
2057
|
+
scheduleResponsePaneRepaintNudge();
|
|
2006
2058
|
return;
|
|
2007
2059
|
}
|
|
2008
2060
|
|
|
2009
2061
|
finishPreviewRender(critiqueViewEl);
|
|
2010
2062
|
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
|
|
2063
|
+
applyPendingResponseScrollReset();
|
|
2064
|
+
scheduleResponsePaneRepaintNudge();
|
|
2011
2065
|
return;
|
|
2012
2066
|
}
|
|
2013
2067
|
|
|
2014
2068
|
finishPreviewRender(critiqueViewEl);
|
|
2015
2069
|
critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
|
|
2070
|
+
applyPendingResponseScrollReset();
|
|
2071
|
+
scheduleResponsePaneRepaintNudge();
|
|
2016
2072
|
}
|
|
2017
2073
|
|
|
2018
2074
|
function updateResultActionButtons(normalizedEditorText) {
|
|
@@ -2558,17 +2614,49 @@
|
|
|
2558
2614
|
}
|
|
2559
2615
|
|
|
2560
2616
|
if (lang === "latex") {
|
|
2561
|
-
const texPattern = /(%.*$)|(\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\b)|(\\[A-Za-z]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2617
|
+
const texPattern = /(%.*$)|(\[an:\s*[^\]]+\])|(\\(?:documentclass|usepackage|newtheorem|begin|end|section|subsection|subsubsection|chapter|part|title|author|date|maketitle|tableofcontents|includegraphics|caption|label|ref|eqref|cite|textbf|textit|texttt|emph|footnote|centering|newcommand|renewcommand|providecommand|bibliography|bibliographystyle|bibitem|item|input|include)\b)|(\\[A-Za-z]+)|(\{|\})|(\$\$?(?:[^$\\]|\\.)+\$\$?)|(\[(?:.*?)\])/gi;
|
|
2618
|
+
let out = "";
|
|
2619
|
+
let lastIndex = 0;
|
|
2620
|
+
texPattern.lastIndex = 0;
|
|
2621
|
+
|
|
2622
|
+
let match;
|
|
2623
|
+
while ((match = texPattern.exec(source)) !== null) {
|
|
2624
|
+
const token = match[0] || "";
|
|
2625
|
+
const start = typeof match.index === "number" ? match.index : 0;
|
|
2626
|
+
|
|
2627
|
+
if (start > lastIndex) {
|
|
2628
|
+
out += escapeHtml(source.slice(lastIndex, start));
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
if (match[1]) {
|
|
2632
|
+
out += wrapHighlight("hl-code-com", token);
|
|
2633
|
+
} else if (match[2]) {
|
|
2634
|
+
out += highlightInlineAnnotations(token, renderMode);
|
|
2635
|
+
} else if (match[3]) {
|
|
2636
|
+
out += wrapHighlight("hl-code-kw", token);
|
|
2637
|
+
} else if (match[4]) {
|
|
2638
|
+
out += wrapHighlight("hl-code-fn", token);
|
|
2639
|
+
} else if (match[5]) {
|
|
2640
|
+
out += wrapHighlight("hl-code-op", token);
|
|
2641
|
+
} else if (match[6]) {
|
|
2642
|
+
out += wrapHighlight("hl-code-str", token);
|
|
2643
|
+
} else if (match[7]) {
|
|
2644
|
+
out += wrapHighlight("hl-code-num", token);
|
|
2645
|
+
} else {
|
|
2646
|
+
out += escapeHtml(token);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
lastIndex = start + token.length;
|
|
2650
|
+
if (token.length === 0) {
|
|
2651
|
+
texPattern.lastIndex += 1;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
if (lastIndex < source.length) {
|
|
2656
|
+
out += escapeHtml(source.slice(lastIndex));
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
return out;
|
|
2572
2660
|
}
|
|
2573
2661
|
|
|
2574
2662
|
if (lang === "diff") {
|
|
@@ -3024,15 +3112,29 @@
|
|
|
3024
3112
|
return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
|
|
3025
3113
|
}
|
|
3026
3114
|
|
|
3027
|
-
function handleIncomingResponse(markdown, kind, timestamp, thinking) {
|
|
3115
|
+
function handleIncomingResponse(markdown, kind, timestamp, thinking, options) {
|
|
3028
3116
|
const responseTimestamp =
|
|
3029
3117
|
typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
|
|
3030
3118
|
? timestamp
|
|
3031
3119
|
: Date.now();
|
|
3120
|
+
const responseThinking = typeof thinking === "string" ? thinking : "";
|
|
3121
|
+
const responseKind = kind === "critique" ? "critique" : "annotation";
|
|
3122
|
+
const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
|
|
3123
|
+
? Boolean(options.resetScroll)
|
|
3124
|
+
: (
|
|
3125
|
+
latestResponseKind !== responseKind
|
|
3126
|
+
|| latestResponseTimestamp !== responseTimestamp
|
|
3127
|
+
|| latestResponseNormalized !== normalizeForCompare(markdown)
|
|
3128
|
+
|| latestResponseThinkingNormalized !== normalizeForCompare(responseThinking)
|
|
3129
|
+
);
|
|
3130
|
+
|
|
3131
|
+
if (resetScroll) {
|
|
3132
|
+
pendingResponseScrollReset = true;
|
|
3133
|
+
}
|
|
3032
3134
|
|
|
3033
3135
|
latestResponseMarkdown = markdown;
|
|
3034
|
-
latestResponseThinking =
|
|
3035
|
-
latestResponseKind =
|
|
3136
|
+
latestResponseThinking = responseThinking;
|
|
3137
|
+
latestResponseKind = responseKind;
|
|
3036
3138
|
latestResponseTimestamp = responseTimestamp;
|
|
3037
3139
|
latestResponseIsStructuredCritique = isStructuredCritique(markdown);
|
|
3038
3140
|
latestResponseHasContent = Boolean(markdown && markdown.trim());
|
|
@@ -3050,10 +3152,10 @@
|
|
|
3050
3152
|
refreshResponseUi();
|
|
3051
3153
|
}
|
|
3052
3154
|
|
|
3053
|
-
function applyLatestPayload(payload) {
|
|
3155
|
+
function applyLatestPayload(payload, options) {
|
|
3054
3156
|
if (!payload || typeof payload.markdown !== "string") return false;
|
|
3055
3157
|
const responseKind = payload.kind === "critique" ? "critique" : "annotation";
|
|
3056
|
-
handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
|
|
3158
|
+
handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
|
|
3057
3159
|
return true;
|
|
3058
3160
|
}
|
|
3059
3161
|
|
package/client/studio.css
CHANGED
|
@@ -874,7 +874,9 @@
|
|
|
874
874
|
|
|
875
875
|
.rendered-markdown mjx-container[display="true"] {
|
|
876
876
|
display: block;
|
|
877
|
-
|
|
877
|
+
width: fit-content;
|
|
878
|
+
max-width: 100%;
|
|
879
|
+
margin: 1em auto;
|
|
878
880
|
text-align: center;
|
|
879
881
|
overflow-x: auto;
|
|
880
882
|
overflow-y: hidden;
|
|
@@ -893,7 +895,7 @@
|
|
|
893
895
|
|
|
894
896
|
.rendered-markdown .studio-display-equation-body math[display="block"],
|
|
895
897
|
.rendered-markdown .studio-display-equation-body mjx-container[display="true"] {
|
|
896
|
-
margin: 0;
|
|
898
|
+
margin: 0 auto;
|
|
897
899
|
}
|
|
898
900
|
|
|
899
901
|
.rendered-markdown .studio-display-equation-number {
|
|
@@ -910,7 +912,9 @@
|
|
|
910
912
|
|
|
911
913
|
.rendered-markdown math[display="block"] {
|
|
912
914
|
display: block;
|
|
913
|
-
|
|
915
|
+
width: fit-content;
|
|
916
|
+
max-width: 100%;
|
|
917
|
+
margin: 1em auto;
|
|
914
918
|
text-align: center;
|
|
915
919
|
overflow-x: auto;
|
|
916
920
|
overflow-y: hidden;
|
|
@@ -964,6 +968,12 @@
|
|
|
964
968
|
opacity: 0.64;
|
|
965
969
|
}
|
|
966
970
|
|
|
971
|
+
.panel-scroll.response-repaint-nudge {
|
|
972
|
+
outline: 1px solid transparent;
|
|
973
|
+
-webkit-transform: translateZ(0);
|
|
974
|
+
transform: translateZ(0);
|
|
975
|
+
}
|
|
976
|
+
|
|
967
977
|
.preview-error {
|
|
968
978
|
color: var(--warn);
|
|
969
979
|
margin-bottom: 0.75em;
|
package/index.ts
CHANGED
|
@@ -205,6 +205,11 @@ const PDF_PREAMBLE = `\\usepackage{titlesec}
|
|
|
205
205
|
\\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
|
|
206
206
|
\\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
|
|
207
207
|
\\titlespacing*{\\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.6ex plus 0.1ex}
|
|
208
|
+
\\usepackage{xcolor}
|
|
209
|
+
\\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
|
|
210
|
+
\\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
|
|
211
|
+
\\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
|
|
212
|
+
\\newcommand{\\studioannotation}[1]{\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}}\\endgroup}
|
|
208
213
|
\\usepackage{caption}
|
|
209
214
|
\\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
|
|
210
215
|
\\usepackage{enumitem}
|
|
@@ -2466,6 +2471,9 @@ function isLikelyMathExpression(expr: string): boolean {
|
|
|
2466
2471
|
|
|
2467
2472
|
function collapseDisplayMathContent(expr: string): string {
|
|
2468
2473
|
let content = expr.trim();
|
|
2474
|
+
if (/\\begin\{[^}]+\}|\\end\{[^}]+\}/.test(content)) {
|
|
2475
|
+
return content;
|
|
2476
|
+
}
|
|
2469
2477
|
if (content.includes("\\\\") || content.includes("\n")) {
|
|
2470
2478
|
content = content.replace(/\\\\\s*/g, " ");
|
|
2471
2479
|
content = content.replace(/\s*\n\s*/g, " ");
|
|
@@ -2646,6 +2654,85 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
|
|
|
2646
2654
|
return undefined;
|
|
2647
2655
|
}
|
|
2648
2656
|
|
|
2657
|
+
function escapeStudioPdfLatexText(text: string): string {
|
|
2658
|
+
return String(text ?? "")
|
|
2659
|
+
.replace(/\r\n/g, "\n")
|
|
2660
|
+
.replace(/\s*\n\s*/g, " ")
|
|
2661
|
+
.trim()
|
|
2662
|
+
.replace(/\\/g, "\\textbackslash{}")
|
|
2663
|
+
.replace(/([{}%#$&_])/g, "\\$1")
|
|
2664
|
+
.replace(/~/g, "\\textasciitilde{}")
|
|
2665
|
+
.replace(/\^/g, "\\textasciicircum{}")
|
|
2666
|
+
.replace(/\s{2,}/g, " ");
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
|
|
2670
|
+
return String(text ?? "")
|
|
2671
|
+
.replace(/\[an:\s*([^\]]+?)\]/gi, (_match, markerText: string) => {
|
|
2672
|
+
const cleaned = escapeStudioPdfLatexText(markerText);
|
|
2673
|
+
if (!cleaned) return "";
|
|
2674
|
+
return `\\studioannotation{${cleaned}}`;
|
|
2675
|
+
})
|
|
2676
|
+
.replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
|
|
2677
|
+
const cleaned = escapeStudioPdfLatexText(markerText);
|
|
2678
|
+
if (!cleaned) return "";
|
|
2679
|
+
return `\\studioannotation{${cleaned}}`;
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
|
|
2684
|
+
const lines = String(markdown ?? "").split("\n");
|
|
2685
|
+
const out: string[] = [];
|
|
2686
|
+
let plainBuffer: string[] = [];
|
|
2687
|
+
let inFence = false;
|
|
2688
|
+
let fenceChar: "`" | "~" | undefined;
|
|
2689
|
+
let fenceLength = 0;
|
|
2690
|
+
|
|
2691
|
+
const flushPlain = () => {
|
|
2692
|
+
if (plainBuffer.length === 0) return;
|
|
2693
|
+
out.push(replaceStudioAnnotationMarkersForPdfInSegment(plainBuffer.join("\n")));
|
|
2694
|
+
plainBuffer = [];
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2697
|
+
for (const line of lines) {
|
|
2698
|
+
const trimmed = line.trimStart();
|
|
2699
|
+
const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
|
|
2700
|
+
|
|
2701
|
+
if (fenceMatch) {
|
|
2702
|
+
const marker = fenceMatch[1]!;
|
|
2703
|
+
const markerChar = marker[0] as "`" | "~";
|
|
2704
|
+
const markerLength = marker.length;
|
|
2705
|
+
|
|
2706
|
+
if (!inFence) {
|
|
2707
|
+
flushPlain();
|
|
2708
|
+
inFence = true;
|
|
2709
|
+
fenceChar = markerChar;
|
|
2710
|
+
fenceLength = markerLength;
|
|
2711
|
+
out.push(line);
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
2716
|
+
inFence = false;
|
|
2717
|
+
fenceChar = undefined;
|
|
2718
|
+
fenceLength = 0;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
out.push(line);
|
|
2722
|
+
continue;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
if (inFence) {
|
|
2726
|
+
out.push(line);
|
|
2727
|
+
} else {
|
|
2728
|
+
plainBuffer.push(line);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
flushPlain();
|
|
2733
|
+
return out.join("\n");
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2649
2736
|
function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLanguage?: string): string {
|
|
2650
2737
|
if (isLatex) return markdown;
|
|
2651
2738
|
const effectiveEditorLanguage = inferStudioPdfLanguage(markdown, editorLanguage);
|
|
@@ -2653,7 +2740,10 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
2653
2740
|
&& !isStudioSingleFencedCodeBlock(markdown)
|
|
2654
2741
|
? wrapStudioCodeAsMarkdown(markdown, effectiveEditorLanguage)
|
|
2655
2742
|
: markdown;
|
|
2656
|
-
|
|
2743
|
+
const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
|
|
2744
|
+
? replaceStudioAnnotationMarkersForPdf(source)
|
|
2745
|
+
: source;
|
|
2746
|
+
return normalizeObsidianImages(normalizeMathDelimiters(annotationReadySource));
|
|
2657
2747
|
}
|
|
2658
2748
|
|
|
2659
2749
|
function stripMathMlAnnotationTags(html: string): string {
|
|
@@ -2980,6 +3070,46 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
|
|
|
2980
3070
|
}
|
|
2981
3071
|
}
|
|
2982
3072
|
|
|
3073
|
+
function replaceStudioAnnotationMarkersInGeneratedLatex(latex: string): string {
|
|
3074
|
+
const lines = String(latex ?? "").split("\n");
|
|
3075
|
+
const out: string[] = [];
|
|
3076
|
+
const rawEnvStack: string[] = [];
|
|
3077
|
+
const rawEnvNames = new Set(["verbatim", "Verbatim", "Highlighting", "lstlisting"]);
|
|
3078
|
+
|
|
3079
|
+
const updateRawEnvStack = (line: string) => {
|
|
3080
|
+
const envPattern = /\\(begin|end)\{([^}]+)\}/g;
|
|
3081
|
+
let match: RegExpExecArray | null;
|
|
3082
|
+
while ((match = envPattern.exec(line)) !== null) {
|
|
3083
|
+
const kind = match[1];
|
|
3084
|
+
const envName = match[2];
|
|
3085
|
+
if (!envName || !rawEnvNames.has(envName)) continue;
|
|
3086
|
+
if (kind === "begin") {
|
|
3087
|
+
rawEnvStack.push(envName);
|
|
3088
|
+
} else {
|
|
3089
|
+
for (let i = rawEnvStack.length - 1; i >= 0; i -= 1) {
|
|
3090
|
+
if (rawEnvStack[i] === envName) {
|
|
3091
|
+
rawEnvStack.splice(i, 1);
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
for (const line of lines) {
|
|
3100
|
+
if (rawEnvStack.length > 0) {
|
|
3101
|
+
out.push(line);
|
|
3102
|
+
updateRawEnvStack(line);
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
out.push(replaceStudioAnnotationMarkersForPdfInSegment(line));
|
|
3107
|
+
updateRawEnvStack(line);
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
return out.join("\n");
|
|
3111
|
+
}
|
|
3112
|
+
|
|
2983
3113
|
async function renderStudioPdfFromGeneratedLatex(
|
|
2984
3114
|
markdown: string,
|
|
2985
3115
|
pandocCommand: string,
|
|
@@ -3057,7 +3187,8 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
3057
3187
|
|
|
3058
3188
|
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
3059
3189
|
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
3060
|
-
const
|
|
3190
|
+
const annotationReadyLatex = replaceStudioAnnotationMarkersInGeneratedLatex(injectedLatex);
|
|
3191
|
+
const normalizedLatex = normalizeStudioGeneratedFigureCaptions(annotationReadyLatex);
|
|
3061
3192
|
await writeFile(latexPath, normalizedLatex, "utf-8");
|
|
3062
3193
|
|
|
3063
3194
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -3219,7 +3350,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
3219
3350
|
}
|
|
3220
3351
|
};
|
|
3221
3352
|
|
|
3222
|
-
if (isLatex && latexSubfigurePdfTransform.groups.length > 0) {
|
|
3353
|
+
if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || /\[an:\s*[^\]]+\]/i.test(sourceWithResolvedRefs))) {
|
|
3223
3354
|
return await renderStudioPdfFromGeneratedLatex(
|
|
3224
3355
|
sourceWithResolvedRefs,
|
|
3225
3356
|
pandocCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.31",
|
|
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",
|