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 +13 -0
- package/client/studio.css +106 -0
- package/index.ts +765 -50
- package/package.json +1 -1
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, "'");
|
|
1220
1905
|
}
|
|
1221
1906
|
|
|
1222
|
-
function decorateStudioLatexRenderedHtml(
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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="
|
|
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="
|
|
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="
|
|
3154
|
-
<option value="
|
|
3155
|
-
<option value="
|
|
3156
|
-
<option value="
|
|
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>
|