pi-studio 0.5.21 → 0.5.23

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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.23] — 2026-03-20
8
+
9
+ ### Fixed
10
+ - 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.
11
+
12
+ ## [0.5.22] — 2026-03-20
13
+
14
+ ### Fixed
15
+ - Citeproc-rendered LaTeX bibliographies now request a visible `References` section heading in Studio preview/PDF output.
16
+ - LaTeX preview now regroups `subfigure`-based figures so adjacent subfigures keep their shared overall figure/caption structure instead of rendering as unrelated standalone figures, including visible `(a)` / `(b)` subfigure markers and `Figure n` main-caption labels when `.aux` labels are available.
17
+ - LaTeX preview now converts common `algorithm` / `algorithmic` / `algpseudocode` blocks into readable algorithm cards with preserved captions, indentation, and optional line numbers instead of showing the raw environment text.
18
+ - The editor language dropdown is now alphabetised for quicker scanning.
19
+
7
20
  ## [0.5.21] — 2026-03-19
8
21
 
9
22
  ### Fixed
package/client/studio.css CHANGED
@@ -569,6 +569,14 @@
569
569
  text-align: center;
570
570
  margin-bottom: 2em;
571
571
  }
572
+
573
+ .rendered-markdown #bibliography {
574
+ margin-bottom: 0.75em;
575
+ }
576
+
577
+ .rendered-markdown #refs {
578
+ margin-top: 0.5em;
579
+ }
572
580
  .rendered-markdown #title-block-header .title {
573
581
  margin-bottom: 0.25em;
574
582
  }
@@ -758,6 +766,104 @@
758
766
  max-width: 100%;
759
767
  }
760
768
 
769
+ .rendered-markdown .studio-subfigure-group {
770
+ margin: 1.25em auto;
771
+ }
772
+
773
+ .rendered-markdown .studio-subfigure-grid {
774
+ display: flex;
775
+ flex-wrap: wrap;
776
+ gap: 1rem;
777
+ justify-content: center;
778
+ align-items: flex-start;
779
+ }
780
+
781
+ .rendered-markdown .studio-subfigure-entry {
782
+ flex: 1 1 220px;
783
+ margin: 0;
784
+ max-width: 100%;
785
+ }
786
+
787
+ .rendered-markdown .studio-subfigure-entry img {
788
+ width: 100%;
789
+ height: auto;
790
+ display: block;
791
+ }
792
+
793
+ .rendered-markdown .studio-subfigure-entry > figcaption,
794
+ .rendered-markdown .studio-subfigure-group > figcaption {
795
+ text-align: center;
796
+ }
797
+
798
+ .rendered-markdown .studio-subfigure-group > figcaption {
799
+ margin-top: 0.8em;
800
+ }
801
+
802
+ .rendered-markdown .studio-subfigure-caption-label,
803
+ .rendered-markdown .studio-figure-caption-label {
804
+ font-weight: 600;
805
+ }
806
+
807
+ .rendered-markdown .studio-subfigure-caption-label {
808
+ margin-right: 0.35em;
809
+ }
810
+
811
+ .rendered-markdown .studio-figure-caption-label {
812
+ margin-right: 0.45em;
813
+ }
814
+
815
+ .rendered-markdown .studio-algorithm-block {
816
+ margin: 1.25em 0;
817
+ border: 1px solid var(--md-codeblock-border);
818
+ border-radius: 10px;
819
+ background: var(--panel-2);
820
+ overflow: hidden;
821
+ }
822
+
823
+ .rendered-markdown .studio-algorithm-block > figcaption {
824
+ padding: 0.8em 1em 0.65em;
825
+ border-bottom: 1px solid var(--border-muted);
826
+ background: rgba(127, 127, 127, 0.06);
827
+ text-align: left;
828
+ }
829
+
830
+ .rendered-markdown .studio-algorithm-caption-label {
831
+ font-weight: 600;
832
+ margin-right: 0.45em;
833
+ }
834
+
835
+ .rendered-markdown .studio-algorithm-body {
836
+ padding: 0.7em 0.9em 0.85em;
837
+ font-family: var(--font-mono);
838
+ font-size: 0.95em;
839
+ }
840
+
841
+ .rendered-markdown .studio-algorithm-line {
842
+ display: grid;
843
+ grid-template-columns: 2.6em minmax(0, 1fr);
844
+ gap: 0.8em;
845
+ align-items: baseline;
846
+ padding: 0.08em 0;
847
+ }
848
+
849
+ .rendered-markdown .studio-algorithm-line-number {
850
+ color: var(--muted);
851
+ text-align: right;
852
+ font-variant-numeric: tabular-nums;
853
+ user-select: none;
854
+ }
855
+
856
+ .rendered-markdown .studio-algorithm-line-content {
857
+ min-width: 0;
858
+ padding-left: calc(var(--studio-algorithm-indent, 0) * 1.35em);
859
+ white-space: pre-wrap;
860
+ overflow-wrap: anywhere;
861
+ }
862
+
863
+ .rendered-markdown .studio-algorithm-line-content math {
864
+ font-size: 1em;
865
+ }
866
+
761
867
  .rendered-markdown math {
762
868
  font-family: "STIX Two Math", "Cambria Math", "Latin Modern Math", "STIXGeneral", serif;
763
869
  }
package/index.ts CHANGED
@@ -1106,10 +1106,695 @@ function buildStudioPandocBibliographyArgs(markdown: string, isLatex: boolean |
1106
1106
  if (bibliographyPaths.length === 0) return [];
1107
1107
  return [
1108
1108
  "--citeproc",
1109
+ "-M",
1110
+ "reference-section-title=References",
1109
1111
  ...bibliographyPaths.flatMap((path) => ["--bibliography", path]),
1110
1112
  ];
1111
1113
  }
1112
1114
 
