pi-studio 0.5.22 → 0.5.24
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 +452 -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.24] — 2026-03-20
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
## [0.5.23] — 2026-03-20
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- LaTeX PDF export now preprocesses common `algorithm` / `algorithmic` / `algpseudocode` blocks into pandoc-friendly quoted step layouts, improving exported algorithm readability while keeping the existing Studio PDF pipeline.
|
|
16
|
+
|
|
7
17
|
## [0.5.22] — 2026-03-20
|
|
8
18
|
|
|
9
19
|
### Fixed
|
package/index.ts
CHANGED
|
@@ -1123,6 +1123,25 @@ interface StudioLatexSubfigurePreviewTransformResult {
|
|
|
1123
1123
|
subfigureGroups: StudioLatexSubfigurePreviewGroup[];
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
|
+
interface StudioLatexPdfSubfigureItem {
|
|
1127
|
+
imagePath: string;
|
|
1128
|
+
imageOptions: string | null;
|
|
1129
|
+
widthSpec: string | null;
|
|
1130
|
+
caption: string | null;
|
|
1131
|
+
label: string | null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
interface StudioLatexPdfSubfigureGroup {
|
|
1135
|
+
caption: string | null;
|
|
1136
|
+
label: string | null;
|
|
1137
|
+
items: StudioLatexPdfSubfigureItem[];
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
interface StudioLatexPdfSubfigureTransformResult {
|
|
1141
|
+
markdown: string;
|
|
1142
|
+
groups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1126
1145
|
interface StudioLatexAlgorithmPreviewLine {
|
|
1127
1146
|
indent: number;
|
|
1128
1147
|
content: string;
|
|
@@ -1197,6 +1216,17 @@ function readStudioLatexEnvironmentBlock(
|
|
|
1197
1216
|
return null;
|
|
1198
1217
|
}
|
|
1199
1218
|
|
|
1219
|
+
function extractStudioLatexFirstCommandArgument(input: string, commandName: string, allowStar = false): string | null {
|
|
1220
|
+
const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1221
|
+
const pattern = new RegExp(`\\\\${escapedCommand}${allowStar ? "\\*?" : ""}(?:\\s*\\[[^\\]]*\\])?\\s*\\{`, "g");
|
|
1222
|
+
const match = pattern.exec(input);
|
|
1223
|
+
if (!match) return null;
|
|
1224
|
+
const openBraceIndex = pattern.lastIndex - 1;
|
|
1225
|
+
const closeBraceIndex = findStudioLatexMatchingBrace(input, openBraceIndex);
|
|
1226
|
+
if (closeBraceIndex < 0) return null;
|
|
1227
|
+
return input.slice(openBraceIndex + 1, closeBraceIndex).trim() || null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1200
1230
|
function extractStudioLatexLastCommandArgument(input: string, commandName: string, allowStar = false): string | null {
|
|
1201
1231
|
const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1202
1232
|
const pattern = new RegExp(`\\\\${escapedCommand}${allowStar ? "\\*?" : ""}(?:\\s*\\[[^\\]]*\\])?\\s*\\{`, "g");
|
|
@@ -1233,10 +1263,98 @@ function convertStudioLatexLengthToCss(length: string): string | null {
|
|
|
1233
1263
|
return null;
|
|
1234
1264
|
}
|
|
1235
1265
|
|
|
1236
|
-
function
|
|
1266
|
+
function extractStudioLatexSubfigureWidthSpec(blockText: string): string | null {
|
|
1237
1267
|
const match = blockText.match(/^\\begin\s*\{subfigure\*?\}(?:\s*\[[^\]]*\])?\s*\{([^}]*)\}/);
|
|
1268
|
+
return match?.[1]?.trim() || null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function extractStudioLatexSubfigureWidth(blockText: string): string | null {
|
|
1272
|
+
const widthSpec = extractStudioLatexSubfigureWidthSpec(blockText);
|
|
1273
|
+
if (!widthSpec) return null;
|
|
1274
|
+
return convertStudioLatexLengthToCss(widthSpec);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function extractStudioLatexIncludeGraphics(input: string): { path: string; options: string | null } | null {
|
|
1278
|
+
const pattern = /\\includegraphics\*?(?:\s*\[[^\]]*\])?\s*\{/g;
|
|
1279
|
+
const match = pattern.exec(input);
|
|
1238
1280
|
if (!match) return null;
|
|
1239
|
-
|
|
1281
|
+
const openBraceIndex = pattern.lastIndex - 1;
|
|
1282
|
+
const closeBraceIndex = findStudioLatexMatchingBrace(input, openBraceIndex);
|
|
1283
|
+
if (closeBraceIndex < 0) return null;
|
|
1284
|
+
const optionMatch = match[0].match(/\[([^\]]*)\]/);
|
|
1285
|
+
return {
|
|
1286
|
+
path: input.slice(openBraceIndex + 1, closeBraceIndex).trim(),
|
|
1287
|
+
options: optionMatch?.[1]?.trim() || null,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function collectStudioLatexPdfSubfigureGroups(markdown: string): Array<{ start: number; end: number; group: StudioLatexPdfSubfigureGroup }> {
|
|
1292
|
+
const groups: Array<{ start: number; end: number; group: StudioLatexPdfSubfigureGroup }> = [];
|
|
1293
|
+
const figurePattern = /\\begin\s*\{(figure\*?)\}/g;
|
|
1294
|
+
|
|
1295
|
+
for (;;) {
|
|
1296
|
+
const figureMatch = figurePattern.exec(markdown);
|
|
1297
|
+
if (!figureMatch) break;
|
|
1298
|
+
const envName = figureMatch[1] ?? "figure";
|
|
1299
|
+
const block = readStudioLatexEnvironmentBlock(markdown, figureMatch.index, envName);
|
|
1300
|
+
if (!block) continue;
|
|
1301
|
+
const inner = block.innerText;
|
|
1302
|
+
const subfigurePattern = /\\begin\s*\{(subfigure\*?)\}/g;
|
|
1303
|
+
const subfigureBlocks: Array<{ start: number; end: number; fullText: string }> = [];
|
|
1304
|
+
for (;;) {
|
|
1305
|
+
const subfigureMatch = subfigurePattern.exec(inner);
|
|
1306
|
+
if (!subfigureMatch) break;
|
|
1307
|
+
const subfigureEnvName = subfigureMatch[1] ?? "subfigure";
|
|
1308
|
+
const subfigureBlock = readStudioLatexEnvironmentBlock(inner, subfigureMatch.index, subfigureEnvName);
|
|
1309
|
+
if (!subfigureBlock) continue;
|
|
1310
|
+
subfigureBlocks.push({
|
|
1311
|
+
start: subfigureMatch.index,
|
|
1312
|
+
end: subfigureBlock.endIndex,
|
|
1313
|
+
fullText: subfigureBlock.fullText.trim(),
|
|
1314
|
+
});
|
|
1315
|
+
subfigurePattern.lastIndex = subfigureBlock.endIndex;
|
|
1316
|
+
}
|
|
1317
|
+
if (subfigureBlocks.length === 0) continue;
|
|
1318
|
+
|
|
1319
|
+
let outerResidual = "";
|
|
1320
|
+
let residualCursor = 0;
|
|
1321
|
+
for (const subfigureBlock of subfigureBlocks) {
|
|
1322
|
+
outerResidual += inner.slice(residualCursor, subfigureBlock.start);
|
|
1323
|
+
residualCursor = subfigureBlock.end;
|
|
1324
|
+
}
|
|
1325
|
+
outerResidual += inner.slice(residualCursor);
|
|
1326
|
+
|
|
1327
|
+
const items: StudioLatexPdfSubfigureItem[] = [];
|
|
1328
|
+
let allHaveImages = true;
|
|
1329
|
+
for (const subfigureBlock of subfigureBlocks) {
|
|
1330
|
+
const image = extractStudioLatexIncludeGraphics(subfigureBlock.fullText);
|
|
1331
|
+
if (!image?.path) {
|
|
1332
|
+
allHaveImages = false;
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
items.push({
|
|
1336
|
+
imagePath: image.path,
|
|
1337
|
+
imageOptions: image.options,
|
|
1338
|
+
widthSpec: extractStudioLatexSubfigureWidthSpec(subfigureBlock.fullText),
|
|
1339
|
+
caption: extractStudioLatexFirstCommandArgument(subfigureBlock.fullText, "caption", true),
|
|
1340
|
+
label: extractStudioLatexLastCommandArgument(subfigureBlock.fullText, "label"),
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (!allHaveImages || items.length === 0) continue;
|
|
1344
|
+
|
|
1345
|
+
groups.push({
|
|
1346
|
+
start: figureMatch.index,
|
|
1347
|
+
end: block.endIndex,
|
|
1348
|
+
group: {
|
|
1349
|
+
caption: extractStudioLatexLastCommandArgument(outerResidual, "caption", true),
|
|
1350
|
+
label: extractStudioLatexLastCommandArgument(outerResidual, "label"),
|
|
1351
|
+
items,
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
figurePattern.lastIndex = block.endIndex;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return groups;
|
|
1240
1358
|
}
|
|
1241
1359
|
|
|
1242
1360
|
function preprocessStudioLatexSubfiguresForPreview(markdown: string): StudioLatexSubfigurePreviewTransformResult {
|
|
@@ -1560,6 +1678,78 @@ function preprocessStudioLatexAlgorithmsForPreview(markdown: string): StudioLate
|
|
|
1560
1678
|
};
|
|
1561
1679
|
}
|
|
1562
1680
|
|
|
1681
|
+
function renderStudioLatexAlgorithmPdfLines(
|
|
1682
|
+
lines: StudioLatexAlgorithmPreviewLine[],
|
|
1683
|
+
startIndex: number,
|
|
1684
|
+
indent: number,
|
|
1685
|
+
): { latex: string; nextIndex: number } {
|
|
1686
|
+
const parts: string[] = [];
|
|
1687
|
+
let index = startIndex;
|
|
1688
|
+
|
|
1689
|
+
while (index < lines.length) {
|
|
1690
|
+
const line = lines[index]!;
|
|
1691
|
+
if (line.indent < indent) break;
|
|
1692
|
+
if (line.indent > indent) {
|
|
1693
|
+
const nested = renderStudioLatexAlgorithmPdfLines(lines, index, line.indent);
|
|
1694
|
+
if (nested.latex.trim()) {
|
|
1695
|
+
parts.push(`\\begin{quote}\n${nested.latex}\n\\end{quote}`);
|
|
1696
|
+
}
|
|
1697
|
+
index = nested.nextIndex;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const prefix = line.lineNumber == null ? "" : `${line.lineNumber}. `;
|
|
1702
|
+
parts.push(`${prefix}${line.content}`.trim());
|
|
1703
|
+
index++;
|
|
1704
|
+
|
|
1705
|
+
while (index < lines.length && lines[index]!.indent > indent) {
|
|
1706
|
+
const nested = renderStudioLatexAlgorithmPdfLines(lines, index, lines[index]!.indent);
|
|
1707
|
+
if (nested.latex.trim()) {
|
|
1708
|
+
parts.push(`\\begin{quote}\n${nested.latex}\n\\end{quote}`);
|
|
1709
|
+
}
|
|
1710
|
+
index = nested.nextIndex;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
return {
|
|
1715
|
+
latex: parts.filter(Boolean).join("\n\n"),
|
|
1716
|
+
nextIndex: index,
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function buildStudioLatexAlgorithmPdfBlock(
|
|
1721
|
+
block: StudioLatexAlgorithmPreviewBlock,
|
|
1722
|
+
labels: Map<string, { number: string; kind: string }>,
|
|
1723
|
+
): string {
|
|
1724
|
+
const body = renderStudioLatexAlgorithmPdfLines(block.lines, 0, 0).latex.trim();
|
|
1725
|
+
const captionLabel = formatStudioLatexMainAlgorithmCaptionLabel(block.label, labels);
|
|
1726
|
+
const heading = captionLabel
|
|
1727
|
+
? (block.caption ? `\\textbf{${captionLabel}} ${block.caption}` : `\\textbf{${captionLabel}}`)
|
|
1728
|
+
: (block.caption ? `\\textbf{${block.caption}}` : "");
|
|
1729
|
+
const parts = [heading, body].filter(Boolean);
|
|
1730
|
+
return `\n\n\\begin{quote}\n${parts.join("\n\n")}\n\\end{quote}\n\n`;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function preprocessStudioLatexAlgorithmsForPdf(markdown: string, sourcePath: string | undefined, baseDir: string | undefined): string {
|
|
1734
|
+
const previewTransform = preprocessStudioLatexAlgorithmsForPreview(markdown);
|
|
1735
|
+
if (previewTransform.algorithmBlocks.length === 0) return markdown;
|
|
1736
|
+
const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
|
|
1737
|
+
let transformed = previewTransform.markdown;
|
|
1738
|
+
|
|
1739
|
+
for (const block of previewTransform.algorithmBlocks) {
|
|
1740
|
+
const startMarker = `PISTUDIOALGORITHMSTART${block.markerId}`;
|
|
1741
|
+
const endMarker = `PISTUDIOALGORITHMEND${block.markerId}`;
|
|
1742
|
+
const startIndex = transformed.indexOf(startMarker);
|
|
1743
|
+
if (startIndex < 0) continue;
|
|
1744
|
+
const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
|
|
1745
|
+
if (endIndex < 0) continue;
|
|
1746
|
+
const endSliceIndex = endIndex + endMarker.length;
|
|
1747
|
+
transformed = transformed.slice(0, startIndex) + buildStudioLatexAlgorithmPdfBlock(block, labels) + transformed.slice(endSliceIndex);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
return transformed;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1563
1753
|
function appendStudioHtmlClassAttribute(attrs: string, className: string): string {
|
|
1564
1754
|
if (/\bclass="([^"]*)"/.test(attrs)) {
|
|
1565
1755
|
return attrs.replace(/\bclass="([^"]*)"/, (_match, existing) => {
|
|
@@ -1619,6 +1809,106 @@ function formatStudioLatexMainFigureCaptionLabel(label: string | null, labels: M
|
|
|
1619
1809
|
return `Figure ${entry.number}`;
|
|
1620
1810
|
}
|
|
1621
1811
|
|
|
1812
|
+
function estimateStudioLatexRelativeWidth(widthSpec: string | null | undefined): number | null {
|
|
1813
|
+
const normalized = String(widthSpec ?? "").replace(/\s+/g, "");
|
|
1814
|
+
if (!normalized) return null;
|
|
1815
|
+
const fractionalMatch = normalized.match(/^([0-9]*\.?[0-9]+)\\(?:textwidth|linewidth|columnwidth|hsize)$/);
|
|
1816
|
+
if (!fractionalMatch) return null;
|
|
1817
|
+
const value = Number.parseFloat(fractionalMatch[1] ?? "");
|
|
1818
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function buildStudioLatexInjectedPdfSubfigureBlock(
|
|
1822
|
+
group: StudioLatexPdfSubfigureGroup,
|
|
1823
|
+
labels: Map<string, { number: string; kind: string }>,
|
|
1824
|
+
): string {
|
|
1825
|
+
const figureLabel = formatStudioLatexMainFigureCaptionLabel(group.label, labels);
|
|
1826
|
+
const figureCaption = figureLabel
|
|
1827
|
+
? (group.caption ? `\\textbf{${figureLabel}} ${group.caption}` : `\\textbf{${figureLabel}}`)
|
|
1828
|
+
: (group.caption ? group.caption : "");
|
|
1829
|
+
|
|
1830
|
+
const minipageBlocks = group.items.map((item) => {
|
|
1831
|
+
const widthSpec = item.widthSpec || "0.48\\textwidth";
|
|
1832
|
+
const imageCommand = `\\includegraphics${item.imageOptions ? `[${item.imageOptions}]` : "[width=\\linewidth]"}{${item.imagePath}}`;
|
|
1833
|
+
const subfigureLabel = formatStudioLatexSubfigureCaptionLabel(item.label, labels);
|
|
1834
|
+
const captionLine = subfigureLabel
|
|
1835
|
+
? (item.caption ? `\\textbf{${subfigureLabel}} ${item.caption}` : `\\textbf{${subfigureLabel}}`)
|
|
1836
|
+
: (item.caption ? item.caption : "");
|
|
1837
|
+
const parts = [
|
|
1838
|
+
`\\begin{minipage}[t]{${widthSpec}}`,
|
|
1839
|
+
"\\centering",
|
|
1840
|
+
imageCommand,
|
|
1841
|
+
captionLine ? `\\par\\smallskip ${captionLine}` : "",
|
|
1842
|
+
"\\end{minipage}",
|
|
1843
|
+
].filter(Boolean);
|
|
1844
|
+
return {
|
|
1845
|
+
latex: parts.join("\n"),
|
|
1846
|
+
relativeWidth: estimateStudioLatexRelativeWidth(widthSpec) ?? 0.48,
|
|
1847
|
+
};
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
const rows: string[] = [];
|
|
1851
|
+
let currentRow: string[] = [];
|
|
1852
|
+
let currentWidth = 0;
|
|
1853
|
+
for (const block of minipageBlocks) {
|
|
1854
|
+
if (currentRow.length > 0 && currentWidth + block.relativeWidth > 1.02) {
|
|
1855
|
+
rows.push(currentRow.join("\n\\hfill\n"));
|
|
1856
|
+
currentRow = [];
|
|
1857
|
+
currentWidth = 0;
|
|
1858
|
+
}
|
|
1859
|
+
currentRow.push(block.latex);
|
|
1860
|
+
currentWidth += block.relativeWidth;
|
|
1861
|
+
}
|
|
1862
|
+
if (currentRow.length > 0) rows.push(currentRow.join("\n\\hfill\n"));
|
|
1863
|
+
|
|
1864
|
+
const bodyParts = [
|
|
1865
|
+
"\\clearpage",
|
|
1866
|
+
"\\begin{figure}[p]",
|
|
1867
|
+
"\\centering",
|
|
1868
|
+
rows.join("\n\\par\\medskip\n"),
|
|
1869
|
+
figureCaption ? `\\par\\bigskip ${figureCaption}` : "",
|
|
1870
|
+
"\\end{figure}",
|
|
1871
|
+
"\\clearpage",
|
|
1872
|
+
].filter(Boolean);
|
|
1873
|
+
return `\n${bodyParts.join("\n")}\n`;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function preprocessStudioLatexSubfiguresForPdf(markdown: string): StudioLatexPdfSubfigureTransformResult {
|
|
1877
|
+
const groups = collectStudioLatexPdfSubfigureGroups(markdown);
|
|
1878
|
+
if (groups.length === 0) return { markdown, groups: [] };
|
|
1879
|
+
let transformed = "";
|
|
1880
|
+
let cursor = 0;
|
|
1881
|
+
const placeholderGroups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }> = [];
|
|
1882
|
+
|
|
1883
|
+
for (const [index, entry] of groups.entries()) {
|
|
1884
|
+
const placeholder = `PISTUDIOSUBFIGUREPDFPLACEHOLDER${index + 1}`;
|
|
1885
|
+
placeholderGroups.push({ placeholder, group: entry.group });
|
|
1886
|
+
transformed += markdown.slice(cursor, entry.start);
|
|
1887
|
+
transformed += `\n\n${placeholder}\n\n`;
|
|
1888
|
+
cursor = entry.end;
|
|
1889
|
+
}
|
|
1890
|
+
transformed += markdown.slice(cursor);
|
|
1891
|
+
return {
|
|
1892
|
+
markdown: transformed,
|
|
1893
|
+
groups: placeholderGroups,
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function injectStudioLatexPdfSubfigureBlocks(
|
|
1898
|
+
latex: string,
|
|
1899
|
+
groups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>,
|
|
1900
|
+
sourcePath: string | undefined,
|
|
1901
|
+
baseDir: string | undefined,
|
|
1902
|
+
): string {
|
|
1903
|
+
if (groups.length === 0) return latex;
|
|
1904
|
+
const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
|
|
1905
|
+
let transformed = String(latex ?? "");
|
|
1906
|
+
for (const entry of groups) {
|
|
1907
|
+
transformed = transformed.replace(entry.placeholder, buildStudioLatexInjectedPdfSubfigureBlock(entry.group, labels));
|
|
1908
|
+
}
|
|
1909
|
+
return transformed;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1622
1912
|
function formatStudioLatexMainAlgorithmCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
|
|
1623
1913
|
const normalizedLabel = String(label ?? "").trim();
|
|
1624
1914
|
if (!normalizedLabel) return null;
|
|
@@ -2637,6 +2927,141 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export")
|
|
|
2637
2927
|
}
|
|
2638
2928
|
}
|
|
2639
2929
|
|
|
2930
|
+
async function renderStudioPdfFromGeneratedLatex(
|
|
2931
|
+
markdown: string,
|
|
2932
|
+
pandocCommand: string,
|
|
2933
|
+
pdfEngine: string,
|
|
2934
|
+
resourcePath: string | undefined,
|
|
2935
|
+
pandocWorkingDir: string | undefined,
|
|
2936
|
+
bibliographyArgs: string[],
|
|
2937
|
+
sourcePath: string | undefined,
|
|
2938
|
+
subfigureGroups: Array<{ placeholder: string; group: StudioLatexPdfSubfigureGroup }>,
|
|
2939
|
+
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
2940
|
+
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
2941
|
+
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
2942
|
+
const latexPath = join(tempDir, "studio-export.tex");
|
|
2943
|
+
const outputPath = join(tempDir, "studio-export.pdf");
|
|
2944
|
+
|
|
2945
|
+
await mkdir(tempDir, { recursive: true });
|
|
2946
|
+
await writeFile(preamblePath, PDF_PREAMBLE, "utf-8");
|
|
2947
|
+
|
|
2948
|
+
const pandocArgs = [
|
|
2949
|
+
"-f", "latex",
|
|
2950
|
+
"-t", "latex",
|
|
2951
|
+
"-s",
|
|
2952
|
+
"-o", latexPath,
|
|
2953
|
+
"-V", "geometry:margin=2.2cm",
|
|
2954
|
+
"-V", "fontsize=11pt",
|
|
2955
|
+
"-V", "linestretch=1.25",
|
|
2956
|
+
"-V", "urlcolor=blue",
|
|
2957
|
+
"-V", "linkcolor=blue",
|
|
2958
|
+
"--include-in-header", preamblePath,
|
|
2959
|
+
...bibliographyArgs,
|
|
2960
|
+
];
|
|
2961
|
+
if (resourcePath) pandocArgs.push(`--resource-path=${resourcePath}`);
|
|
2962
|
+
|
|
2963
|
+
try {
|
|
2964
|
+
await new Promise<void>((resolve, reject) => {
|
|
2965
|
+
const child = spawn(pandocCommand, pandocArgs, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
2966
|
+
const stderrChunks: Buffer[] = [];
|
|
2967
|
+
let settled = false;
|
|
2968
|
+
|
|
2969
|
+
const fail = (error: Error) => {
|
|
2970
|
+
if (settled) return;
|
|
2971
|
+
settled = true;
|
|
2972
|
+
reject(error);
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2975
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
2976
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
child.once("error", (error) => {
|
|
2980
|
+
const errno = error as NodeJS.ErrnoException;
|
|
2981
|
+
if (errno.code === "ENOENT") {
|
|
2982
|
+
const commandHint = pandocCommand === "pandoc"
|
|
2983
|
+
? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
|
|
2984
|
+
: `${pandocCommand} was not found. Check PANDOC_PATH.`;
|
|
2985
|
+
fail(new Error(commandHint));
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
fail(error);
|
|
2989
|
+
});
|
|
2990
|
+
|
|
2991
|
+
child.once("close", (code) => {
|
|
2992
|
+
if (settled) return;
|
|
2993
|
+
if (code === 0) {
|
|
2994
|
+
settled = true;
|
|
2995
|
+
resolve();
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
2999
|
+
fail(new Error(`pandoc LaTeX generation failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
|
|
3000
|
+
});
|
|
3001
|
+
|
|
3002
|
+
child.stdin.end(markdown);
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
const generatedLatex = await readFile(latexPath, "utf-8");
|
|
3006
|
+
const injectedLatex = injectStudioLatexPdfSubfigureBlocks(generatedLatex, subfigureGroups, sourcePath, resourcePath);
|
|
3007
|
+
await writeFile(latexPath, injectedLatex, "utf-8");
|
|
3008
|
+
|
|
3009
|
+
await new Promise<void>((resolve, reject) => {
|
|
3010
|
+
const child = spawn(pdfEngine, [
|
|
3011
|
+
"-interaction=nonstopmode",
|
|
3012
|
+
"-halt-on-error",
|
|
3013
|
+
`-output-directory=${tempDir}`,
|
|
3014
|
+
latexPath,
|
|
3015
|
+
], { stdio: ["ignore", "pipe", "pipe"], cwd: pandocWorkingDir });
|
|
3016
|
+
const stdoutChunks: Buffer[] = [];
|
|
3017
|
+
const stderrChunks: Buffer[] = [];
|
|
3018
|
+
let settled = false;
|
|
3019
|
+
|
|
3020
|
+
const fail = (error: Error) => {
|
|
3021
|
+
if (settled) return;
|
|
3022
|
+
settled = true;
|
|
3023
|
+
reject(error);
|
|
3024
|
+
};
|
|
3025
|
+
|
|
3026
|
+
child.stdout.on("data", (chunk: Buffer | string) => {
|
|
3027
|
+
stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3028
|
+
});
|
|
3029
|
+
child.stderr.on("data", (chunk: Buffer | string) => {
|
|
3030
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
3031
|
+
});
|
|
3032
|
+
|
|
3033
|
+
child.once("error", (error) => {
|
|
3034
|
+
const errno = error as NodeJS.ErrnoException;
|
|
3035
|
+
if (errno.code === "ENOENT") {
|
|
3036
|
+
fail(new Error(
|
|
3037
|
+
`${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`,
|
|
3038
|
+
));
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
fail(error);
|
|
3042
|
+
});
|
|
3043
|
+
|
|
3044
|
+
child.once("close", (code) => {
|
|
3045
|
+
if (settled) return;
|
|
3046
|
+
if (code === 0) {
|
|
3047
|
+
settled = true;
|
|
3048
|
+
resolve();
|
|
3049
|
+
return;
|
|
3050
|
+
}
|
|
3051
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
3052
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
3053
|
+
const errorMatch = stdout.match(/^! .+$/m);
|
|
3054
|
+
const hint = errorMatch ? `: ${errorMatch[0]}` : (stderr ? `: ${stderr}` : "");
|
|
3055
|
+
fail(new Error(`${pdfEngine} PDF export failed with exit code ${code}${hint}`));
|
|
3056
|
+
});
|
|
3057
|
+
});
|
|
3058
|
+
|
|
3059
|
+
return { pdf: await readFile(outputPath) };
|
|
3060
|
+
} finally {
|
|
3061
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
2640
3065
|
async function renderStudioPdfWithPandoc(
|
|
2641
3066
|
markdown: string,
|
|
2642
3067
|
isLatex?: boolean,
|
|
@@ -2646,12 +3071,22 @@ async function renderStudioPdfWithPandoc(
|
|
|
2646
3071
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
2647
3072
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
2648
3073
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
3074
|
+
const latexSubfigurePdfTransform = isLatex
|
|
3075
|
+
? preprocessStudioLatexSubfiguresForPdf(markdown)
|
|
3076
|
+
: { markdown, groups: [] };
|
|
3077
|
+
const latexPdfSource = isLatex
|
|
3078
|
+
? preprocessStudioLatexAlgorithmsForPdf(
|
|
3079
|
+
latexSubfigurePdfTransform.markdown,
|
|
3080
|
+
sourcePath,
|
|
3081
|
+
resourcePath,
|
|
3082
|
+
)
|
|
3083
|
+
: markdown;
|
|
2649
3084
|
const sourceWithResolvedRefs = isLatex
|
|
2650
|
-
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(
|
|
3085
|
+
? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
|
|
2651
3086
|
: markdown;
|
|
2652
3087
|
const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
|
|
2653
3088
|
const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
|
|
2654
|
-
const bibliographyArgs = buildStudioPandocBibliographyArgs(
|
|
3089
|
+
const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
|
|
2655
3090
|
|
|
2656
3091
|
const runPandocPdfExport = async (
|
|
2657
3092
|
inputFormat: string,
|
|
@@ -2730,6 +3165,19 @@ async function renderStudioPdfWithPandoc(
|
|
|
2730
3165
|
}
|
|
2731
3166
|
};
|
|
2732
3167
|
|
|
3168
|
+
if (isLatex && latexSubfigurePdfTransform.groups.length > 0) {
|
|
3169
|
+
return await renderStudioPdfFromGeneratedLatex(
|
|
3170
|
+
sourceWithResolvedRefs,
|
|
3171
|
+
pandocCommand,
|
|
3172
|
+
pdfEngine,
|
|
3173
|
+
resourcePath,
|
|
3174
|
+
pandocWorkingDir,
|
|
3175
|
+
bibliographyArgs,
|
|
3176
|
+
sourcePath,
|
|
3177
|
+
latexSubfigurePdfTransform.groups,
|
|
3178
|
+
);
|
|
3179
|
+
}
|
|
3180
|
+
|
|
2733
3181
|
if (!isLatex && effectiveEditorLanguage === "diff") {
|
|
2734
3182
|
const inputFormat = "markdown+lists_without_preceding_blankline+tex_math_dollars+autolink_bare_uris+superscript+subscript-raw_html";
|
|
2735
3183
|
const diffMarkdown = prepareStudioPdfMarkdown(markdown, false, effectiveEditorLanguage);
|