pi-studio 0.5.24 → 0.5.26
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 +14 -0
- package/README.md +2 -0
- package/client/studio-client.js +170 -0
- package/client/studio.css +12 -2
- package/index.ts +120 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.26] — 2026-03-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added a file-based `/studio-pdf <path>` command that exports a local file to `<name>.studio.pdf` using the existing Studio PDF pipeline and opens the result in the default PDF viewer, without requiring the Studio browser UI.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Markdown preview/PDF rendering now also allows blockquotes without a preceding blank line, matching the earlier tolerant list parsing and preventing leading `>` quote lines from collapsing into plain paragraph text.
|
|
14
|
+
- Studio browser preview now keeps the existing MathML rendering for ordinary equations but falls back to MathJax for pandoc-unsupported math blocks, improving advanced LaTeX matrix/array preview cases without switching all preview math to MathJax.
|
|
15
|
+
|
|
16
|
+
## [0.5.25] — 2026-03-21
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Studio PDF exports now add more space below ruled section headings to keep bibliography entries clear of the `References` underline, and figure captions now use left-aligned ragged-right formatting for long multi-line captions, including reinjected PDF subfigure groups, without disturbing normal figure centering.
|
|
20
|
+
|
|
7
21
|
## [0.5.24] — 2026-03-20
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens
|
|
|
28
28
|
- saves `.annotated.md`
|
|
29
29
|
- Renders Markdown/LaTeX/code previews (math + Mermaid), theme-synced with pi
|
|
30
30
|
- Exports right-pane preview as PDF (pandoc + LaTeX)
|
|
31
|
+
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf`
|
|
31
32
|
- Shows model/session/context usage in the footer, plus a compact-context action
|
|
32
33
|
|
|
33
34
|
## Commands
|
|
@@ -41,6 +42,7 @@ Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens
|
|
|
41
42
|
| `/studio --status` | Show studio server status |
|
|
42
43
|
| `/studio --stop` | Stop studio server |
|
|
43
44
|
| `/studio --help` | Show help |
|
|
45
|
+
| `/studio-pdf <path>` | Export a local file to `<name>.studio.pdf` via the Studio PDF pipeline |
|
|
44
46
|
|
|
45
47
|
## Install
|
|
46
48
|
|
package/client/studio-client.js
CHANGED
|
@@ -223,6 +223,7 @@
|
|
|
223
223
|
const ANNOTATION_MARKER_REGEX = /\[an:\s*([^\]]+?)\]/gi;
|
|
224
224
|
const EMPTY_OVERLAY_LINE = "\u200b";
|
|
225
225
|
const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
226
|
+
const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
|
|
226
227
|
const BOOT = (typeof window.__PI_STUDIO_BOOT__ === "object" && window.__PI_STUDIO_BOOT__)
|
|
227
228
|
? window.__PI_STUDIO_BOOT__
|
|
228
229
|
: {};
|
|
@@ -231,8 +232,11 @@
|
|
|
231
232
|
: {};
|
|
232
233
|
const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
|
|
233
234
|
const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
|
|
235
|
+
const MATHJAX_UNAVAILABLE_MESSAGE = "Math fallback unavailable. Some unsupported equations may remain as raw TeX.";
|
|
236
|
+
const MATHJAX_RENDER_FAIL_MESSAGE = "Math fallback could not render some unsupported equations.";
|
|
234
237
|
let mermaidModulePromise = null;
|
|
235
238
|
let mermaidInitialized = false;
|
|
239
|
+
let mathJaxPromise = null;
|
|
236
240
|
|
|
237
241
|
const DEBUG_ENABLED = (() => {
|
|
238
242
|
try {
|
|
@@ -1137,6 +1141,171 @@
|
|
|
1137
1141
|
return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
|
|
1138
1142
|
}
|
|
1139
1143
|
|
|
1144
|
+
function appendMathFallbackNotice(targetEl, message) {
|
|
1145
|
+
if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
if (targetEl.querySelector(".preview-math-warning")) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const warningEl = document.createElement("div");
|
|
1154
|
+
warningEl.className = "preview-warning preview-math-warning";
|
|
1155
|
+
warningEl.textContent = String(message || MATHJAX_UNAVAILABLE_MESSAGE);
|
|
1156
|
+
targetEl.appendChild(warningEl);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function extractMathFallbackTex(text, displayMode) {
|
|
1160
|
+
const source = typeof text === "string" ? text.trim() : "";
|
|
1161
|
+
if (!source) return "";
|
|
1162
|
+
|
|
1163
|
+
if (displayMode) {
|
|
1164
|
+
if (source.startsWith("$$") && source.endsWith("$$") && source.length >= 4) {
|
|
1165
|
+
return source.slice(2, -2).replace(/^\s+|\s+$/g, "");
|
|
1166
|
+
}
|
|
1167
|
+
if (source.startsWith("\\[") && source.endsWith("\\]") && source.length >= 4) {
|
|
1168
|
+
return source.slice(2, -2).replace(/^\s+|\s+$/g, "");
|
|
1169
|
+
}
|
|
1170
|
+
return source;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (source.startsWith("\\(") && source.endsWith("\\)") && source.length >= 4) {
|
|
1174
|
+
return source.slice(2, -2).trim();
|
|
1175
|
+
}
|
|
1176
|
+
if (source.startsWith("$") && source.endsWith("$") && source.length >= 2) {
|
|
1177
|
+
return source.slice(1, -1).trim();
|
|
1178
|
+
}
|
|
1179
|
+
return source;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function collectMathFallbackTargets(targetEl) {
|
|
1183
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
|
|
1184
|
+
|
|
1185
|
+
const nodes = Array.from(targetEl.querySelectorAll(".math.display, .math.inline"));
|
|
1186
|
+
const targets = [];
|
|
1187
|
+
const seenTargets = new Set();
|
|
1188
|
+
|
|
1189
|
+
nodes.forEach((node) => {
|
|
1190
|
+
if (!node || !node.classList) return;
|
|
1191
|
+
const displayMode = node.classList.contains("display");
|
|
1192
|
+
const rawText = typeof node.textContent === "string" ? node.textContent : "";
|
|
1193
|
+
const tex = extractMathFallbackTex(rawText, displayMode);
|
|
1194
|
+
if (!tex) return;
|
|
1195
|
+
|
|
1196
|
+
let renderTarget = node;
|
|
1197
|
+
if (displayMode) {
|
|
1198
|
+
const parent = node.parentElement;
|
|
1199
|
+
const parentText = parent && typeof parent.textContent === "string" ? parent.textContent.trim() : "";
|
|
1200
|
+
if (parent && parent.tagName === "P" && parentText === rawText.trim()) {
|
|
1201
|
+
renderTarget = parent;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (seenTargets.has(renderTarget)) return;
|
|
1206
|
+
seenTargets.add(renderTarget);
|
|
1207
|
+
targets.push({ node, renderTarget, displayMode, tex });
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
return targets;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function ensureMathJax() {
|
|
1214
|
+
if (window.MathJax && typeof window.MathJax.typesetPromise === "function") {
|
|
1215
|
+
return Promise.resolve(window.MathJax);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (mathJaxPromise) {
|
|
1219
|
+
return mathJaxPromise;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
mathJaxPromise = new Promise((resolve, reject) => {
|
|
1223
|
+
const globalMathJax = (window.MathJax && typeof window.MathJax === "object") ? window.MathJax : {};
|
|
1224
|
+
const texConfig = (globalMathJax.tex && typeof globalMathJax.tex === "object") ? globalMathJax.tex : {};
|
|
1225
|
+
const loaderConfig = (globalMathJax.loader && typeof globalMathJax.loader === "object") ? globalMathJax.loader : {};
|
|
1226
|
+
const startupConfig = (globalMathJax.startup && typeof globalMathJax.startup === "object") ? globalMathJax.startup : {};
|
|
1227
|
+
const optionsConfig = (globalMathJax.options && typeof globalMathJax.options === "object") ? globalMathJax.options : {};
|
|
1228
|
+
const loaderEntries = Array.isArray(loaderConfig.load) ? loaderConfig.load.slice() : [];
|
|
1229
|
+
["[tex]/ams", "[tex]/noerrors", "[tex]/noundefined"].forEach((entry) => {
|
|
1230
|
+
if (loaderEntries.indexOf(entry) === -1) loaderEntries.push(entry);
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
window.MathJax = Object.assign({}, globalMathJax, {
|
|
1234
|
+
loader: Object.assign({}, loaderConfig, {
|
|
1235
|
+
load: loaderEntries,
|
|
1236
|
+
}),
|
|
1237
|
+
tex: Object.assign({}, texConfig, {
|
|
1238
|
+
inlineMath: [["\\(", "\\)"], ["$", "$"]],
|
|
1239
|
+
displayMath: [["\\[", "\\]"], ["$$", "$$"]],
|
|
1240
|
+
packages: Object.assign({}, texConfig.packages || {}, { "[+]": ["ams", "noerrors", "noundefined"] }),
|
|
1241
|
+
}),
|
|
1242
|
+
options: Object.assign({}, optionsConfig, {
|
|
1243
|
+
skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"],
|
|
1244
|
+
}),
|
|
1245
|
+
startup: Object.assign({}, startupConfig, {
|
|
1246
|
+
typeset: false,
|
|
1247
|
+
}),
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
const script = document.createElement("script");
|
|
1251
|
+
script.src = MATHJAX_CDN_URL;
|
|
1252
|
+
script.async = true;
|
|
1253
|
+
script.dataset.piStudioMathjax = "1";
|
|
1254
|
+
script.onload = () => {
|
|
1255
|
+
const api = window.MathJax;
|
|
1256
|
+
if (api && api.startup && api.startup.promise && typeof api.startup.promise.then === "function") {
|
|
1257
|
+
api.startup.promise.then(() => resolve(api)).catch(reject);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (api && typeof api.typesetPromise === "function") {
|
|
1261
|
+
resolve(api);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
reject(new Error("MathJax did not initialize."));
|
|
1265
|
+
};
|
|
1266
|
+
script.onerror = () => {
|
|
1267
|
+
reject(new Error("Failed to load MathJax."));
|
|
1268
|
+
};
|
|
1269
|
+
document.head.appendChild(script);
|
|
1270
|
+
}).catch((error) => {
|
|
1271
|
+
mathJaxPromise = null;
|
|
1272
|
+
throw error;
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
return mathJaxPromise;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function renderMathFallbackInElement(targetEl) {
|
|
1279
|
+
const fallbackTargets = collectMathFallbackTargets(targetEl);
|
|
1280
|
+
if (fallbackTargets.length === 0) return;
|
|
1281
|
+
|
|
1282
|
+
fallbackTargets.forEach((entry) => {
|
|
1283
|
+
entry.renderTarget.classList.add("studio-mathjax-fallback");
|
|
1284
|
+
if (entry.displayMode) {
|
|
1285
|
+
entry.renderTarget.classList.add("studio-mathjax-fallback-display");
|
|
1286
|
+
entry.renderTarget.textContent = "\\[\n" + entry.tex + "\n\\]";
|
|
1287
|
+
} else {
|
|
1288
|
+
entry.renderTarget.textContent = "\\(" + entry.tex + "\\)";
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
let mathJax;
|
|
1293
|
+
try {
|
|
1294
|
+
mathJax = await ensureMathJax();
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
console.error("MathJax load failed:", error);
|
|
1297
|
+
appendMathFallbackNotice(targetEl, MATHJAX_UNAVAILABLE_MESSAGE);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
try {
|
|
1302
|
+
await mathJax.typesetPromise(fallbackTargets.map((entry) => entry.renderTarget));
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
console.error("MathJax fallback render failed:", error);
|
|
1305
|
+
appendMathFallbackNotice(targetEl, MATHJAX_RENDER_FAIL_MESSAGE);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1140
1309
|
function applyAnnotationMarkersToElement(targetEl, mode) {
|
|
1141
1310
|
if (!targetEl || mode === "none") return;
|
|
1142
1311
|
if (typeof document.createTreeWalker !== "function") return;
|
|
@@ -1596,6 +1765,7 @@
|
|
|
1596
1765
|
: "none";
|
|
1597
1766
|
applyAnnotationMarkersToElement(targetEl, annotationMode);
|
|
1598
1767
|
await renderMermaidInElement(targetEl);
|
|
1768
|
+
await renderMathFallbackInElement(targetEl);
|
|
1599
1769
|
|
|
1600
1770
|
// Warn if relative images are present but unlikely to resolve (non-file-backed content)
|
|
1601
1771
|
if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
|
package/client/studio.css
CHANGED
|
@@ -860,7 +860,8 @@
|
|
|
860
860
|
overflow-wrap: anywhere;
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
-
.rendered-markdown .studio-algorithm-line-content math
|
|
863
|
+
.rendered-markdown .studio-algorithm-line-content math,
|
|
864
|
+
.rendered-markdown .studio-algorithm-line-content mjx-container {
|
|
864
865
|
font-size: 1em;
|
|
865
866
|
}
|
|
866
867
|
|
|
@@ -868,6 +869,14 @@
|
|
|
868
869
|
font-family: "STIX Two Math", "Cambria Math", "Latin Modern Math", "STIXGeneral", serif;
|
|
869
870
|
}
|
|
870
871
|
|
|
872
|
+
.rendered-markdown mjx-container[display="true"] {
|
|
873
|
+
display: block;
|
|
874
|
+
margin: 1em 0;
|
|
875
|
+
text-align: center;
|
|
876
|
+
overflow-x: auto;
|
|
877
|
+
overflow-y: hidden;
|
|
878
|
+
}
|
|
879
|
+
|
|
871
880
|
.rendered-markdown .studio-display-equation {
|
|
872
881
|
position: relative;
|
|
873
882
|
margin: 1em 0;
|
|
@@ -879,7 +888,8 @@
|
|
|
879
888
|
overflow-y: hidden;
|
|
880
889
|
}
|
|
881
890
|
|
|
882
|
-
.rendered-markdown .studio-display-equation-body math[display="block"]
|
|
891
|
+
.rendered-markdown .studio-display-equation-body math[display="block"],
|
|
892
|
+
.rendered-markdown .studio-display-equation-body mjx-container[display="true"] {
|
|
883
893
|
margin: 0;
|
|
884
894
|
}
|
|
885
895
|
|
package/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
7
7
|
import { homedir, tmpdir } from "node:os";
|
|
8
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
9
|
import { URL, pathToFileURL } from "node:url";
|
|
10
10
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
11
11
|
|
|
@@ -175,11 +175,13 @@ const CMUX_STUDIO_STATUS_COLOR_DARK = "#5ea1ff";
|
|
|
175
175
|
const CMUX_STUDIO_STATUS_COLOR_LIGHT = "#0047ab";
|
|
176
176
|
|
|
177
177
|
const PDF_PREAMBLE = `\\usepackage{titlesec}
|
|
178
|
-
\\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{
|
|
178
|
+
\\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
|
|
179
179
|
\\titleformat{\\subsection}{\\large\\bfseries\\sffamily}{}{0pt}{}
|
|
180
180
|
\\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
|
|
181
181
|
\\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
|
|
182
182
|
\\titlespacing*{\\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.6ex plus 0.1ex}
|
|
183
|
+
\\usepackage{caption}
|
|
184
|
+
\\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
|
|
183
185
|
\\usepackage{enumitem}
|
|
184
186
|
\\setlist[itemize]{nosep, leftmargin=1.5em}
|
|
185
187
|
\\setlist[enumerate]{nosep, leftmargin=1.5em}
|
|
@@ -925,6 +927,23 @@ function readStudioFile(pathArg: string, cwd: string):
|
|
|
925
927
|
}
|
|
926
928
|
}
|
|
927
929
|
|
|
930
|
+
function inferStudioPdfLanguageFromPath(pathInput: string): string | undefined {
|
|
931
|
+
const extension = extname(pathInput).toLowerCase();
|
|
932
|
+
if (extension === ".tex" || extension === ".latex") return "latex";
|
|
933
|
+
if (extension === ".md" || extension === ".markdown" || extension === ".mdx") return "markdown";
|
|
934
|
+
if (extension === ".diff" || extension === ".patch") return "diff";
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function buildStudioPdfOutputPath(sourcePath: string): string {
|
|
939
|
+
const sourceDir = dirname(sourcePath);
|
|
940
|
+
const sourceName = basename(sourcePath);
|
|
941
|
+
const sourceExt = extname(sourceName);
|
|
942
|
+
const sourceStem = sourceExt ? sourceName.slice(0, -sourceExt.length) : sourceName;
|
|
943
|
+
const outputStem = sourceStem || sourceName || "studio-export";
|
|
944
|
+
return join(sourceDir, `${outputStem}.studio.pdf`);
|
|
945
|
+
}
|
|
946
|
+
|
|
928
947
|
function writeStudioFile(pathArg: string, cwd: string, content: string):
|
|
929
948
|
| { ok: true; label: string; resolvedPath: string }
|
|
930
949
|
| { ok: false; message: string } {
|
|
@@ -1827,7 +1846,7 @@ function buildStudioLatexInjectedPdfSubfigureBlock(
|
|
|
1827
1846
|
? (group.caption ? `\\textbf{${figureLabel}} ${group.caption}` : `\\textbf{${figureLabel}}`)
|
|
1828
1847
|
: (group.caption ? group.caption : "");
|
|
1829
1848
|
|
|
1830
|
-
|
|
1849
|
+
const minipageBlocks = group.items.map((item) => {
|
|
1831
1850
|
const widthSpec = item.widthSpec || "0.48\\textwidth";
|
|
1832
1851
|
const imageCommand = `\\includegraphics${item.imageOptions ? `[${item.imageOptions}]` : "[width=\\linewidth]"}{${item.imagePath}}`;
|
|
1833
1852
|
const subfigureLabel = formatStudioLatexSubfigureCaptionLabel(item.label, labels);
|
|
@@ -1838,7 +1857,7 @@ function buildStudioLatexInjectedPdfSubfigureBlock(
|
|
|
1838
1857
|
`\\begin{minipage}[t]{${widthSpec}}`,
|
|
1839
1858
|
"\\centering",
|
|
1840
1859
|
imageCommand,
|
|
1841
|
-
captionLine ? `\\par\\smallskip ${captionLine}` : "",
|
|
1860
|
+
captionLine ? `\\par\\smallskip{\\raggedright ${captionLine}\\par}` : "",
|
|
1842
1861
|
"\\end{minipage}",
|
|
1843
1862
|
].filter(Boolean);
|
|
1844
1863
|
return {
|
|
@@ -1866,7 +1885,7 @@ function buildStudioLatexInjectedPdfSubfigureBlock(
|
|
|
1866
1885
|
"\\begin{figure}[p]",
|
|
1867
1886
|
"\\centering",
|
|
1868
1887
|
rows.join("\n\\par\\medskip\n"),
|
|
1869
|
-
figureCaption ? `\\par\\bigskip ${figureCaption}` : "",
|
|
1888
|
+
figureCaption ? `\\par\\bigskip{\\raggedright ${figureCaption}\\par}` : "",
|
|
1870
1889
|
"\\end{figure}",
|
|
1871
1890
|
"\\clearpage",
|
|
1872
1891
|
].filter(Boolean);
|
|
@@ -1909,6 +1928,15 @@ function injectStudioLatexPdfSubfigureBlocks(
|
|
|
1909
1928
|
return transformed;
|
|
1910
1929
|
}
|
|
1911
1930
|
|
|
1931
|
+
function normalizeStudioGeneratedFigureCaptions(latex: string): string {
|
|
1932
|
+
return String(latex ?? "").replace(/\\begin\{figure\*?\}(?:\[[^\]]*\])?[\s\S]*?\\end\{figure\*?\}/g, (figureEnv) => {
|
|
1933
|
+
return String(figureEnv).replace(/\\caption(\[[^\]]*\])?\{/g, (_match, optionalArg) => {
|
|
1934
|
+
const suffix = typeof optionalArg === "string" ? optionalArg : "";
|
|
1935
|
+
return `\\captionsetup{justification=raggedright,singlelinecheck=false}\\caption${suffix}{\\raggedright `;
|
|
1936
|
+
});
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1912
1940
|
function formatStudioLatexMainAlgorithmCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
|
|
1913
1941
|
const normalizedLabel = String(label ?? "").trim();
|
|
1914
1942
|
if (!normalizedLabel) return null;
|
|
@@ -2774,7 +2802,7 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
|
|
|
2774
2802
|
const sourceWithResolvedRefs = isLatex
|
|
2775
2803
|
? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
|
|
2776
2804
|
: markdown;
|
|
2777
|
-
const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
2805
|
+
const inputFormat = isLatex ? "latex" : "markdown+lists_without_preceding_blankline-blank_before_blockquote+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris-raw_html";
|
|
2778
2806
|
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
2779
2807
|
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
|
|
2780
2808
|
if (resourcePath) {
|
|
@@ -3004,7 +3032,8 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
3004
3032
|
|
|
3005
3033
|
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
3006
3034
|
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
3007
|
-
|
|
3035
|
+
const normalizedLatex = normalizeStudioGeneratedFigureCaptions(injectedLatex);
|
|
3036
|
+
await writeFile(latexPath, normalizedLatex, "utf-8");
|
|
3008
3037
|
|
|
3009
3038
|
await new Promise<void>((resolve, reject) => {
|
|
3010
3039
|
const child = spawn(pdfEngine, [
|
|
@@ -3179,7 +3208,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
3179
3208
|
}
|
|
3180
3209
|
|
|
3181
3210
|
if (!isLatex && effectiveEditorLanguage === "diff") {
|
|
3182
|
-
const inputFormat = "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
3211
|
+
const inputFormat = "markdown+lists_without_preceding_blankline-blank_before_blockquote+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
3183
3212
|
const diffMarkdown = prepareStudioPdfMarkdown(markdown, false, effectiveEditorLanguage);
|
|
3184
3213
|
try {
|
|
3185
3214
|
return await runPandocPdfExport(inputFormat, diffMarkdown);
|
|
@@ -3195,7 +3224,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
3195
3224
|
|
|
3196
3225
|
const inputFormat = isLatex
|
|
3197
3226
|
? "latex"
|
|
3198
|
-
: "markdown+lists_without_preceding_blankline+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
|
|
3227
|
+
: "markdown+lists_without_preceding_blankline-blank_before_blockquote+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
|
|
3199
3228
|
const normalizedMarkdown = prepareStudioPdfMarkdown(sourceWithResolvedRefs, isLatex, effectiveEditorLanguage);
|
|
3200
3229
|
|
|
3201
3230
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
@@ -6238,7 +6267,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6238
6267
|
+ " /studio --last Open with last model response\n"
|
|
6239
6268
|
+ " /studio --status Show studio status\n"
|
|
6240
6269
|
+ " /studio --stop Stop studio server\n"
|
|
6241
|
-
+ " /studio-current <path> Load a file into currently open Studio tab(s)"
|
|
6270
|
+
+ " /studio-current <path> Load a file into currently open Studio tab(s)\n"
|
|
6271
|
+
+ " /studio-pdf <path> Export a file to <name>.studio.pdf via Studio PDF",
|
|
6242
6272
|
"info",
|
|
6243
6273
|
);
|
|
6244
6274
|
return;
|
|
@@ -6356,6 +6386,86 @@ export default function (pi: ExtensionAPI) {
|
|
|
6356
6386
|
},
|
|
6357
6387
|
});
|
|
6358
6388
|
|
|
6389
|
+
pi.registerCommand("studio-pdf", {
|
|
6390
|
+
description: "Export a file to PDF via the Studio PDF pipeline (/studio-pdf <file>)",
|
|
6391
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
6392
|
+
const trimmed = args.trim();
|
|
6393
|
+
if (!trimmed || trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
|
|
6394
|
+
ctx.ui.notify(
|
|
6395
|
+
"Usage: /studio-pdf <path>\n"
|
|
6396
|
+
+ " Export a local Markdown/LaTeX file to <name>.studio.pdf using the Studio PDF pipeline.",
|
|
6397
|
+
"info",
|
|
6398
|
+
);
|
|
6399
|
+
return;
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
if (trimmed.startsWith("-")) {
|
|
6403
|
+
ctx.ui.notify(`Unknown flag: ${trimmed}. Use /studio-pdf --help`, "error");
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
|
|
6407
|
+
const pathArg = parsePathArgument(trimmed);
|
|
6408
|
+
if (!pathArg) {
|
|
6409
|
+
ctx.ui.notify("Invalid file path argument.", "error");
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6413
|
+
const file = readStudioFile(pathArg, ctx.cwd);
|
|
6414
|
+
if (file.ok === false) {
|
|
6415
|
+
ctx.ui.notify(file.message, "error");
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
6418
|
+
|
|
6419
|
+
if (file.text.length > PDF_EXPORT_MAX_CHARS) {
|
|
6420
|
+
ctx.ui.notify(`PDF export text exceeds ${PDF_EXPORT_MAX_CHARS} characters.`, "error");
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
await ctx.waitForIdle();
|
|
6425
|
+
const pathPdfLanguage = inferStudioPdfLanguageFromPath(file.resolvedPath);
|
|
6426
|
+
const editorPdfLanguage = pathPdfLanguage ?? inferStudioPdfLanguage(file.text);
|
|
6427
|
+
const isLatex = editorPdfLanguage === "latex"
|
|
6428
|
+
|| (
|
|
6429
|
+
!pathPdfLanguage
|
|
6430
|
+
&& (editorPdfLanguage === undefined || editorPdfLanguage === "markdown")
|
|
6431
|
+
&& /\\documentclass\b|\\begin\{document\}/.test(file.text)
|
|
6432
|
+
);
|
|
6433
|
+
const resourcePath = resolveStudioBaseDir(file.resolvedPath, undefined, ctx.cwd);
|
|
6434
|
+
const outputPath = buildStudioPdfOutputPath(file.resolvedPath);
|
|
6435
|
+
|
|
6436
|
+
try {
|
|
6437
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(
|
|
6438
|
+
file.text,
|
|
6439
|
+
isLatex,
|
|
6440
|
+
resourcePath,
|
|
6441
|
+
editorPdfLanguage,
|
|
6442
|
+
file.resolvedPath,
|
|
6443
|
+
);
|
|
6444
|
+
await writeFile(outputPath, pdf);
|
|
6445
|
+
|
|
6446
|
+
let openError: string | null = null;
|
|
6447
|
+
try {
|
|
6448
|
+
await openPathInDefaultViewer(outputPath);
|
|
6449
|
+
} catch (error) {
|
|
6450
|
+
openError = error instanceof Error ? error.message : String(error);
|
|
6451
|
+
}
|
|
6452
|
+
|
|
6453
|
+
ctx.ui.notify(`Exported Studio PDF: ${outputPath}`, "info");
|
|
6454
|
+
if (warning) {
|
|
6455
|
+
ctx.ui.notify(warning, "warning");
|
|
6456
|
+
}
|
|
6457
|
+
if (openError) {
|
|
6458
|
+
ctx.ui.notify(`PDF was exported but could not be opened automatically: ${openError}`, "warning");
|
|
6459
|
+
}
|
|
6460
|
+
} catch (error) {
|
|
6461
|
+
ctx.ui.notify(
|
|
6462
|
+
`Studio PDF export failed for ${file.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
6463
|
+
"error",
|
|
6464
|
+
);
|
|
6465
|
+
}
|
|
6466
|
+
},
|
|
6467
|
+
});
|
|
6468
|
+
|
|
6359
6469
|
pi.registerCommand("studio-current", {
|
|
6360
6470
|
description: "Load a file into current open Studio tab(s) without opening a new browser session",
|
|
6361
6471
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|