1115
+ interface StudioLatexSubfigurePreviewGroup {
1116
+ markerId: string;
1117
+ label: string | null;
1118
+ subfigureWidths: Array<string | null>;
1119
+ }
1120
+
1121
+ interface StudioLatexSubfigurePreviewTransformResult {
1122
+ markdown: string;
1123
+ subfigureGroups: StudioLatexSubfigurePreviewGroup[];
1124
+ }
1125
+
1126
+ interface StudioLatexAlgorithmPreviewLine {
1127
+ indent: number;
1128
+ content: string;
1129
+ lineNumber: number | null;
1130
+ }
1131
+
1132
+ interface StudioLatexAlgorithmPreviewBlock {
1133
+ markerId: string;
1134
+ label: string | null;
1135
+ caption: string | null;
1136
+ lines: StudioLatexAlgorithmPreviewLine[];
1137
+ }
1138
+
1139
+ interface StudioLatexAlgorithmPreviewTransformResult {
1140
+ markdown: string;
1141
+ algorithmBlocks: StudioLatexAlgorithmPreviewBlock[];
1142
+ }
1143
+
1144
+ function findStudioLatexMatchingBrace(input: string, openBraceIndex: number): number {
1145
+ if (input[openBraceIndex] !== "{") return -1;
1146
+ let depth = 0;
1147
+ for (let i = openBraceIndex; i < input.length; i++) {
1148
+ const ch = input[i]!;
1149
+ if (ch === "%") {
1150
+ while (i + 1 < input.length && input[i + 1] !== "\n") i++;
1151
+ continue;
1152
+ }
1153
+ if (ch === "\\") {
1154
+ i++;
1155
+ continue;
1156
+ }
1157
+ if (ch === "{") depth++;
1158
+ else if (ch === "}") {
1159
+ depth--;
1160
+ if (depth === 0) return i;
1161
+ }
1162
+ }
1163
+ return -1;
1164
+ }
1165
+
1166
+ function readStudioLatexEnvironmentBlock(
1167
+ input: string,
1168
+ startIndex: number,
1169
+ envName: string,
1170
+ ): { fullText: string; innerText: string; endIndex: number } | null {
1171
+ const escapedEnvName = envName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1172
+ const beginPattern = new RegExp(`\\\\begin\\s*\\{${escapedEnvName}\\}`, "g");
1173
+ beginPattern.lastIndex = startIndex;
1174
+ const beginMatch = beginPattern.exec(input);
1175
+ if (!beginMatch || beginMatch.index !== startIndex) return null;
1176
+ const contentStart = beginPattern.lastIndex;
1177
+ const tokenPattern = new RegExp(`\\\\(?:begin|end)\\s*\\{${escapedEnvName}\\}`, "g");
1178
+ tokenPattern.lastIndex = startIndex;
1179
+ let depth = 0;
1180
+ for (;;) {
1181
+ const tokenMatch = tokenPattern.exec(input);
1182
+ if (!tokenMatch) break;
1183
+ if (tokenMatch.index === startIndex) {
1184
+ depth = 1;
1185
+ continue;
1186
+ }
1187
+ if (tokenMatch[0].startsWith("\\begin")) depth++;
1188
+ else depth--;
1189
+ if (depth === 0) {
1190
+ return {
1191
+ fullText: input.slice(startIndex, tokenPattern.lastIndex),
1192
+ innerText: input.slice(contentStart, tokenMatch.index),
1193
+ endIndex: tokenPattern.lastIndex,
1194
+ };
1195
+ }
1196
+ }
1197
+ return null;
1198
+ }
1199
+
1200
+ function extractStudioLatexLastCommandArgument(input: string, commandName: string, allowStar = false): string | null {
1201
+ const escapedCommand = commandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1202
+ const pattern = new RegExp(`\\\\${escapedCommand}${allowStar ? "\\*?" : ""}(?:\\s*\\[[^\\]]*\\])?\\s*\\{`, "g");
1203
+ let lastValue: string | null = null;
1204
+ for (;;) {
1205
+ const match = pattern.exec(input);
1206
+ if (!match) break;
1207
+ const openBraceIndex = pattern.lastIndex - 1;
1208
+ const closeBraceIndex = findStudioLatexMatchingBrace(input, openBraceIndex);
1209
+ if (closeBraceIndex < 0) continue;
1210
+ lastValue = input.slice(openBraceIndex + 1, closeBraceIndex).trim() || null;
1211
+ pattern.lastIndex = closeBraceIndex + 1;
1212
+ }
1213
+ return lastValue;
1214
+ }
1215
+
1216
+ function convertStudioLatexLengthToCss(length: string): string | null {
1217
+ const normalized = String(length ?? "").replace(/\s+/g, "");
1218
+ if (!normalized) return null;
1219
+ const fractionalMatch = normalized.match(/^([0-9]*\.?[0-9]+)\\(?:textwidth|linewidth|columnwidth|hsize)$/);
1220
+ if (fractionalMatch) {
1221
+ const fraction = Number.parseFloat(fractionalMatch[1] ?? "");
1222
+ if (Number.isFinite(fraction) && fraction > 0) {
1223
+ return `${Math.min(fraction * 100, 100)}%`;
1224
+ }
1225
+ }
1226
+ const percentMatch = normalized.match(/^([0-9]*\.?[0-9]+)%$/);
1227
+ if (percentMatch) {
1228
+ const percent = Number.parseFloat(percentMatch[1] ?? "");
1229
+ if (Number.isFinite(percent) && percent > 0) {
1230
+ return `${Math.min(percent, 100)}%`;
1231
+ }
1232
+ }
1233
+ return null;
1234
+ }
1235
+
1236
+ function extractStudioLatexSubfigureWidth(blockText: string): string | null {
1237
+ const match = blockText.match(/^\\begin\s*\{subfigure\*?\}(?:\s*\[[^\]]*\])?\s*\{([^}]*)\}/);
1238
+ if (!match) return null;
1239
+ return convertStudioLatexLengthToCss(match[1] ?? "");
1240
+ }
1241
+
1242
+ function preprocessStudioLatexSubfiguresForPreview(markdown: string): StudioLatexSubfigurePreviewTransformResult {
1243
+ const subfigureGroups: StudioLatexSubfigurePreviewGroup[] = [];
1244
+ const figurePattern = /\\begin\s*\{(figure\*?)\}/g;
1245
+ let transformed = "";
1246
+ let cursor = 0;
1247
+
1248
+ for (;;) {
1249
+ const figureMatch = figurePattern.exec(markdown);
1250
+ if (!figureMatch) break;
1251
+ const envName = figureMatch[1] ?? "figure";
1252
+ const block = readStudioLatexEnvironmentBlock(markdown, figureMatch.index, envName);
1253
+ if (!block) continue;
1254
+ const inner = block.innerText;
1255
+ const subfigurePattern = /\\begin\s*\{(subfigure\*?)\}/g;
1256
+ const subfigureBlocks: Array<{ start: number; end: number; fullText: string; widthCss: string | null }> = [];
1257
+ for (;;) {
1258
+ const subfigureMatch = subfigurePattern.exec(inner);
1259
+ if (!subfigureMatch) break;
1260
+ const subfigureEnvName = subfigureMatch[1] ?? "subfigure";
1261
+ const subfigureBlock = readStudioLatexEnvironmentBlock(inner, subfigureMatch.index, subfigureEnvName);
1262
+ if (!subfigureBlock) continue;
1263
+ subfigureBlocks.push({
1264
+ start: subfigureMatch.index,
1265
+ end: subfigureBlock.endIndex,
1266
+ fullText: subfigureBlock.fullText.trim(),
1267
+ widthCss: extractStudioLatexSubfigureWidth(subfigureBlock.fullText),
1268
+ });
1269
+ subfigurePattern.lastIndex = subfigureBlock.endIndex;
1270
+ }
1271
+
1272
+ if (subfigureBlocks.length === 0) continue;
1273
+
1274
+ let outerResidual = "";
1275
+ let residualCursor = 0;
1276
+ for (const subfigureBlock of subfigureBlocks) {
1277
+ outerResidual += inner.slice(residualCursor, subfigureBlock.start);
1278
+ residualCursor = subfigureBlock.end;
1279
+ }
1280
+ outerResidual += inner.slice(residualCursor);
1281
+
1282
+ const markerId = String(subfigureGroups.length + 1);
1283
+ const overallCaption = extractStudioLatexLastCommandArgument(outerResidual, "caption", true);
1284
+ const overallLabel = extractStudioLatexLastCommandArgument(outerResidual, "label");
1285
+ subfigureGroups.push({
1286
+ markerId,
1287
+ label: overallLabel,
1288
+ subfigureWidths: subfigureBlocks.map((blockEntry) => blockEntry.widthCss),
1289
+ });
1290
+
1291
+ const replacementParts = [
1292
+ `PISTUDIOSUBFIGURESTART${markerId}`,
1293
+ ...subfigureBlocks.map((blockEntry) => blockEntry.fullText),
1294
+ overallCaption ? `PISTUDIOSUBFIGURECAPTION${markerId} ${overallCaption}` : "",
1295
+ `PISTUDIOSUBFIGUREEND${markerId}`,
1296
+ ].filter(Boolean);
1297
+
1298
+ transformed += markdown.slice(cursor, figureMatch.index);
1299
+ transformed += replacementParts.join("\n\n");
1300
+ cursor = block.endIndex;
1301
+ figurePattern.lastIndex = block.endIndex;
1302
+ }
1303
+
1304
+ transformed += markdown.slice(cursor);
1305
+ return {
1306
+ markdown: transformed,
1307
+ subfigureGroups,
1308
+ };
1309
+ }
1310
+
1311
+ function parseStudioLatexLeadingCommand(line: string): { name: string; args: string[]; rest: string } | null {
1312
+ const trimmed = String(line ?? "").trim();
1313
+ const commandMatch = trimmed.match(/^\\([A-Za-z]+\*?)/);
1314
+ if (!commandMatch) return null;
1315
+ let cursor = commandMatch[0].length;
1316
+ const args: string[] = [];
1317
+
1318
+ for (;;) {
1319
+ while (cursor < trimmed.length && /\s/.test(trimmed[cursor]!)) cursor++;
1320
+ if (trimmed[cursor] === "[") {
1321
+ const closeBracket = trimmed.indexOf("]", cursor + 1);
1322
+ if (closeBracket < 0) break;
1323
+ cursor = closeBracket + 1;
1324
+ continue;
1325
+ }
1326
+ if (trimmed[cursor] !== "{") break;
1327
+ const closeBraceIndex = findStudioLatexMatchingBrace(trimmed, cursor);
1328
+ if (closeBraceIndex < 0) break;
1329
+ args.push(trimmed.slice(cursor + 1, closeBraceIndex));
1330
+ cursor = closeBraceIndex + 1;
1331
+ }
1332
+
1333
+ return {
1334
+ name: commandMatch[1] ?? "",
1335
+ args,
1336
+ rest: trimmed.slice(cursor).trim(),
1337
+ };
1338
+ }
1339
+
1340
+ function stripStudioLatexOptionalBracketPrefix(text: string): string {
1341
+ const normalized = String(text ?? "").trimStart();
1342
+ if (!normalized.startsWith("[")) return normalized;
1343
+ const closeBracketIndex = normalized.indexOf("]");
1344
+ if (closeBracketIndex < 0) return normalized;
1345
+ return normalized.slice(closeBracketIndex + 1).trimStart();
1346
+ }
1347
+
1348
+ function normalizeStudioLatexAlgorithmInlineText(text: string): string {
1349
+ return String(text ?? "")
1350
+ .replace(/\\Comment\s*\{([^}]*)\}/g, " // $1")
1351
+ .replace(/\\\s+/g, " ")
1352
+ .replace(/\s+/g, " ")
1353
+ .trim();
1354
+ }
1355
+
1356
+ function pushStudioLatexAlgorithmPreviewLine(
1357
+ lines: StudioLatexAlgorithmPreviewLine[],
1358
+ indent: number,
1359
+ content: string,
1360
+ showLineNumbers: boolean,
1361
+ lineCounterRef: { value: number },
1362
+ ): void {
1363
+ const normalizedContent = normalizeStudioLatexAlgorithmInlineText(content);
1364
+ if (!normalizedContent) return;
1365
+ lines.push({
1366
+ indent: Math.max(0, indent),
1367
+ content: normalizedContent,
1368
+ lineNumber: showLineNumbers ? lineCounterRef.value++ : null,
1369
+ });
1370
+ }
1371
+
1372
+ function parseStudioLatexAlgorithmicLines(content: string, showLineNumbers: boolean): StudioLatexAlgorithmPreviewLine[] {
1373
+ const lines: StudioLatexAlgorithmPreviewLine[] = [];
1374
+ const lineCounterRef = { value: 1 };
1375
+ let indent = 0;
1376
+ const stripped = stripStudioLatexComments(content);
1377
+
1378
+ for (const rawLine of stripped.split(/\r?\n/)) {
1379
+ const trimmed = rawLine.trim();
1380
+ if (!trimmed) continue;
1381
+ const command = parseStudioLatexLeadingCommand(trimmed);
1382
+ if (!command) {
1383
+ if (lines.length > 0) {
1384
+ const continuation = normalizeStudioLatexAlgorithmInlineText(trimmed);
1385
+ if (continuation) {
1386
+ lines[lines.length - 1]!.content += ` ${continuation}`;
1387
+ }
1388
+ } else {
1389
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, trimmed, showLineNumbers, lineCounterRef);
1390
+ }
1391
+ continue;
1392
+ }
1393
+
1394
+ const name = command.name.replace(/\*$/, "");
1395
+ const arg0 = command.args[0] ?? "";
1396
+ const arg1 = command.args[1] ?? "";
1397
+
1398
+ if (/^(caption|label|begin|end)$/.test(name)) continue;
1399
+ if (/^End(?:For|ForAll|While|If|Procedure|Function)$/i.test(name)) {
1400
+ indent = Math.max(0, indent - 1);
1401
+ const suffix = name.replace(/^End/i, "").replace(/ForAll/i, "for all");
1402
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `end ${suffix.toLowerCase()}`, showLineNumbers, lineCounterRef);
1403
+ continue;
1404
+ }
1405
+ if (/^Else$/i.test(name)) {
1406
+ indent = Math.max(0, indent - 1);
1407
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, "else", showLineNumbers, lineCounterRef);
1408
+ indent++;
1409
+ continue;
1410
+ }
1411
+ if (/^ElsIf$/i.test(name)) {
1412
+ indent = Math.max(0, indent - 1);
1413
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `else if ${arg0}`, showLineNumbers, lineCounterRef);
1414
+ indent++;
1415
+ continue;
1416
+ }
1417
+ if (/^Until$/i.test(name)) {
1418
+ indent = Math.max(0, indent - 1);
1419
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `until ${arg0}`, showLineNumbers, lineCounterRef);
1420
+ continue;
1421
+ }
1422
+ if (/^Statex$/i.test(name)) {
1423
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, command.rest, false, lineCounterRef);
1424
+ continue;
1425
+ }
1426
+ if (/^State$/i.test(name)) {
1427
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, command.rest || arg0, showLineNumbers, lineCounterRef);
1428
+ continue;
1429
+ }
1430
+ if (/^Return$/i.test(name)) {
1431
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `return ${command.rest || arg0}`.trim(), showLineNumbers, lineCounterRef);
1432
+ continue;
1433
+ }
1434
+ if (/^(Require|Input)$/i.test(name)) {
1435
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `Input: ${command.rest || arg0}`.trim(), showLineNumbers, lineCounterRef);
1436
+ continue;
1437
+ }
1438
+ if (/^(Ensure|Output)$/i.test(name)) {
1439
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `Output: ${command.rest || arg0}`.trim(), showLineNumbers, lineCounterRef);
1440
+ continue;
1441
+ }
1442
+ if (/^Comment$/i.test(name)) {
1443
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `// ${arg0 || command.rest}`.trim(), false, lineCounterRef);
1444
+ continue;
1445
+ }
1446
+ if (/^Repeat$/i.test(name)) {
1447
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, "repeat", showLineNumbers, lineCounterRef);
1448
+ indent++;
1449
+ continue;
1450
+ }
1451
+ if (/^ForAll$/i.test(name)) {
1452
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `for all ${arg0}`, showLineNumbers, lineCounterRef);
1453
+ indent++;
1454
+ continue;
1455
+ }
1456
+ if (/^For$/i.test(name)) {
1457
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `for ${arg0}`, showLineNumbers, lineCounterRef);
1458
+ indent++;
1459
+ continue;
1460
+ }
1461
+ if (/^While$/i.test(name)) {
1462
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `while ${arg0}`, showLineNumbers, lineCounterRef);
1463
+ indent++;
1464
+ continue;
1465
+ }
1466
+ if (/^If$/i.test(name)) {
1467
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `if ${arg0}`, showLineNumbers, lineCounterRef);
1468
+ indent++;
1469
+ continue;
1470
+ }
1471
+ if (/^Procedure$/i.test(name)) {
1472
+ const signature = arg1 ? `${arg0}(${arg1})` : arg0;
1473
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `procedure ${signature}`.trim(), showLineNumbers, lineCounterRef);
1474
+ indent++;
1475
+ continue;
1476
+ }
1477
+ if (/^Function$/i.test(name)) {
1478
+ const signature = arg1 ? `${arg0}(${arg1})` : arg0;
1479
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, `function ${signature}`.trim(), showLineNumbers, lineCounterRef);
1480
+ indent++;
1481
+ continue;
1482
+ }
1483
+
1484
+ pushStudioLatexAlgorithmPreviewLine(lines, indent, trimmed, showLineNumbers, lineCounterRef);
1485
+ }
1486
+
1487
+ return lines;
1488
+ }
1489
+
1490
+ function buildStudioLatexAlgorithmPreviewReplacement(block: StudioLatexAlgorithmPreviewBlock): string {
1491
+ const parts = [
1492
+ `PISTUDIOALGORITHMSTART${block.markerId}`,
1493
+ block.caption ? `PISTUDIOALGORITHMCAPTION${block.markerId} ${block.caption}` : "",
1494
+ ...block.lines.map((line) => `PISTUDIOALGORITHMLINE${block.markerId}::${line.indent}::${line.lineNumber == null ? "-" : String(line.lineNumber)}:: ${line.content}`),
1495
+ `PISTUDIOALGORITHMEND${block.markerId}`,
1496
+ ].filter(Boolean);
1497
+ return `\n\n${parts.join("\n\n")}\n\n`;
1498
+ }
1499
+
1500
+ function preprocessStudioLatexAlgorithmsForPreview(markdown: string): StudioLatexAlgorithmPreviewTransformResult {
1501
+ const algorithmBlocks: StudioLatexAlgorithmPreviewBlock[] = [];
1502
+ const transformEnvironment = (input: string, envPattern: RegExp, buildBlock: (block: { fullText: string; innerText: string; endIndex: number }, markerId: string) => StudioLatexAlgorithmPreviewBlock | null): string => {
1503
+ let transformed = "";
1504
+ let cursor = 0;
1505
+ envPattern.lastIndex = 0;
1506
+ for (;;) {
1507
+ const envMatch = envPattern.exec(input);
1508
+ if (!envMatch) break;
1509
+ const envName = envMatch[1] ?? "";
1510
+ const block = readStudioLatexEnvironmentBlock(input, envMatch.index, envName);
1511
+ if (!block) continue;
1512
+ const markerId = String(algorithmBlocks.length + 1);
1513
+ const previewBlock = buildBlock(block, markerId);
1514
+ if (!previewBlock || previewBlock.lines.length === 0) continue;
1515
+ algorithmBlocks.push(previewBlock);
1516
+ transformed += input.slice(cursor, envMatch.index);
1517
+ transformed += buildStudioLatexAlgorithmPreviewReplacement(previewBlock);
1518
+ cursor = block.endIndex;
1519
+ envPattern.lastIndex = block.endIndex;
1520
+ }
1521
+ transformed += input.slice(cursor);
1522
+ return transformed;
1523
+ };
1524
+
1525
+ let transformed = transformEnvironment(markdown, /\\begin\s*\{(algorithm\*?)\}/g, (block, markerId) => {
1526
+ const inner = block.innerText;
1527
+ const algorithmicPattern = /\\begin\s*\{(algorithmic\*?)\}(?:\s*\[[^\]]*\])?/g;
1528
+ const algorithmicMatch = algorithmicPattern.exec(inner);
1529
+ let content = inner;
1530
+ let showLineNumbers = false;
1531
+ if (algorithmicMatch) {
1532
+ const algorithmicEnvName = algorithmicMatch[1] ?? "algorithmic";
1533
+ const algorithmicBlock = readStudioLatexEnvironmentBlock(inner, algorithmicMatch.index, algorithmicEnvName);
1534
+ if (algorithmicBlock) {
1535
+ content = stripStudioLatexOptionalBracketPrefix(algorithmicBlock.innerText);
1536
+ showLineNumbers = /^\\begin\s*\{algorithmic\*?\}\s*\[[^\]]+\]/.test(algorithmicBlock.fullText);
1537
+ }
1538
+ }
1539
+ return {
1540
+ markerId,
1541
+ label: extractStudioLatexLastCommandArgument(inner, "label"),
1542
+ caption: extractStudioLatexLastCommandArgument(inner, "caption", true),
1543
+ lines: parseStudioLatexAlgorithmicLines(content, showLineNumbers),
1544
+ };
1545
+ });
1546
+
1547
+ transformed = transformEnvironment(transformed, /\\begin\s*\{(algorithmic\*?)\}(?:\s*\[[^\]]*\])?/g, (block, markerId) => ({
1548
+ markerId,
1549
+ label: extractStudioLatexLastCommandArgument(block.innerText, "label"),
1550
+ caption: null,
1551
+ lines: parseStudioLatexAlgorithmicLines(
1552
+ stripStudioLatexOptionalBracketPrefix(block.innerText),
1553
+ /^\\begin\s*\{algorithmic\*?\}\s*\[[^\]]+\]/.test(block.fullText),
1554
+ ),
1555
+ }));
1556
+
1557
+ return {
1558
+ markdown: transformed,
1559
+ algorithmBlocks,
1560
+ };
1561
+ }
1562
+
1563
+ function renderStudioLatexAlgorithmPdfLines(
1564
+ lines: StudioLatexAlgorithmPreviewLine[],
1565
+ startIndex: number,
1566
+ indent: number,
1567
+ ): { latex: string; nextIndex: number } {
1568
+ const parts: string[] = [];
1569
+ let index = startIndex;
1570
+
1571
+ while (index < lines.length) {
1572
+ const line = lines[index]!;
1573
+ if (line.indent < indent) break;
1574
+ if (line.indent > indent) {
1575
+ const nested = renderStudioLatexAlgorithmPdfLines(lines, index, line.indent);
1576
+ if (nested.latex.trim()) {
1577
+ parts.push(`\\begin{quote}\n${nested.latex}\n\\end{quote}`);
1578
+ }
1579
+ index = nested.nextIndex;
1580
+ continue;
1581
+ }
1582
+
1583
+ const prefix = line.lineNumber == null ? "" : `${line.lineNumber}. `;
1584
+ parts.push(`${prefix}${line.content}`.trim());
1585
+ index++;
1586
+
1587
+ while (index < lines.length && lines[index]!.indent > indent) {
1588
+ const nested = renderStudioLatexAlgorithmPdfLines(lines, index, lines[index]!.indent);
1589
+ if (nested.latex.trim()) {
1590
+ parts.push(`\\begin{quote}\n${nested.latex}\n\\end{quote}`);
1591
+ }
1592
+ index = nested.nextIndex;
1593
+ }
1594
+ }
1595
+
1596
+ return {
1597
+ latex: parts.filter(Boolean).join("\n\n"),
1598
+ nextIndex: index,
1599
+ };
1600
+ }
1601
+
1602
+ function buildStudioLatexAlgorithmPdfBlock(
1603
+ block: StudioLatexAlgorithmPreviewBlock,
1604
+ labels: Map<string, { number: string; kind: string }>,
1605
+ ): string {
1606
+ const body = renderStudioLatexAlgorithmPdfLines(block.lines, 0, 0).latex.trim();
1607
+ const captionLabel = formatStudioLatexMainAlgorithmCaptionLabel(block.label, labels);
1608
+ const heading = captionLabel
1609
+ ? (block.caption ? `\\textbf{${captionLabel}} ${block.caption}` : `\\textbf{${captionLabel}}`)
1610
+ : (block.caption ? `\\textbf{${block.caption}}` : "");
1611
+ const parts = [heading, body].filter(Boolean);
1612
+ return `\n\n\\begin{quote}\n${parts.join("\n\n")}\n\\end{quote}\n\n`;
1613
+ }
1614
+
1615
+ function preprocessStudioLatexAlgorithmsForPdf(markdown: string, sourcePath: string | undefined, baseDir: string | undefined): string {
1616
+ const previewTransform = preprocessStudioLatexAlgorithmsForPreview(markdown);
1617
+ if (previewTransform.algorithmBlocks.length === 0) return markdown;
1618
+ const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
1619
+ let transformed = previewTransform.markdown;
1620
+
1621
+ for (const block of previewTransform.algorithmBlocks) {
1622
+ const startMarker = `PISTUDIOALGORITHMSTART${block.markerId}`;
1623
+ const endMarker = `PISTUDIOALGORITHMEND${block.markerId}`;
1624
+ const startIndex = transformed.indexOf(startMarker);
1625
+ if (startIndex < 0) continue;
1626
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
1627
+ if (endIndex < 0) continue;
1628
+ const endSliceIndex = endIndex + endMarker.length;
1629
+ transformed = transformed.slice(0, startIndex) + buildStudioLatexAlgorithmPdfBlock(block, labels) + transformed.slice(endSliceIndex);
1630
+ }
1631
+
1632
+ return transformed;
1633
+ }
1634
+
1635
+ function appendStudioHtmlClassAttribute(attrs: string, className: string): string {
1636
+ if (/\bclass="([^"]*)"/.test(attrs)) {
1637
+ return attrs.replace(/\bclass="([^"]*)"/, (_match, existing) => {
1638
+ const classNames = String(existing ?? "").split(/\s+/).filter(Boolean);
1639
+ if (!classNames.includes(className)) classNames.push(className);
1640
+ return `class="${classNames.join(" ")}"`;
1641
+ });
1642
+ }
1643
+ return `${attrs} class="${className}"`;
1644
+ }
1645
+
1646
+ function appendStudioHtmlStyleAttribute(attrs: string, styleText: string): string {
1647
+ if (/\bstyle="([^"]*)"/.test(attrs)) {
1648
+ return attrs.replace(/\bstyle="([^"]*)"/, (_match, existing) => {
1649
+ const prefix = String(existing ?? "").trim();
1650
+ const separator = prefix && !prefix.endsWith(";") ? "; " : (prefix ? " " : "");
1651
+ return `style="${prefix}${separator}${styleText}"`;
1652
+ });
1653
+ }
1654
+ return `${attrs} style="${styleText}"`;
1655
+ }
1656
+
1657
+ function prependStudioHtmlCaptionLabel(captionHtml: string, labelHtml: string, className: string): string {
1658
+ const normalizedCaption = String(captionHtml ?? "");
1659
+ const normalizedLabel = String(labelHtml ?? "").trim();
1660
+ if (!normalizedCaption || !normalizedLabel) return normalizedCaption;
1661
+ if (normalizedCaption.includes(`class="${className}"`)) return normalizedCaption;
1662
+ return normalizedCaption.replace(/<figcaption\b([^>]*)>([\s\S]*?)<\/figcaption>/i, (_match, attrs, inner) => {
1663
+ const trimmedInner = String(inner ?? "").trim();
1664
+ const spacer = trimmedInner ? " " : "";
1665
+ return `<figcaption${attrs}><span class="${className}">${normalizedLabel}</span>${spacer}${trimmedInner}</figcaption>`;
1666
+ });
1667
+ }
1668
+
1669
+ function extractStudioHtmlIdAttribute(html: string): string | null {
1670
+ const match = String(html ?? "").match(/\bid="([^"]+)"/i);
1671
+ return match?.[1]?.trim() || null;
1672
+ }
1673
+
1674
+ function formatStudioLatexSubfigureCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1675
+ const normalizedLabel = String(label ?? "").trim();
1676
+ if (!normalizedLabel) return null;
1677
+ const subfigureEntry = labels.get(`sub@${normalizedLabel}`);
1678
+ if (subfigureEntry?.number) return `(${subfigureEntry.number})`;
1679
+ const figureEntry = labels.get(normalizedLabel);
1680
+ if (!figureEntry?.number) return null;
1681
+ const suffixMatch = figureEntry.number.match(/([A-Za-z]+)$/);
1682
+ return suffixMatch ? `(${suffixMatch[1]})` : null;
1683
+ }
1684
+
1685
+ function formatStudioLatexMainFigureCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1686
+ const normalizedLabel = String(label ?? "").trim();
1687
+ if (!normalizedLabel) return null;
1688
+ const entry = labels.get(normalizedLabel);
1689
+ if (!entry?.number) return null;
1690
+ if (entry.kind === "table") return `Table ${entry.number}`;
1691
+ return `Figure ${entry.number}`;
1692
+ }
1693
+
1694
+ function formatStudioLatexMainAlgorithmCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1695
+ const normalizedLabel = String(label ?? "").trim();
1696
+ if (!normalizedLabel) return null;
1697
+ const entry = labels.get(normalizedLabel);
1698
+ if (!entry?.number) return null;
1699
+ return `Algorithm ${entry.number}`;
1700
+ }
1701
+
1702
+ function decorateStudioLatexSubfigureRenderedHtml(
1703
+ html: string,
1704
+ subfigureGroups: StudioLatexSubfigurePreviewGroup[],
1705
+ labels: Map<string, { number: string; kind: string }>,
1706
+ ): string {
1707
+ let transformed = String(html ?? "");
1708
+ for (const group of subfigureGroups) {
1709
+ const startMarker = `<p>PISTUDIOSUBFIGURESTART${group.markerId}</p>`;
1710
+ const endMarker = `<p>PISTUDIOSUBFIGUREEND${group.markerId}</p>`;
1711
+ const startIndex = transformed.indexOf(startMarker);
1712
+ if (startIndex < 0) continue;
1713
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
1714
+ if (endIndex < 0) continue;
1715
+
1716
+ let groupBody = transformed.slice(startIndex + startMarker.length, endIndex).trim();
1717
+ let captionHtml = "";
1718
+ const captionPattern = new RegExp(`<p>PISTUDIOSUBFIGURECAPTION${group.markerId}\\s*([\\s\\S]*?)<\\/p>\\s*$`);
1719
+ const captionMatch = groupBody.match(captionPattern);
1720
+ if (captionMatch) {
1721
+ captionHtml = String(captionMatch[1] ?? "").trim();
1722
+ groupBody = groupBody.slice(0, captionMatch.index).trim();
1723
+ }
1724
+ if (!/<figure\b/i.test(groupBody)) continue;
1725
+
1726
+ let figureIndex = 0;
1727
+ const figureBlocks = Array.from(groupBody.matchAll(/<figure\b([^>]*)>([\s\S]*?)<\/figure>/g));
1728
+ const gridHtml = figureBlocks.map((figureMatch) => {
1729
+ let attrs = String(figureMatch[1] ?? "");
1730
+ let innerHtml = String(figureMatch[2] ?? "").trim();
1731
+ attrs = appendStudioHtmlClassAttribute(attrs, "studio-subfigure-entry");
1732
+ const widthCss = group.subfigureWidths[figureIndex++] ?? null;
1733
+ if (widthCss) {
1734
+ attrs = appendStudioHtmlStyleAttribute(attrs, `flex-basis: ${widthCss}; width: min(100%, ${widthCss});`);
1735
+ }
1736
+ const subfigureLabel = formatStudioLatexSubfigureCaptionLabel(extractStudioHtmlIdAttribute(innerHtml), labels);
1737
+ if (subfigureLabel) {
1738
+ innerHtml = prependStudioHtmlCaptionLabel(innerHtml, subfigureLabel, "studio-subfigure-caption-label");
1739
+ }
1740
+ return `<figure${attrs}>${innerHtml}</figure>`;
1741
+ }).join("\n").trim();
1742
+ if (!gridHtml) continue;
1743
+
1744
+ const idAttr = group.label ? ` id="${escapeStudioHtmlText(group.label)}"` : "";
1745
+ const mainFigureLabel = formatStudioLatexMainFigureCaptionLabel(group.label, labels);
1746
+ const figcaptionHtml = captionHtml
1747
+ ? prependStudioHtmlCaptionLabel(`<figcaption>${captionHtml}</figcaption>`, mainFigureLabel ?? "", "studio-figure-caption-label")
1748
+ : "";
1749
+ const replacement = `<figure class="studio-subfigure-group"${idAttr}><div class="studio-subfigure-grid">${gridHtml}</div>${figcaptionHtml}</figure>`;
1750
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
1751
+ }
1752
+ return transformed;
1753
+ }
1754
+
1755
+ function decorateStudioLatexAlgorithmRenderedHtml(
1756
+ html: string,
1757
+ algorithmBlocks: StudioLatexAlgorithmPreviewBlock[],
1758
+ labels: Map<string, { number: string; kind: string }>,
1759
+ ): string {
1760
+ let transformed = String(html ?? "");
1761
+ for (const block of algorithmBlocks) {
1762
+ const startMarker = `<p>PISTUDIOALGORITHMSTART${block.markerId}</p>`;
1763
+ const endMarker = `<p>PISTUDIOALGORITHMEND${block.markerId}</p>`;
1764
+ const startIndex = transformed.indexOf(startMarker);
1765
+ if (startIndex < 0) continue;
1766
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
1767
+ if (endIndex < 0) continue;
1768
+
1769
+ let blockBody = transformed.slice(startIndex + startMarker.length, endIndex).trim();
1770
+ let captionHtml = "";
1771
+ const captionPattern = new RegExp(`<p>PISTUDIOALGORITHMCAPTION${block.markerId}\\s*([\\s\\S]*?)<\\/p>`);
1772
+ const captionMatch = blockBody.match(captionPattern);
1773
+ if (captionMatch && captionMatch.index != null) {
1774
+ captionHtml = String(captionMatch[1] ?? "").trim();
1775
+ blockBody = blockBody.slice(0, captionMatch.index) + blockBody.slice(captionMatch.index + captionMatch[0].length);
1776
+ }
1777
+
1778
+ const linePattern = new RegExp(`<p>PISTUDIOALGORITHMLINE${block.markerId}::(\\d+)::([^:]+)::\\s*([\\s\\S]*?)<\\/p>`, "g");
1779
+ const renderedLines = Array.from(blockBody.matchAll(linePattern)).map((lineMatch) => {
1780
+ const indent = Number.parseInt(lineMatch[1] ?? "0", 10);
1781
+ const lineNumber = String(lineMatch[2] ?? "-").trim();
1782
+ const lineHtml = String(lineMatch[3] ?? "").trim();
1783
+ return `<div class="studio-algorithm-line" style="--studio-algorithm-indent:${Number.isFinite(indent) ? Math.max(0, indent) : 0};"><span class="studio-algorithm-line-number">${lineNumber === "-" ? "" : escapeStudioHtmlText(lineNumber)}</span><span class="studio-algorithm-line-content">${lineHtml}</span></div>`;
1784
+ }).join("");
1785
+ if (!renderedLines) continue;
1786
+
1787
+ const idAttr = block.label ? ` id="${escapeStudioHtmlText(block.label)}"` : "";
1788
+ const captionLabel = formatStudioLatexMainAlgorithmCaptionLabel(block.label, labels);
1789
+ const figcaptionHtml = captionHtml
1790
+ ? prependStudioHtmlCaptionLabel(`<figcaption>${captionHtml}</figcaption>`, captionLabel ?? "", "studio-algorithm-caption-label")
1791
+ : (captionLabel ? `<figcaption><span class="studio-algorithm-caption-label">${escapeStudioHtmlText(captionLabel)}</span></figcaption>` : "");
1792
+ const replacement = `<figure class="studio-algorithm-block"${idAttr}>${figcaptionHtml}<div class="studio-algorithm-body">${renderedLines}</div></figure>`;
1793
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
1794
+ }
1795
+ return transformed;
1796
+ }
1797
+
1113
1798
  function parseStudioAuxTopLevelGroups(input: string): string[] {
1114
1799
  const groups: string[] = [];
1115
1800
  let i = 0;
@@ -1219,37 +1904,51 @@ function escapeStudioHtmlText(text: string): string {
1219
1904
  .replace(/'/g, "&#39;");
1220
1905
  }
1221
1906
 
1222
- function decorateStudioLatexRenderedHtml(html: string, sourcePath: string | undefined, baseDir: string | undefined): string {
1907
+ function decorateStudioLatexRenderedHtml(
1908
+ html: string,
1909
+ sourcePath: string | undefined,
1910
+ baseDir: string | undefined,
1911
+ subfigureGroups: StudioLatexSubfigurePreviewGroup[] = [],
1912
+ algorithmBlocks: StudioLatexAlgorithmPreviewBlock[] = [],
1913
+ ): string {
1223
1914
  const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
1224
- if (labels.size === 0) return html;
1225
1915
  let transformed = String(html ?? "");
1226
1916
 
1227
- transformed = transformed.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/g, (match, attrs) => {
1228
- const typeMatch = String(attrs ?? "").match(/\bdata-reference-type="([^"]+)"/);
1229
- const labelMatch = String(attrs ?? "").match(/\bdata-reference="([^"]+)"/);
1230
- if (!typeMatch || !labelMatch) return match;
1231
- const referenceTypeRaw = String(typeMatch[1] ?? "").trim();
1232
- const label = String(labelMatch[1] ?? "").trim();
1233
- const referenceType =
1234
- referenceTypeRaw === "eqref" || referenceTypeRaw === "autoref" || referenceTypeRaw === "ref"
1235
- ? referenceTypeRaw
1236
- : null;
1237
- if (!referenceType || !label) return match;
1238
- const formatted = formatStudioLatexReference(label, referenceType, labels);
1239
- if (!formatted) return match;
1240
- return `<a${attrs}>${escapeStudioHtmlText(formatted)}</a>`;
1241
- });
1917
+ if (labels.size > 0) {
1918
+ transformed = transformed.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/g, (match, attrs) => {
1919
+ const typeMatch = String(attrs ?? "").match(/\bdata-reference-type="([^"]+)"/);
1920
+ const labelMatch = String(attrs ?? "").match(/\bdata-reference="([^"]+)"/);
1921
+ if (!typeMatch || !labelMatch) return match;
1922
+ const referenceTypeRaw = String(typeMatch[1] ?? "").trim();
1923
+ const label = String(labelMatch[1] ?? "").trim();
1924
+ const referenceType =
1925
+ referenceTypeRaw === "eqref" || referenceTypeRaw === "autoref" || referenceTypeRaw === "ref"
1926
+ ? referenceTypeRaw
1927
+ : null;
1928
+ if (!referenceType || !label) return match;
1929
+ const formatted = formatStudioLatexReference(label, referenceType, labels);
1930
+ if (!formatted) return match;
1931
+ return `<a${attrs}>${escapeStudioHtmlText(formatted)}</a>`;
1932
+ });
1242
1933
 
