pi-studio 0.5.23 → 0.5.25
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 +10 -0
- package/index.ts +389 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.25] — 2026-03-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
## [0.5.24] — 2026-03-20
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- LaTeX PDF export now intercepts grouped `subfigure` blocks before Pandoc and reinjects them into the generated LaTeX as grouped minipage-based figure pages with aux-derived `Figure n` / `(a)` / `(b)` labels, preserving grouped subfigure layout more faithfully in exported PDFs.
|
|
16
|
+
|
|
7
17
|
## [0.5.23] — 2026-03-20
|
|
8
18
|
|
|
9
19
|
### Fixed
|
package/index.ts
CHANGED
|
@@ -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}
|
|
@@ -1123,6 +1125,25 @@ interface StudioLatexSubfigurePreviewTransformResult {
|
|
|
1123
1125
|
subfigureGroups: StudioLatexSubfigurePreviewGroup[];
|
|
1124
1126
|
}
|
|
1125
1127
|
|
|
1128
|
+
interface StudioLatexPdfSubfigureItem {
|
|
1129
|
+
imagePath: string;
|
|
1130
|
+
imageOptions: string | null;
|
|
1131
|
+
widthSpec: string | null;
|
|
1132
|
+
caption: string | null;
|
|
1133
|
+
label: string | null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
interface StudioLatexPdfSubfigureGroup {
|
|
1137
|
+
caption: string | null;
|
|
1138
|
+
label: string | null;
|
|
1139
|
+
items: StudioLatexPdfSubfigureItem[];
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
interface StudioLatexPdfSubfigureTransformResult {
|
|
1143
|
+
markdown: string;
|
|
1144
|
+
groups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1126
1147
|
interface StudioLatexAlgorithmPreviewLine {
|
|
1127
1148
|
indent: number;
|
|
1128
1149
|
content: string;
|
|
@@ -1197,6 +1218,17 @@ function readStudioLatexEnvironmentBlock(
|
|
|
1197
1218
|
return null;
|
|
1198
1219
|
}
|
|
1199
1220
|
|
|
1221
|
+
function extractStudioLatexFirstCommandArgument(input: string, commandName: string, allowStar = false): string | null {
|
|
1222
|
+
const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1223
|
+
const pattern = new RegExp(`\\\\${escapedCommand}${allowStar ? "\\*?" : ""}(?:\\s*\\[[^\\]]*\\])?\\s*\\{`, "g");
|
|
1224
|
+
const match = pattern.exec(input);
|
|
1225
|
+
if (!match) return null;
|
|
1226
|
+
const openBraceIndex = pattern.lastIndex - 1;
|
|
1227
|
+
const closeBraceIndex = findStudioLatexMatchingBrace(input, openBraceIndex);
|
|
1228
|
+
if (closeBraceIndex < 0) return null;
|
|
1229
|
+
return input.slice(openBraceIndex + 1, closeBraceIndex).trim() || null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1200
1232
|
function extractStudioLatexLastCommandArgument(input: string, commandName: string, allowStar = false): string | null {
|
|
1201
1233
|
const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1202
1234
|
const pattern = new RegExp(`\\\\${escapedCommand}${allowStar ? "\\*?" : ""}(?:\\s*\\[[^\\]]*\\])?\\s*\\{`, "g");
|
|
@@ -1233,10 +1265,98 @@ function convertStudioLatexLengthToCss(length: string): string | null {
|
|
|
1233
1265
|
return null;
|
|
1234
1266
|
}
|
|
1235
1267
|
|
|
1236
|
-
function
|
|
1268
|
+
function extractStudioLatexSubfigureWidthSpec(blockText: string): string | null {
|
|
1237
1269
|
const match = blockText.match(/^\\begin\s*\{subfigure\*?\}(?:\s*\[[^\]]*\])?\s*\{([^}]*)\}/);
|
|
1270
|
+
return match?.[1]?.trim() || null;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function extractStudioLatexSubfigureWidth(blockText: string): string | null {
|
|
1274
|
+
const widthSpec = extractStudioLatexSubfigureWidthSpec(blockText);
|
|
1275
|
+
if (!widthSpec) return null;
|
|
1276
|
+
return convertStudioLatexLengthToCss(widthSpec);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function extractStudioLatexIncludeGraphics(input: string): { path: string; options: string | null } | null {
|
|
1280
|
+
const pattern = /\\includegraphics\*?(?:\s*\[[^\]]*\])?\s*\{/g;
|
|
1281
|
+
const match = pattern.exec(input);
|
|
1238
1282
|
if (!match) return null;
|
|
1239
|
-
|
|
1283
|
+
const openBraceIndex = pattern.lastIndex - 1;
|
|
1284
|
+
const closeBraceIndex = findStudioLatexMatchingBrace(input, openBraceIndex);
|
|
1285
|
+
if (closeBraceIndex < 0) return null;
|
|
1286
|
+
const optionMatch = match[0].match(/\[([^\]]*)\]/);
|
|
1287
|
+
return {
|
|
1288
|
+
path: input.slice(openBraceIndex + 1, closeBraceIndex).trim(),
|
|
1289
|
+
options: optionMatch?.[1]?.trim() || null,
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function collectStudioLatexPdfSubfigureGroups(markdown: string): Array<{ start: number; end: number; group: StudioLatexPdfSubfigureGroup }> {
|
|
1294
|
+
const groups: Array<{ start: number; end: number; group: StudioLatexPdfSubfigureGroup }> = [];
|
|
1295
|
+
const figurePattern = /\\begin\s*\{(figure\*?)\}/g;
|
|
1296
|
+
|
|
1297
|
+
for (;;) {
|
|
1298
|
+
const figureMatch = figurePattern.exec(markdown);
|
|
1299
|
+
if (!figureMatch) break;
|
|
1300
|
+
const envName = figureMatch[1] ?? "figure";
|
|
1301
|
+
const block = readStudioLatexEnvironmentBlock(markdown, figureMatch.index, envName);
|
|
1302
|
+
if (!block) continue;
|
|
1303
|
+
const inner = block.innerText;
|
|
1304
|
+
const subfigurePattern = /\\begin\s*\{(subfigure\*?)\}/g;
|
|
1305
|
+
const subfigureBlocks: Array<{ start: number; end: number; fullText: string }> = [];
|
|
1306
|
+
for (;;) {
|
|
1307
|
+
const subfigureMatch = subfigurePattern.exec(inner);
|
|
1308
|
+
if (!subfigureMatch) break;
|
|
1309
|
+
const subfigureEnvName = subfigureMatch[1] ?? "subfigure";
|
|
1310
|
+
const subfigureBlock = readStudioLatexEnvironmentBlock(inner, subfigureMatch.index, subfigureEnvName);
|
|
1311
|
+
if (!subfigureBlock) continue;
|
|
1312
|
+
subfigureBlocks.push({
|
|
1313
|
+
start: subfigureMatch.index,
|
|
1314
|
+
end: subfigureBlock.endIndex,
|
|
1315
|
+
fullText: subfigureBlock.fullText.trim(),
|
|
1316
|
+
});
|
|
1317
|
+
subfigurePattern.lastIndex = subfigureBlock.endIndex;
|
|
1318
|
+
}
|
|
1319
|
+
if (subfigureBlocks.length === 0) continue;
|
|
1320
|
+
|
|
1321
|
+
let outerResidual = "";
|
|
1322
|
+
let residualCursor = 0;
|
|
1323
|
+
for (const subfigureBlock of subfigureBlocks) {
|
|
1324
|
+
outerResidual += inner.slice(residualCursor, subfigureBlock.start);
|
|
1325
|
+
residualCursor = subfigureBlock.end;
|
|
1326
|
+
}
|
|
1327
|
+
outerResidual += inner.slice(residualCursor);
|
|
1328
|
+
|
|
1329
|
+
const items: StudioLatexPdfSubfigureItem[] = [];
|
|
1330
|
+
let allHaveImages = true;
|
|
1331
|
+
for (const subfigureBlock of subfigureBlocks) {
|
|
1332
|
+
const image = extractStudioLatexIncludeGraphics(subfigureBlock.fullText);
|
|
1333
|
+
if (!image?.path) {
|
|
1334
|
+
allHaveImages = false;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
items.push({
|
|
1338
|
+
imagePath: image.path,
|
|
1339
|
+
imageOptions: image.options,
|
|
1340
|
+
widthSpec: extractStudioLatexSubfigureWidthSpec(subfigureBlock.fullText),
|
|
1341
|
+
caption: extractStudioLatexFirstCommandArgument(subfigureBlock.fullText, "caption", true),
|
|
1342
|
+
label: extractStudioLatexLastCommandArgument(subfigureBlock.fullText, "label"),
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
if (!allHaveImages || items.length === 0) continue;
|
|
1346
|
+
|
|
1347
|
+
groups.push({
|
|
1348
|
+
start: figureMatch.index,
|
|
1349
|
+
end: block.endIndex,
|
|
1350
|
+
group: {
|
|
1351
|
+
caption: extractStudioLatexLastCommandArgument(outerResidual, "caption", true),
|
|
1352
|
+
label: extractStudioLatexLastCommandArgument(outerResidual, "label"),
|
|
1353
|
+
items,
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
figurePattern.lastIndex = block.endIndex;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return groups;
|
|
1240
1360
|
}
|
|
1241
1361
|
|
|
1242
1362
|
function preprocessStudioLatexSubfiguresForPreview(markdown: string): StudioLatexSubfigurePreviewTransformResult {
|
|
@@ -1691,6 +1811,115 @@ function formatStudioLatexMainFigureCaptionLabel(label: string | null, labels: M
|
|
|
1691
1811
|
return `Figure ${entry.number}`;
|
|
1692
1812
|
}
|
|
1693
1813
|
|
|
1814
|
+
function estimateStudioLatexRelativeWidth(widthSpec: string | null | undefined): number | null {
|
|
1815
|
+
const normalized = String(widthSpec ?? "").replace(/\s+/g, "");
|
|
1816
|
+
if (!normalized) return null;
|
|
1817
|
+
const fractionalMatch = normalized.match(/^([0-9]*\.?[0-9]+)\\(?:textwidth|linewidth|columnwidth|hsize)$/);
|
|
1818
|
+
if (!fractionalMatch) return null;
|
|
1819
|
+
const value = Number.parseFloat(fractionalMatch[1] ?? "");
|
|
1820
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function buildStudioLatexInjectedPdfSubfigureBlock(
|
|
1824
|
+
group: StudioLatexPdfSubfigureGroup,
|
|
1825
|
+
labels: Map<string, { number: string; kind: string }>,
|
|
1826
|
+
): string {
|
|
1827
|
+
const figureLabel = formatStudioLatexMainFigureCaptionLabel(group.label, labels);
|
|
1828
|
+
const figureCaption = figureLabel
|
|
1829
|
+
? (group.caption ? `\\textbf{${figureLabel}} ${group.caption}` : `\\textbf{${figureLabel}}`)
|
|
1830
|
+
: (group.caption ? group.caption : "");
|
|
1831
|
+
|
|
1832
|
+
const minipageBlocks = group.items.map((item) => {
|
|
1833
|
+
const widthSpec = item.widthSpec || "0.48\\textwidth";
|
|
1834
|
+
const imageCommand = `\\includegraphics${item.imageOptions ? `[${item.imageOptions}]` : "[width=\\linewidth]"}{${item.imagePath}}`;
|
|
1835
|
+
const subfigureLabel = formatStudioLatexSubfigureCaptionLabel(item.label, labels);
|
|
1836
|
+
const captionLine = subfigureLabel
|
|
1837
|
+
? (item.caption ? `\\textbf{${subfigureLabel}} ${item.caption}` : `\\textbf{${subfigureLabel}}`)
|
|
1838
|
+
: (item.caption ? item.caption : "");
|
|
1839
|
+
const parts = [
|
|
1840
|
+
`\\begin{minipage}[t]{${widthSpec}}`,
|
|
1841
|
+
"\\centering",
|
|
1842
|
+
imageCommand,
|
|
1843
|
+
captionLine ? `\\par\\smallskip{\\raggedright ${captionLine}\\par}` : "",
|
|
1844
|
+
"\\end{minipage}",
|
|
1845
|
+
].filter(Boolean);
|
|
1846
|
+
return {
|
|
1847
|
+
latex: parts.join("\n"),
|
|
1848
|
+
relativeWidth: estimateStudioLatexRelativeWidth(widthSpec) ?? 0.48,
|
|
1849
|
+
};
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
const rows: string[] = [];
|
|
1853
|
+
let currentRow: string[] = [];
|
|
1854
|
+
let currentWidth = 0;
|
|
1855
|
+
for (const block of minipageBlocks) {
|
|
1856
|
+
if (currentRow.length > 0 && currentWidth + block.relativeWidth > 1.02) {
|
|
1857
|
+
rows.push(currentRow.join("\n\\hfill\n"));
|
|
1858
|
+
currentRow = [];
|
|
1859
|
+
currentWidth = 0;
|
|
1860
|
+
}
|
|
1861
|
+
currentRow.push(block.latex);
|
|
1862
|
+
currentWidth += block.relativeWidth;
|
|
1863
|
+
}
|
|
1864
|
+
if (currentRow.length > 0) rows.push(currentRow.join("\n\\hfill\n"));
|
|
1865
|
+
|
|
1866
|
+
const bodyParts = [
|
|
1867
|
+
"\\clearpage",
|
|
1868
|
+
"\\begin{figure}[p]",
|
|
1869
|
+
"\\centering",
|
|
1870
|
+
rows.join("\n\\par\\medskip\n"),
|
|
1871
|
+
figureCaption ? `\\par\\bigskip{\\raggedright ${figureCaption}\\par}` : "",
|
|
1872
|
+
"\\end{figure}",
|
|
1873
|
+
"\\clearpage",
|
|
1874
|
+
].filter(Boolean);
|
|
1875
|
+
return `\n${bodyParts.join("\n")}\n`;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function preprocessStudioLatexSubfiguresForPdf(markdown: string): StudioLatexPdfSubfigureTransformResult {
|
|
1879
|
+
const groups = collectStudioLatexPdfSubfigureGroups(markdown);
|
|
1880
|
+
if (groups.length === 0) return { markdown, groups: [] };
|
|
1881
|
+
let transformed = "";
|
|
1882
|
+
let cursor = 0;
|
|
1883
|
+
const placeholderGroups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }> = [];
|
|
1884
|
+
|
|
1885
|
+
for (const [index, entry] of groups.entries()) {
|
|
1886
|
+
const placeholder = `PISTUDIOSUBFIGUREPDFPLACEHOLDER${index + 1}`;
|
|
1887
|
+
placeholderGroups.push({ placeholder, group: entry.group });
|
|
1888
|
+
transformed += markdown.slice(cursor, entry.start);
|
|
1889
|
+
transformed += `\n\n${placeholder}\n\n`;
|
|
1890
|
+
cursor = entry.end;
|
|
1891
|
+
}
|
|
1892
|
+
transformed += markdown.slice(cursor);
|
|
1893
|
+
return {
|
|
1894
|
+
markdown: transformed,
|
|
1895
|
+
groups: placeholderGroups,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function injectStudioLatexPdfSubfigureBlocks(
|
|
1900
|
+
latex: string,
|
|
1901
|
+
groups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>,
|
|
1902
|
+
sourcePath: string | undefined,
|
|
1903
|
+
baseDir: string | undefined,
|
|
1904
|
+
): string {
|
|
1905
|
+
if (groups.length === 0) return latex;
|
|
1906
|
+
const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
|
|
1907
|
+
let transformed = String(latex ?? "");
|
|
1908
|
+
for (const entry of groups) {
|
|
1909
|
+
transformed = transformed.replace(entry.placeholder, buildStudioLatexInjectedPdfSubfigureBlock(entry.group, labels));
|
|
1910
|
+
}
|
|
1911
|
+
return transformed;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function normalizeStudioGeneratedFigureCaptions(latex: string): string {
|
|
1915
|
+
return String(latex ?? "").replace(/\\begin\{figure\*?\}(?:\[[^\]]*\])?[\s\S]*?\\end\{figure\*?\}/g, (figureEnv) => {
|
|
1916
|
+
return String(figureEnv).replace(/\\caption(\[[^\]]*\])?\{/g, (_match, optionalArg) => {
|
|
1917
|
+
const suffix = typeof optionalArg === "string" ? optionalArg : "";
|
|
1918
|
+
return `\\captionsetup{justification=raggedright,singlelinecheck=false}\\caption${suffix}{\\raggedright `;
|
|
1919
|
+
});
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1694
1923
|
function formatStudioLatexMainAlgorithmCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
|
|
1695
1924
|
const normalizedLabel = String(label ?? "").trim();
|
|
1696
1925
|
if (!normalizedLabel) return null;
|
|
@@ -2709,6 +2938,142 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
|
|
|
2709
2938
|
}
|
|
2710
2939
|
}
|
|
2711
2940
|
|
|
2941
|
+
async function renderStudioPdfFromGeneratedLatex(
|
|
2942
|
+
markdown: string,
|
|
2943
|
+
pandocCommand: string,
|
|
2944
|
+
pdfEngine: string,
|
|
2945
|
+
resourcePath: string | undefined,
|
|
2946
|
+
pandocWorkingDir: string | undefined,
|
|
2947
|
+
bibliographyArgs: string[],
|
|
2948
|
+
sourcePath: string | undefined,
|
|
2949
|
+
subfigureGroups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>,
|
|
2950
|
+
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
2951
|
+
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
2952
|
+
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
2953
|
+
const latexPath = join(tempDir, "studio-export.tex");
|
|
2954
|
+
const outputPath = join(tempDir, "studio-export.pdf");
|
|
2955
|
+
|
|
2956
|
+
await mkdir(tempDir, { recursive: true });
|
|
2957
|
+
await writeFile(preamblePath, PDF_PREAMBLE, "utf-8");
|
|
2958
|
+
|
|
2959
|
+
const pandocArgs = [
|
|
2960
|
+
"-f", "latex",
|
|
2961
|
+
"-t", "latex",
|
|
2962
|
+
"-s",
|
|
2963
|
+
"-o", latexPath,
|
|
2964
|
+
"-V", "geometry:margin=2.2cm",
|
|
2965
|
+
"-V", "fontsize=11pt",
|
|
2966
|
+
"-V", "linestretch=1.25",
|
|
2967
|
+
"-V", "urlcolor=blue",
|
|
2968
|
+
"-V", "linkcolor=blue",
|
|
2969
|
+
"--include-in-header", preamblePath,
|
|
2970
|
+
...bibliographyArgs,
|
|
2971
|
+
];
|
|
2972
|
+
if (resourcePath) pandocArgs.push(`--resource-path=${resourcePath}`);
|
|
2973
|
+
|
|
2974
|
+
try {
|
|
2975
|
+
await new Promise<void>((resolve, reject) => {
|
|
2976
|
+
const child = spawn(pandocCommand, pandocArgs, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
2977
|
+
const stderrChunks: Buffer[] = [];
|
|
2978
|
+
let settled = false;
|
|
2979
|
+
|
|
2980
|
+
const fail = (error: Error) => {
|
|
2981
|
+
if (settled) return;
|
|
2982
|
+
settled = true;
|
|
2983
|
+
reject(error);
|
|
2984
|
+
};
|
|
2985
|
+
|
|
2986
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
2987
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2988
|
+
});
|
|
2989
|
+
|
|
2990
|
+
child.once("error", (error) => {
|
|
2991
|
+
const errno = error as NodeJS.ErrnoException;
|
|
2992
|
+
if (errno.code === "ENOENT") {
|
|
2993
|
+
const commandHint = pandocCommand === "pandoc"
|
|
2994
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
2995
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
2996
|
+
fail(new Error(commandHint));
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
fail(error);
|
|
3000
|
+
});
|
|
3001
|
+
|
|
3002
|
+
child.once("close", (code) => {
|
|
3003
|
+
if (settled) return;
|
|
3004
|
+
if (code === 0) {
|
|
3005
|
+
settled = true;
|
|
3006
|
+
resolve();
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
3010
|
+
fail(new Error(`pandoc LaTeX generation failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
child.stdin.end(markdown);
|
|
3014
|
+
});
|
|
3015
|
+
|
|
3016
|
+
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
3017
|
+
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
3018
|
+
const normalizedLatex = normalizeStudioGeneratedFigureCaptions(injectedLatex);
|
|
3019
|
+
await writeFile(latexPath, normalizedLatex, "utf-8");
|
|
3020
|
+
|
|
3021
|
+
await new Promise<void>((resolve, reject) => {
|
|
3022
|
+
const child = spawn(pdfEngine, [
|
|
3023
|
+
"-interaction=nonstopmode",
|
|
3024
|
+
"-halt-on-error",
|
|
3025
|
+
`-output-directory=${tempDir}`,
|
|
3026
|
+
latexPath,
|
|
3027
|
+
], { stdio: ["ignore", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
3028
|
+
const stdoutChunks: Buffer[] = [];
|
|
3029
|
+
const stderrChunks: Buffer[] = [];
|
|
3030
|
+
let settled = false;
|
|
3031
|
+
|
|
3032
|
+
const fail = (error: Error) => {
|
|
3033
|
+
if (settled) return;
|
|
3034
|
+
settled = true;
|
|
3035
|
+
reject(error);
|
|
3036
|
+
};
|
|
3037
|
+
|
|
3038
|
+
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
3039
|
+
stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3040
|
+
});
|
|
3041
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
3042
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3043
|
+
});
|
|
3044
|
+
|
|
3045
|
+
child.once("error", (error) => {
|
|
3046
|
+
const errno = error as NodeJS.ErrnoException;
|
|
3047
|
+
if (errno.code === "ENOENT") {
|
|
3048
|
+
fail(new Error(
|
|
3049
|
+
`${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
3050
|
+
));
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
fail(error);
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
child.once("close", (code) => {
|
|
3057
|
+
if (settled) return;
|
|
3058
|
+
if (code === 0) {
|
|
3059
|
+
settled = true;
|
|
3060
|
+
resolve();
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
3064
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
3065
|
+
const errorMatch = stdout.match(/^! .+$/m);
|
|
3066
|
+
const hint = errorMatch ? `: ${errorMatch[0]}` : (stderr ? `: ${stderr}` : "");
|
|
3067
|
+
fail(new Error(`${pdfEngine} PDF export failed with exit code ${code}${hint}`));
|
|
3068
|
+
});
|
|
3069
|
+
});
|
|
3070
|
+
|
|
3071
|
+
return { pdf: await readFile(outputPath) };
|
|
3072
|
+
} finally {
|
|
3073
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
2712
3077
|
async function renderStudioPdfWithPandoc(
|
|
2713
3078
|
markdown: string,
|
|
2714
3079
|
isLatex?: boolean,
|
|
@@ -2718,8 +3083,15 @@ async function renderStudioPdfWithPandoc(
|
|
|
2718
3083
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
2719
3084
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
2720
3085
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
3086
|
+
const latexSubfigurePdfTransform = isLatex
|
|
3087
|
+
? preprocessStudioLatexSubfiguresForPdf(markdown)
|
|
3088
|
+
: { markdown, groups: [] };
|
|
2721
3089
|
const latexPdfSource = isLatex
|
|
2722
|
-
? preprocessStudioLatexAlgorithmsForPdf(
|
|
3090
|
+
? preprocessStudioLatexAlgorithmsForPdf(
|
|
3091
|
+
latexSubfigurePdfTransform.markdown,
|
|
3092
|
+
sourcePath,
|
|
3093
|
+
resourcePath,
|
|
3094
|
+
)
|
|
2723
3095
|
: markdown;
|
|
2724
3096
|
const sourceWithResolvedRefs = isLatex
|
|
2725
3097
|
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
|
|
@@ -2805,6 +3177,19 @@ async function renderStudioPdfWithPandoc(
|
|
|
2805
3177
|
}
|
|
2806
3178
|
};
|
|
2807
3179
|
|
|
3180
|
+
if (isLatex && latexSubfigurePdfTransform.groups.length > 0) {
|
|
3181
|
+
return await renderStudioPdfFromGeneratedLatex(
|
|
3182
|
+
sourceWithResolvedRefs,
|
|
3183
|
+
pandocCommand,
|
|
3184
|
+
pdfEngine,
|
|
3185
|
+
resourcePath,
|
|
3186
|
+
pandocWorkingDir,
|
|
3187
|
+
bibliographyArgs,
|
|
3188
|
+
sourcePath,
|
|
3189
|
+
latexSubfigurePdfTransform.groups,
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
2808
3193
|
if (!isLatex && effectiveEditorLanguage === "diff") {
|
|
2809
3194
|
const inputFormat = "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
2810
3195
|
const diffMarkdown = prepareStudioPdfMarkdown(markdown, false, effectiveEditorLanguage);
|