pi-studio 0.5.21 → 0.5.22

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,14 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.22] — 2026-03-20
8
+
9
+ ### Fixed
10
+ - Citeproc-rendered LaTeX bibliographies now request a visible `References` section heading in Studio preview/PDF output.
11
+ - 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.
12
+ - 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.
13
+ - The editor language dropdown is now alphabetised for quicker scanning.
14
+
7
15
  ## [0.5.21] — 2026-03-19
8
16
 
9
17
  ### 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,623 @@ 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 appendStudioHtmlClassAttribute(attrs: string, className: string): string {
1564
+ if (/\bclass="([^"]*)"/.test(attrs)) {
1565
+ return attrs.replace(/\bclass="([^"]*)"/, (_match, existing) => {
1566
+ const classNames = String(existing ?? "").split(/\s+/).filter(Boolean);
1567
+ if (!classNames.includes(className)) classNames.push(className);
1568
+ return `class="${classNames.join(" ")}"`;
1569
+ });
1570
+ }
1571
+ return `${attrs} class="${className}"`;
1572
+ }
1573
+
1574
+ function appendStudioHtmlStyleAttribute(attrs: string, styleText: string): string {
1575
+ if (/\bstyle="([^"]*)"/.test(attrs)) {
1576
+ return attrs.replace(/\bstyle="([^"]*)"/, (_match, existing) => {
1577
+ const prefix = String(existing ?? "").trim();
1578
+ const separator = prefix && !prefix.endsWith(";") ? "; " : (prefix ? " " : "");
1579
+ return `style="${prefix}${separator}${styleText}"`;
1580
+ });
1581
+ }
1582
+ return `${attrs} style="${styleText}"`;
1583
+ }
1584
+
1585
+ function prependStudioHtmlCaptionLabel(captionHtml: string, labelHtml: string, className: string): string {
1586
+ const normalizedCaption = String(captionHtml ?? "");
1587
+ const normalizedLabel = String(labelHtml ?? "").trim();
1588
+ if (!normalizedCaption || !normalizedLabel) return normalizedCaption;
1589
+ if (normalizedCaption.includes(`class="${className}"`)) return normalizedCaption;
1590
+ return normalizedCaption.replace(/<figcaption\b([^>]*)>([\s\S]*?)<\/figcaption>/i, (_match, attrs, inner) => {
1591
+ const trimmedInner = String(inner ?? "").trim();
1592
+ const spacer = trimmedInner ? " " : "";
1593
+ return `<figcaption${attrs}><span class="${className}">${normalizedLabel}</span>${spacer}${trimmedInner}</figcaption>`;
1594
+ });
1595
+ }
1596
+
1597
+ function extractStudioHtmlIdAttribute(html: string): string | null {
1598
+ const match = String(html ?? "").match(/\bid="([^"]+)"/i);
1599
+ return match?.[1]?.trim() || null;
1600
+ }
1601
+
1602
+ function formatStudioLatexSubfigureCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1603
+ const normalizedLabel = String(label ?? "").trim();
1604
+ if (!normalizedLabel) return null;
1605
+ const subfigureEntry = labels.get(`sub@${normalizedLabel}`);
1606
+ if (subfigureEntry?.number) return `(${subfigureEntry.number})`;
1607
+ const figureEntry = labels.get(normalizedLabel);
1608
+ if (!figureEntry?.number) return null;
1609
+ const suffixMatch = figureEntry.number.match(/([A-Za-z]+)$/);
1610
+ return suffixMatch ? `(${suffixMatch[1]})` : null;
1611
+ }
1612
+
1613
+ function formatStudioLatexMainFigureCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1614
+ const normalizedLabel = String(label ?? "").trim();
1615
+ if (!normalizedLabel) return null;
1616
+ const entry = labels.get(normalizedLabel);
1617
+ if (!entry?.number) return null;
1618
+ if (entry.kind === "table") return `Table ${entry.number}`;
1619
+ return `Figure ${entry.number}`;
1620
+ }
1621
+
1622
+ function formatStudioLatexMainAlgorithmCaptionLabel(label: string | null, labels: Map<string, { number: string; kind: string }>): string | null {
1623
+ const normalizedLabel = String(label ?? "").trim();
1624
+ if (!normalizedLabel) return null;
1625
+ const entry = labels.get(normalizedLabel);
1626
+ if (!entry?.number) return null;
1627
+ return `Algorithm ${entry.number}`;
1628
+ }
1629
+
1630
+ function decorateStudioLatexSubfigureRenderedHtml(
1631
+ html: string,
1632
+ subfigureGroups: StudioLatexSubfigurePreviewGroup[],
1633
+ labels: Map<string, { number: string; kind: string }>,
1634
+ ): string {
1635
+ let transformed = String(html ?? "");
1636
+ for (const group of subfigureGroups) {
1637
+ const startMarker = `<p>PISTUDIOSUBFIGURESTART${group.markerId}</p>`;
1638
+ const endMarker = `<p>PISTUDIOSUBFIGUREEND${group.markerId}</p>`;
1639
+ const startIndex = transformed.indexOf(startMarker);
1640
+ if (startIndex < 0) continue;
1641
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
1642
+ if (endIndex < 0) continue;
1643
+
1644
+ let groupBody = transformed.slice(startIndex + startMarker.length, endIndex).trim();
1645
+ let captionHtml = "";
1646
+ const captionPattern = new RegExp(`<p>PISTUDIOSUBFIGURECAPTION${group.markerId}\\s*([\\s\\S]*?)<\\/p>\\s*$`);
1647
+ const captionMatch = groupBody.match(captionPattern);
1648
+ if (captionMatch) {
1649
+ captionHtml = String(captionMatch[1] ?? "").trim();
1650
+ groupBody = groupBody.slice(0, captionMatch.index).trim();
1651
+ }
1652
+ if (!/<figure\b/i.test(groupBody)) continue;
1653
+
1654
+ let figureIndex = 0;
1655
+ const figureBlocks = Array.from(groupBody.matchAll(/<figure\b([^>]*)>([\s\S]*?)<\/figure>/g));
1656
+ const gridHtml = figureBlocks.map((figureMatch) => {
1657
+ let attrs = String(figureMatch[1] ?? "");
1658
+ let innerHtml = String(figureMatch[2] ?? "").trim();
1659
+ attrs = appendStudioHtmlClassAttribute(attrs, "studio-subfigure-entry");
1660
+ const widthCss = group.subfigureWidths[figureIndex++] ?? null;
1661
+ if (widthCss) {
1662
+ attrs = appendStudioHtmlStyleAttribute(attrs, `flex-basis: ${widthCss}; width: min(100%, ${widthCss});`);
1663
+ }
1664
+ const subfigureLabel = formatStudioLatexSubfigureCaptionLabel(extractStudioHtmlIdAttribute(innerHtml), labels);
1665
+ if (subfigureLabel) {
1666
+ innerHtml = prependStudioHtmlCaptionLabel(innerHtml, subfigureLabel, "studio-subfigure-caption-label");
1667
+ }
1668
+ return `<figure${attrs}>${innerHtml}</figure>`;
1669
+ }).join("\n").trim();
1670
+ if (!gridHtml) continue;
1671
+
1672
+ const idAttr = group.label ? ` id="${escapeStudioHtmlText(group.label)}"` : "";
1673
+ const mainFigureLabel = formatStudioLatexMainFigureCaptionLabel(group.label, labels);
1674
+ const figcaptionHtml = captionHtml
1675
+ ? prependStudioHtmlCaptionLabel(`<figcaption>${captionHtml}</figcaption>`, mainFigureLabel ?? "", "studio-figure-caption-label")
1676
+ : "";
1677
+ const replacement = `<figure class="studio-subfigure-group"${idAttr}><div class="studio-subfigure-grid">${gridHtml}</div>${figcaptionHtml}</figure>`;
1678
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
1679
+ }
1680
+ return transformed;
1681
+ }
1682
+
1683
+ function decorateStudioLatexAlgorithmRenderedHtml(
1684
+ html: string,
1685
+ algorithmBlocks: StudioLatexAlgorithmPreviewBlock[],
1686
+ labels: Map<string, { number: string; kind: string }>,
1687
+ ): string {
1688
+ let transformed = String(html ?? "");
1689
+ for (const block of algorithmBlocks) {
1690
+ const startMarker = `<p>PISTUDIOALGORITHMSTART${block.markerId}</p>`;
1691
+ const endMarker = `<p>PISTUDIOALGORITHMEND${block.markerId}</p>`;
1692
+ const startIndex = transformed.indexOf(startMarker);
1693
+ if (startIndex < 0) continue;
1694
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
1695
+ if (endIndex < 0) continue;
1696
+
1697
+ let blockBody = transformed.slice(startIndex + startMarker.length, endIndex).trim();
1698
+ let captionHtml = "";
1699
+ const captionPattern = new RegExp(`<p>PISTUDIOALGORITHMCAPTION${block.markerId}\\s*([\\s\\S]*?)<\\/p>`);
1700
+ const captionMatch = blockBody.match(captionPattern);
1701
+ if (captionMatch && captionMatch.index != null) {
1702
+ captionHtml = String(captionMatch[1] ?? "").trim();
1703
+ blockBody = blockBody.slice(0, captionMatch.index) + blockBody.slice(captionMatch.index + captionMatch[0].length);
1704
+ }
1705
+
1706
+ const linePattern = new RegExp(`<p>PISTUDIOALGORITHMLINE${block.markerId}::(\\d+)::([^:]+)::\\s*([\\s\\S]*?)<\\/p>`, "g");
1707
+ const renderedLines = Array.from(blockBody.matchAll(linePattern)).map((lineMatch) => {
1708
+ const indent = Number.parseInt(lineMatch[1] ?? "0", 10);
1709
+ const lineNumber = String(lineMatch[2] ?? "-").trim();
1710
+ const lineHtml = String(lineMatch[3] ?? "").trim();
1711
+ 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>`;
1712
+ }).join("");
1713
+ if (!renderedLines) continue;
1714
+
1715
+ const idAttr = block.label ? ` id="${escapeStudioHtmlText(block.label)}"` : "";
1716
+ const captionLabel = formatStudioLatexMainAlgorithmCaptionLabel(block.label, labels);
1717
+ const figcaptionHtml = captionHtml
1718
+ ? prependStudioHtmlCaptionLabel(`<figcaption>${captionHtml}</figcaption>`, captionLabel ?? "", "studio-algorithm-caption-label")
1719
+ : (captionLabel ? `<figcaption><span class="studio-algorithm-caption-label">${escapeStudioHtmlText(captionLabel)}</span></figcaption>` : "");
1720
+ const replacement = `<figure class="studio-algorithm-block"${idAttr}>${figcaptionHtml}<div class="studio-algorithm-body">${renderedLines}</div></figure>`;
1721
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
1722
+ }
1723
+ return transformed;
1724
+ }
1725
+
1113
1726
  function parseStudioAuxTopLevelGroups(input: string): string[] {
1114
1727
  const groups: string[] = [];
1115
1728
  let i = 0;
@@ -1219,37 +1832,51 @@ function escapeStudioHtmlText(text: string): string {
1219
1832
  .replace(/'/g, "&#39;");
1220
1833
  }
1221
1834
 
1222
- function decorateStudioLatexRenderedHtml(html: string, sourcePath: string | undefined, baseDir: string | undefined): string {
1835
+ function decorateStudioLatexRenderedHtml(
1836
+ html: string,
1837
+ sourcePath: string | undefined,
1838
+ baseDir: string | undefined,
1839
+ subfigureGroups: StudioLatexSubfigurePreviewGroup[] = [],
1840
+ algorithmBlocks: StudioLatexAlgorithmPreviewBlock[] = [],
1841
+ ): string {
1223
1842
  const labels = readStudioLatexAuxLabels(sourcePath, baseDir);
1224
- if (labels.size === 0) return html;
1225
1843
  let transformed = String(html ?? "");
1226
1844
 
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
- });
1845
+ if (labels.size > 0) {
1846
+ transformed = transformed.replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/g, (match, attrs) => {
1847
+ const typeMatch = String(attrs ?? "").match(/\bdata-reference-type="([^"]+)"/);
1848
+ const labelMatch = String(attrs ?? "").match(/\bdata-reference="([^"]+)"/);
1849
+ if (!typeMatch || !labelMatch) return match;
1850
+ const referenceTypeRaw = String(typeMatch[1] ?? "").trim();
1851
+ const label = String(labelMatch[1] ?? "").trim();
1852
+ const referenceType =
1853
+ referenceTypeRaw === "eqref" || referenceTypeRaw === "autoref" || referenceTypeRaw === "ref"
1854
+ ? referenceTypeRaw
1855
+ : null;
1856
+ if (!referenceType || !label) return match;
1857
+ const formatted = formatStudioLatexReference(label, referenceType, labels);
1858
+ if (!formatted) return match;
1859
+ return `<a${attrs}>${escapeStudioHtmlText(formatted)}</a>`;
1860
+ });
1242
1861
 
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
- });
1862
+ transformed = transformed.replace(/<math\b[^>]*display="block"[^>]*>[\s\S]*?<\/math>/g, (block) => {
1863
+ if (/studio-display-equation/.test(block)) return block;
1864
+ const labelMatch = block.match(/\\label\s*\{([^}]+)\}/);
1865
+ if (!labelMatch) return block;
1866
+ const label = String(labelMatch[1] ?? "").trim();
1867
+ if (!label) return block;
1868
+ const formatted = formatStudioLatexReference(label, "eqref", labels);
1869
+ if (!formatted) return block;
1870
+ return `<div class="studio-display-equation"><div class="studio-display-equation-body">${block}</div><div class="studio-display-equation-number">${escapeStudioHtmlText(formatted)}</div></div>`;
1871
+ });
1872
+ }
1873
+
1874
+ if (subfigureGroups.length > 0) {
1875
+ transformed = decorateStudioLatexSubfigureRenderedHtml(transformed, subfigureGroups, labels);
1876
+ }
1877
+ if (algorithmBlocks.length > 0) {
1878
+ transformed = decorateStudioLatexAlgorithmRenderedHtml(transformed, algorithmBlocks, labels);
1879
+ }
1253
1880
 
1254
1881
  return transformed;
1255
1882
  }
@@ -1848,9 +2475,17 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
1848
2475
 
1849
2476
  async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
1850
2477
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
1851
- const sourceWithResolvedRefs = isLatex ? preprocessStudioLatexReferences(markdown, sourcePath, resourcePath) : markdown;
2478
+ const latexSubfigurePreviewTransform = isLatex
2479
+ ? preprocessStudioLatexSubfiguresForPreview(markdown)
2480
+ : { markdown, subfigureGroups: [] };
2481
+ const latexAlgorithmPreviewTransform = isLatex
2482
+ ? preprocessStudioLatexAlgorithmsForPreview(latexSubfigurePreviewTransform.markdown)
2483
+ : { markdown, algorithmBlocks: [] };
2484
+ const sourceWithResolvedRefs = isLatex
2485
+ ? preprocessStudioLatexReferences(latexAlgorithmPreviewTransform.markdown, sourcePath, resourcePath)
2486
+ : markdown;
1852
2487
  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);
2488
+ const bibliographyArgs = buildStudioPandocBibliographyArgs(markdown, isLatex, resourcePath);
1854
2489
  const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none", ...bibliographyArgs];
1855
2490
  if (resourcePath) {
1856
2491
  args.push(`--resource-path=${resourcePath}`);
@@ -1904,7 +2539,13 @@ async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolea
1904
2539
  if (bodyMatch) renderedHtml = bodyMatch[1];
1905
2540
  }
1906
2541
  if (isLatex) {
1907
- renderedHtml = decorateStudioLatexRenderedHtml(renderedHtml, sourcePath, resourcePath);
2542
+ renderedHtml = decorateStudioLatexRenderedHtml(
2543
+ renderedHtml,
2544
+ sourcePath,
2545
+ resourcePath,
2546
+ latexSubfigurePreviewTransform.subfigureGroups,
2547
+ latexAlgorithmPreviewTransform.algorithmBlocks,
2548
+ );
1908
2549
  }
1909
2550
  succeed(stripMathMlAnnotationTags(renderedHtml));
1910
2551
  return;
@@ -3130,32 +3771,31 @@ ${cssVarsBlock}
3130
3771
  <option value="on" selected>Syntax highlight: On</option>
3131
3772
  </select>
3132
3773
  <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
3774
  <option value="bash">Lang: Bash</option>
3138
- <option value="json">Lang: JSON</option>
3139
- <option value="rust">Lang: Rust</option>
3140
3775
  <option value="c">Lang: C</option>
3141
3776
  <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>
3777
+ <option value="css">Lang: CSS</option>
3147
3778
  <option value="diff">Lang: Diff</option>
3148
- <option value="java">Lang: Java</option>
3779
+ <option value="fortran">Lang: Fortran</option>
3149
3780
  <option value="go">Lang: Go</option>
3150
- <option value="ruby">Lang: Ruby</option>
3151
- <option value="swift">Lang: Swift</option>
3152
3781
  <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>
3782
+ <option value="java">Lang: Java</option>
3783
+ <option value="javascript">Lang: JavaScript</option>
3784
+ <option value="json">Lang: JSON</option>
3785
+ <option value="julia">Lang: Julia</option>
3786
+ <option value="latex">Lang: LaTeX</option>
3157
3787
  <option value="lua">Lang: Lua</option>
3788
+ <option value="markdown" selected>Lang: Markdown</option>
3789
+ <option value="matlab">Lang: MATLAB</option>
3158
3790
  <option value="text">Lang: Plain Text</option>
3791
+ <option value="python">Lang: Python</option>
3792
+ <option value="r">Lang: R</option>
3793
+ <option value="rust">Lang: Rust</option>
3794
+ <option value="swift">Lang: Swift</option>
3795
+ <option value="toml">Lang: TOML</option>
3796
+ <option value="typescript">Lang: TypeScript</option>
3797
+ <option value="xml">Lang: XML</option>
3798
+ <option value="yaml">Lang: YAML</option>
3159
3799
  </select>
3160
3800
  </div>
3161
3801
  </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.22",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",