1243
- transformed = transformed.replace(/<math\b[^>]*display="block"[^>]*>[\s\S]*?<\/math>/g, (block) => {
1244
- if (/studio-display-equation/.test(block)) return block;
1245
- const labelMatch = block.match(/\\label\s*\{([^}]+)\}/);
1246
- if (!labelMatch) return block;
1247
- const label = String(labelMatch[1] ?? "").trim();
1248
- if (!label) return block;
1249
- const formatted = formatStudioLatexReference(label, "eqref", labels);
1250
- if (!formatted) return block;
1251
- return `<div class="studio-display-equation"><div class="studio-display-equation-body">${block}</div><div class="studio-display-equation-number">${escapeStudioHtmlText(formatted)}</div></div>`;
1252
- });
1934
+ transformed = transformed.replace(/<math\b[^>]*display="block"[^>]*>[\s\S]*?<\/math>/g, (block) => {
1935
+ if (/studio-display-equation/.test(block)) return block;
1936
+ const labelMatch = block.match(/\\label\s*\{([^}]+)\}/);
1937
+ if (!labelMatch) return block;
1938
+ const label = String(labelMatch[1] ?? "").trim();
1939
+ if (!label) return block;
1940
+ const formatted = formatStudioLatexReference(label, "eqref", labels);
1941
+ if (!formatted) return block;
1942
+ return `<div class="studio-display-equation"><div class="studio-display-equation-body">${block}</div><div class="studio-display-equation-number">${escapeStudioHtmlText(formatted)}</div></div>`;
1943
+ });
1944
+ }
1945
+
1946
+ if (subfigureGroups.length > 0) {
1947
+ transformed = decorateStudioLatexSubfigureRenderedHtml(transformed, subfigureGroups, labels);
1948
+ }
1949
+ if (algorithmBlocks.length > 0) {
1950
+ transformed = decorateStudioLatexAlgorithmRenderedHtml(transformed, algorithmBlocks, labels);
1951
+ }
1253
1952
 
1254
1953
  return transformed;
1255
1954
  }
@@ -1848,9 +2547,17 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
1848
2547
 
1849
2548
  async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
1850
2549
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
1851
- const sourceWithResolvedRefs = isLatex ? preprocessStudioLatexReferences(markdown, sourcePath, resourcePath) : markdown;
2550
+ const latexSubfigurePreviewTransform = isLatex
2551
+ ? preprocessStudioLatexSubfiguresForPreview(markdown)
2552
+ : { markdown, subfigureGroups: [] };
2553
+ const latexAlgorithmPreviewTransform = isLatex
2554
+ ? preprocessStudioLatexAlgorithmsForPreview(latexSubfigurePreviewTransform.markdown)
2555
+ : { markdown, algorithmBlocks: [] };
2556
+ const sourceWithResolvedRefs = isLatex
2557
+ ? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
2558
+ : markdown;
1852
2559
  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";
1853
- const bibliographyArgs = buildStudioPandocBibliographyArgs(sourceWithResolvedRefs, isLatex, resourcePath);
2560
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
1854
2561
  const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
1855
2562
  if (resourcePath) {
1856
2563
  args.push(`--resource-path=${resourcePath}`);
@@ -1904,7 +2611,13 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
1904
2611
  if (bodyMatch) renderedHtml = bodyMatch[1];
1905
2612
  }
1906
2613
  if (isLatex) {
1907
- renderedHtml = decorateStudioLatexRenderedHtml(renderedHtml, sourcePath, resourcePath);
2614
+ renderedHtml = decorateStudioLatexRenderedHtml(
2615
+ renderedHtml,
2616
+ sourcePath,
2617
+ resourcePath,
2618
+ latexSubfigurePreviewTransform.subfigureGroups,
2619
+ latexAlgorithmPreviewTransform.algorithmBlocks,
2620
+ );
1908
2621
  }
1909
2622
  succeed(stripMathMlAnnotationTags(renderedHtml));
1910
2623
  return;
@@ -2005,12 +2718,15 @@ async function renderStudioPdfWithPandoc(
2005
2718
  ): Promise<{ pdf: Buffer; warning?: string }> {
2006
2719
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
2007
2720
  const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
2721
+ const latexPdfSource = isLatex
2722
+ ? preprocessStudioLatexAlgorithmsForPdf(markdown, sourcePath, resourcePath)
2723
+ : markdown;
2008
2724
  const sourceWithResolvedRefs = isLatex
2009
- ? injectStudioLatexEquationTags(preprocessStudioLatexReferences(markdown, sourcePath, resourcePath), sourcePath, resourcePath)
2725
+ ? injectStudioLatexEquationTags(preprocessStudioLatexReferences(latexPdfSource, sourcePath, resourcePath), sourcePath, resourcePath)
2010
2726
  : markdown;
2011
2727
  const effectiveEditorLanguage = inferStudioPdfLanguage(sourceWithResolvedRefs, editorPdfLanguage);
2012
2728
  const pandocWorkingDir = resolveStudioPandocWorkingDir(resourcePath);
2013
- const bibliographyArgs = buildStudioPandocBibliographyArgs(sourceWithResolvedRefs, isLatex, resourcePath);
2729
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
2014
2730
 
2015
2731
  const runPandocPdfExport = async (
2016
2732
  inputFormat: string,
@@ -3130,32 +3846,31 @@ ${cssVarsBlock}
3130
3846
  <option value="on" selected>Syntax highlight: On</option>
3131
3847
  </select>
3132
3848
  <select id="langSelect" aria-label="Highlight language">
3133
- <option value="markdown" selected>Lang: Markdown</option>
3134
- <option value="javascript">Lang: JavaScript</option>
3135
- <option value="typescript">Lang: TypeScript</option>
3136
- <option value="python">Lang: Python</option>
3137
3849
  <option value="bash">Lang: Bash</option>
3138
- <option value="json">Lang: JSON</option>
3139
- <option value="rust">Lang: Rust</option>
3140
3850
  <option value="c">Lang: C</option>
3141
3851
  <option value="cpp">Lang: C++</option>
3142
- <option value="julia">Lang: Julia</option>
3143
- <option value="fortran">Lang: Fortran</option>
3144
- <option value="r">Lang: R</option>
3145
- <option value="matlab">Lang: MATLAB</option>
3146
- <option value="latex">Lang: LaTeX</option>
3852
+ <option value="css">Lang: CSS</option>
3147
3853
  <option value="diff">Lang: Diff</option>
3148
- <option value="java">Lang: Java</option>
3854
+ <option value="fortran">Lang: Fortran</option>
3149
3855
  <option value="go">Lang: Go</option>
3150
- <option value="ruby">Lang: Ruby</option>
3151
- <option value="swift">Lang: Swift</option>
3152
3856
  <option value="html">Lang: HTML</option>
3153
- <option value="css">Lang: CSS</option>
3154
- <option value="xml">Lang: XML</option>
3155
- <option value="yaml">Lang: YAML</option>
3156
- <option value="toml">Lang: TOML</option>
3857
+ <option value="java">Lang: Java</option>
3858
+ <option value="javascript">Lang: JavaScript</option>
3859
+ <option value="json">Lang: JSON</option>
3860
+ <option value="julia">Lang: Julia</option>
3861
+ <option value="latex">Lang: LaTeX</option>
3157
3862
  <option value="lua">Lang: Lua</option>
3863
+ <option value="markdown" selected>Lang: Markdown</option>
3864
+ <option value="matlab">Lang: MATLAB</option>
3158
3865
  <option value="text">Lang: Plain Text</option>
3866
+ <option value="python">Lang: Python</option>
3867
+ <option value="r">Lang: R</option>
3868
+ <option value="rust">Lang: Rust</option>
3869
+ <option value="swift">Lang: Swift</option>
3870
+ <option value="toml">Lang: TOML</option>
3871
+ <option value="typescript">Lang: TypeScript</option>
3872
+ <option value="xml">Lang: XML</option>
3873
+ <option value="yaml">Lang: YAML</option>
3159
3874
  </select>
3160
3875
  </div>
3161
3876
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.21",
3
+ "version": "0.5.23",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",