pi-studio-opencode 0.1.0

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.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/demo-host-pi.d.ts +1 -0
  5. package/dist/demo-host-pi.js +71 -0
  6. package/dist/demo-host-pi.js.map +1 -0
  7. package/dist/demo-host.d.ts +1 -0
  8. package/dist/demo-host.js +154 -0
  9. package/dist/demo-host.js.map +1 -0
  10. package/dist/host-opencode-plugin.d.ts +52 -0
  11. package/dist/host-opencode-plugin.js +396 -0
  12. package/dist/host-opencode-plugin.js.map +1 -0
  13. package/dist/host-opencode.d.ts +154 -0
  14. package/dist/host-opencode.js +627 -0
  15. package/dist/host-opencode.js.map +1 -0
  16. package/dist/host-pi.d.ts +45 -0
  17. package/dist/host-pi.js +258 -0
  18. package/dist/host-pi.js.map +1 -0
  19. package/dist/install-config.d.ts +36 -0
  20. package/dist/install-config.js +136 -0
  21. package/dist/install-config.js.map +1 -0
  22. package/dist/install.d.ts +16 -0
  23. package/dist/install.js +168 -0
  24. package/dist/install.js.map +1 -0
  25. package/dist/launcher.d.ts +2 -0
  26. package/dist/launcher.js +124 -0
  27. package/dist/launcher.js.map +1 -0
  28. package/dist/main.d.ts +1 -0
  29. package/dist/main.js +732 -0
  30. package/dist/main.js.map +1 -0
  31. package/dist/mock-pi-session.d.ts +27 -0
  32. package/dist/mock-pi-session.js +138 -0
  33. package/dist/mock-pi-session.js.map +1 -0
  34. package/dist/open-browser.d.ts +1 -0
  35. package/dist/open-browser.js +29 -0
  36. package/dist/open-browser.js.map +1 -0
  37. package/dist/opencode-plugin.d.ts +3 -0
  38. package/dist/opencode-plugin.js +326 -0
  39. package/dist/opencode-plugin.js.map +1 -0
  40. package/dist/prototype-pdf.d.ts +12 -0
  41. package/dist/prototype-pdf.js +991 -0
  42. package/dist/prototype-pdf.js.map +1 -0
  43. package/dist/prototype-server.d.ts +88 -0
  44. package/dist/prototype-server.js +1002 -0
  45. package/dist/prototype-server.js.map +1 -0
  46. package/dist/prototype-theme.d.ts +36 -0
  47. package/dist/prototype-theme.js +1471 -0
  48. package/dist/prototype-theme.js.map +1 -0
  49. package/dist/studio-core.d.ts +63 -0
  50. package/dist/studio-core.js +251 -0
  51. package/dist/studio-core.js.map +1 -0
  52. package/dist/studio-host-types.d.ts +50 -0
  53. package/dist/studio-host-types.js +14 -0
  54. package/dist/studio-host-types.js.map +1 -0
  55. package/examples/opencode/INSTALL.md +67 -0
  56. package/examples/opencode/opencode.local-path.jsonc +16 -0
  57. package/package.json +68 -0
  58. package/static/prototype.css +1277 -0
  59. package/static/prototype.html +173 -0
  60. package/static/prototype.js +3198 -0
@@ -0,0 +1,991 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
+ export const PROTOTYPE_PDF_EXPORT_MAX_CHARS = 400_000;
7
+ function expandHome(input) {
8
+ if (!input)
9
+ return input;
10
+ if (input === "~")
11
+ return homedir();
12
+ if (input.startsWith("~/"))
13
+ return resolve(homedir(), input.slice(2));
14
+ return input;
15
+ }
16
+ async function resolvePrototypePdfWorkingDir(baseDir) {
17
+ const normalized = typeof baseDir === "string" ? baseDir.trim() : "";
18
+ if (!normalized)
19
+ return undefined;
20
+ try {
21
+ return (await stat(normalized)).isDirectory() ? normalized : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function stripPrototypeLatexComments(text) {
28
+ const lines = String(text ?? "").replace(/\r\n/g, "\n").split("\n");
29
+ return lines.map((line) => {
30
+ let out = "";
31
+ let backslashRun = 0;
32
+ for (let i = 0; i < line.length; i += 1) {
33
+ const ch = line[i];
34
+ if (ch === "%" && backslashRun % 2 === 0)
35
+ break;
36
+ out += ch;
37
+ if (ch === "\\")
38
+ backslashRun += 1;
39
+ else
40
+ backslashRun = 0;
41
+ }
42
+ return out;
43
+ }).join("\n");
44
+ }
45
+ function collectPrototypeLatexBibliographyCandidates(markdown) {
46
+ const stripped = stripPrototypeLatexComments(markdown);
47
+ const candidates = [];
48
+ const seen = new Set();
49
+ const pushCandidate = (raw) => {
50
+ let candidate = String(raw ?? "").trim().replace(/^file:/i, "").replace(/^['"]|['"]$/g, "");
51
+ if (!candidate)
52
+ return;
53
+ if (!/\.[A-Za-z0-9]+$/.test(candidate))
54
+ candidate += ".bib";
55
+ if (seen.has(candidate))
56
+ return;
57
+ seen.add(candidate);
58
+ candidates.push(candidate);
59
+ };
60
+ for (const match of stripped.matchAll(/\\bibliography\s*\{([^}]+)\}/g)) {
61
+ const rawList = match[1] ?? "";
62
+ for (const part of rawList.split(",")) {
63
+ pushCandidate(part);
64
+ }
65
+ }
66
+ for (const match of stripped.matchAll(/\\addbibresource(?:\[[^\]]*\])?\s*\{([^}]+)\}/g)) {
67
+ pushCandidate(match[1] ?? "");
68
+ }
69
+ return candidates;
70
+ }
71
+ async function resolvePrototypeLatexBibliographyPaths(markdown, baseDir) {
72
+ const workingDir = await resolvePrototypePdfWorkingDir(baseDir);
73
+ if (!workingDir)
74
+ return [];
75
+ const resolvedPaths = [];
76
+ const seen = new Set();
77
+ for (const candidate of collectPrototypeLatexBibliographyCandidates(markdown)) {
78
+ const expanded = expandHome(candidate);
79
+ const resolvedPath = isAbsolute(expanded) ? expanded : resolve(workingDir, expanded);
80
+ try {
81
+ if (!(await stat(resolvedPath)).isFile())
82
+ continue;
83
+ if (seen.has(resolvedPath))
84
+ continue;
85
+ seen.add(resolvedPath);
86
+ resolvedPaths.push(resolvedPath);
87
+ }
88
+ catch {
89
+ // Ignore missing bibliography files; pandoc can still render the document body.
90
+ }
91
+ }
92
+ return resolvedPaths;
93
+ }
94
+ async function buildPrototypePandocBibliographyArgs(markdown, isLatex, baseDir) {
95
+ if (!isLatex)
96
+ return [];
97
+ const bibliographyPaths = await resolvePrototypeLatexBibliographyPaths(markdown, baseDir);
98
+ if (bibliographyPaths.length === 0)
99
+ return [];
100
+ return [
101
+ "--citeproc",
102
+ "-M",
103
+ "reference-section-title=References",
104
+ ...bibliographyPaths.flatMap((path) => ["--bibliography", path]),
105
+ ];
106
+ }
107
+ function normalizePrototypeEditorLanguage(language) {
108
+ const trimmed = typeof language === "string" ? language.trim().toLowerCase() : "";
109
+ if (!trimmed)
110
+ return undefined;
111
+ if (trimmed === "patch" || trimmed === "udiff")
112
+ return "diff";
113
+ return trimmed;
114
+ }
115
+ function parsePrototypeSingleFencedCodeBlock(markdown) {
116
+ const trimmed = markdown.trim();
117
+ if (!trimmed)
118
+ return null;
119
+ const lines = trimmed.split("\n");
120
+ if (lines.length < 2)
121
+ return null;
122
+ const openingLine = (lines[0] ?? "").trim();
123
+ const openingMatch = openingLine.match(/^(`{3,}|~{3,})([^\n]*)$/);
124
+ if (!openingMatch)
125
+ return null;
126
+ const openingFence = openingMatch[1];
127
+ const info = (openingMatch[2] ?? "").trim();
128
+ const closingLine = (lines[lines.length - 1] ?? "").trim();
129
+ const closingMatch = closingLine.match(/^(`{3,}|~{3,})\s*$/);
130
+ if (!closingMatch)
131
+ return null;
132
+ const closingFence = closingMatch[1];
133
+ if (closingFence[0] !== openingFence[0] || closingFence.length < openingFence.length) {
134
+ return null;
135
+ }
136
+ return {
137
+ info,
138
+ content: lines.slice(1, -1).join("\n"),
139
+ };
140
+ }
141
+ function isPrototypeSingleFencedCodeBlock(markdown) {
142
+ return parsePrototypeSingleFencedCodeBlock(markdown) !== null;
143
+ }
144
+ function getLongestPrototypeFenceRun(text, fenceChar) {
145
+ const regex = fenceChar === "`" ? /`+/g : /~+/g;
146
+ let max = 0;
147
+ let match;
148
+ while ((match = regex.exec(text)) !== null) {
149
+ max = Math.max(max, match[0].length);
150
+ }
151
+ return max;
152
+ }
153
+ function wrapPrototypeCodeAsMarkdown(code, language) {
154
+ const source = String(code ?? "").replace(/\r\n/g, "\n").trimEnd();
155
+ const lang = normalizePrototypeEditorLanguage(language) ?? "";
156
+ const maxBackticks = getLongestPrototypeFenceRun(source, "`");
157
+ const maxTildes = getLongestPrototypeFenceRun(source, "~");
158
+ let markerChar = "`";
159
+ if (maxBackticks === 0 && maxTildes === 0) {
160
+ markerChar = "`";
161
+ }
162
+ else if (maxTildes < maxBackticks) {
163
+ markerChar = "~";
164
+ }
165
+ else if (maxBackticks < maxTildes) {
166
+ markerChar = "`";
167
+ }
168
+ else {
169
+ markerChar = maxBackticks > 0 ? "~" : "`";
170
+ }
171
+ const markerLength = Math.max(3, (markerChar === "`" ? maxBackticks : maxTildes) + 1);
172
+ const marker = markerChar.repeat(markerLength);
173
+ return `${marker}${lang}\n${source}\n${marker}`;
174
+ }
175
+ function isLikelyRawPrototypeGitDiff(markdown) {
176
+ const text = String(markdown ?? "");
177
+ if (!text.trim() || isPrototypeSingleFencedCodeBlock(text))
178
+ return false;
179
+ if (/^diff --git\s/m.test(text))
180
+ return true;
181
+ if (/^@@\s.+\s@@/m.test(text) && /^---\s/m.test(text) && /^\+\+\+\s/m.test(text))
182
+ return true;
183
+ return false;
184
+ }
185
+ export function inferPrototypePdfLanguage(markdown, editorLanguage) {
186
+ const normalizedEditorLanguage = normalizePrototypeEditorLanguage(editorLanguage);
187
+ if (normalizedEditorLanguage)
188
+ return normalizedEditorLanguage;
189
+ const fenced = parsePrototypeSingleFencedCodeBlock(markdown);
190
+ if (fenced) {
191
+ const fencedLanguage = normalizePrototypeEditorLanguage(fenced.info.split(/\s+/)[0] ?? "");
192
+ if (fencedLanguage)
193
+ return fencedLanguage;
194
+ }
195
+ if (isLikelyRawPrototypeGitDiff(markdown))
196
+ return "diff";
197
+ return undefined;
198
+ }
199
+ function escapePrototypePdfLatexText(text) {
200
+ return String(text ?? "")
201
+ .replace(/\r\n/g, "\n")
202
+ .replace(/\s*\n\s*/g, " ")
203
+ .trim()
204
+ .replace(/\\/g, "\\textbackslash{}")
205
+ .replace(/([{}%#$&_])/g, "\\$1")
206
+ .replace(/~/g, "\\textasciitilde{}")
207
+ .replace(/\^/g, "\\textasciicircum{}")
208
+ .replace(/\s{2,}/g, " ");
209
+ }
210
+ function replacePrototypeAnnotationMarkersForPdfInSegment(text) {
211
+ return String(text ?? "")
212
+ .replace(/\[an:\s*([^\]]+?)\]/gi, (_match, markerText) => {
213
+ const cleaned = escapePrototypePdfLatexText(markerText);
214
+ if (!cleaned)
215
+ return "";
216
+ return `\\studioannotation{${cleaned}}`;
217
+ })
218
+ .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText) => {
219
+ const cleaned = escapePrototypePdfLatexText(markerText);
220
+ if (!cleaned)
221
+ return "";
222
+ return `\\studioannotation{${cleaned}}`;
223
+ });
224
+ }
225
+ function replacePrototypeAnnotationMarkersForPdf(markdown) {
226
+ const lines = String(markdown ?? "").split("\n");
227
+ const out = [];
228
+ let plainBuffer = [];
229
+ let inFence = false;
230
+ let fenceChar;
231
+ let fenceLength = 0;
232
+ const flushPlain = () => {
233
+ if (plainBuffer.length === 0)
234
+ return;
235
+ out.push(replacePrototypeAnnotationMarkersForPdfInSegment(plainBuffer.join("\n")));
236
+ plainBuffer = [];
237
+ };
238
+ for (const line of lines) {
239
+ const trimmed = line.trimStart();
240
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
241
+ if (fenceMatch) {
242
+ const marker = fenceMatch[1];
243
+ const markerChar = marker[0];
244
+ const markerLength = marker.length;
245
+ if (!inFence) {
246
+ flushPlain();
247
+ inFence = true;
248
+ fenceChar = markerChar;
249
+ fenceLength = markerLength;
250
+ out.push(line);
251
+ continue;
252
+ }
253
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
254
+ inFence = false;
255
+ fenceChar = undefined;
256
+ fenceLength = 0;
257
+ }
258
+ out.push(line);
259
+ continue;
260
+ }
261
+ if (inFence) {
262
+ out.push(line);
263
+ }
264
+ else {
265
+ plainBuffer.push(line);
266
+ }
267
+ }
268
+ flushPlain();
269
+ return out.join("\n");
270
+ }
271
+ function parsePrototypeFencedDivOpenLine(line) {
272
+ const trimmed = String(line ?? "").trim();
273
+ const match = trimmed.match(/^(:{3,})(.+)$/);
274
+ if (!match)
275
+ return null;
276
+ const info = String(match[2] ?? "").trim();
277
+ if (!info)
278
+ return null;
279
+ return {
280
+ markerLength: match[1].length,
281
+ info,
282
+ };
283
+ }
284
+ function parsePrototypePdfCalloutStartLine(line) {
285
+ const open = parsePrototypeFencedDivOpenLine(line);
286
+ if (!open)
287
+ return null;
288
+ const kindMatch = open.info.match(/(?:^|[\s{])\.callout-(note|tip|warning|important|caution)(?=[\s}]|$)/i);
289
+ if (!kindMatch)
290
+ return null;
291
+ return {
292
+ markerLength: open.markerLength,
293
+ kind: kindMatch[1].toLowerCase(),
294
+ };
295
+ }
296
+ function preprocessPrototypeMarkdownCalloutsForPdf(markdown) {
297
+ const lines = String(markdown ?? "").split("\n");
298
+ const out = [];
299
+ const blocks = [];
300
+ let inFence = false;
301
+ let fenceChar;
302
+ let fenceLength = 0;
303
+ let markerId = 0;
304
+ for (let i = 0; i < lines.length; i += 1) {
305
+ const line = lines[i] ?? "";
306
+ const trimmed = line.trimStart();
307
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
308
+ if (fenceMatch) {
309
+ const marker = fenceMatch[1];
310
+ const markerChar = marker[0];
311
+ const markerLength = marker.length;
312
+ if (!inFence) {
313
+ inFence = true;
314
+ fenceChar = markerChar;
315
+ fenceLength = markerLength;
316
+ out.push(line);
317
+ continue;
318
+ }
319
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
320
+ inFence = false;
321
+ fenceChar = undefined;
322
+ fenceLength = 0;
323
+ }
324
+ out.push(line);
325
+ continue;
326
+ }
327
+ if (inFence) {
328
+ out.push(line);
329
+ continue;
330
+ }
331
+ const calloutStart = parsePrototypePdfCalloutStartLine(line);
332
+ if (!calloutStart) {
333
+ out.push(line);
334
+ continue;
335
+ }
336
+ const contentLines = [];
337
+ let innerInFence = false;
338
+ let innerFenceChar;
339
+ let innerFenceLength = 0;
340
+ let nestedDivDepth = 0;
341
+ let closed = false;
342
+ let j = i + 1;
343
+ for (; j < lines.length; j += 1) {
344
+ const innerLine = lines[j] ?? "";
345
+ const innerTrimmed = innerLine.trimStart();
346
+ const innerFenceMatch = innerTrimmed.match(/^(`{3,}|~{3,})/);
347
+ if (innerFenceMatch) {
348
+ const marker = innerFenceMatch[1];
349
+ const markerChar = marker[0];
350
+ const markerLength = marker.length;
351
+ if (!innerInFence) {
352
+ innerInFence = true;
353
+ innerFenceChar = markerChar;
354
+ innerFenceLength = markerLength;
355
+ contentLines.push(innerLine);
356
+ continue;
357
+ }
358
+ if (innerFenceChar === markerChar && markerLength >= innerFenceLength) {
359
+ innerInFence = false;
360
+ innerFenceChar = undefined;
361
+ innerFenceLength = 0;
362
+ }
363
+ contentLines.push(innerLine);
364
+ continue;
365
+ }
366
+ if (!innerInFence) {
367
+ const nestedOpen = parsePrototypeFencedDivOpenLine(innerLine);
368
+ if (nestedOpen) {
369
+ nestedDivDepth += 1;
370
+ contentLines.push(innerLine);
371
+ continue;
372
+ }
373
+ if (/^:{3,}\s*$/.test(innerLine.trim())) {
374
+ if (nestedDivDepth > 0) {
375
+ nestedDivDepth -= 1;
376
+ contentLines.push(innerLine);
377
+ continue;
378
+ }
379
+ closed = true;
380
+ break;
381
+ }
382
+ }
383
+ contentLines.push(innerLine);
384
+ }
385
+ if (!closed) {
386
+ out.push(line);
387
+ out.push(...contentLines);
388
+ i = j - 1;
389
+ continue;
390
+ }
391
+ const block = {
392
+ kind: calloutStart.kind,
393
+ markerId: markerId += 1,
394
+ content: contentLines.join("\n").trim(),
395
+ };
396
+ blocks.push(block);
397
+ out.push(`PISTUDIOPDFCALLOUTSTART${block.kind.toUpperCase()}${block.markerId}`);
398
+ if (block.content)
399
+ out.push(block.content);
400
+ out.push(`PISTUDIOPDFCALLOUTEND${block.kind.toUpperCase()}${block.markerId}`);
401
+ i = j;
402
+ }
403
+ return { markdown: out.join("\n"), blocks };
404
+ }
405
+ function preprocessPrototypeMarkdownImageAlignmentForPdf(markdown) {
406
+ const lines = String(markdown ?? "").split("\n");
407
+ const out = [];
408
+ const blocks = [];
409
+ let inFence = false;
410
+ let fenceChar;
411
+ let fenceLength = 0;
412
+ let markerId = 0;
413
+ for (const line of lines) {
414
+ const trimmed = line.trimStart();
415
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
416
+ if (fenceMatch) {
417
+ const marker = fenceMatch[1];
418
+ const markerChar = marker[0];
419
+ const markerLength = marker.length;
420
+ if (!inFence) {
421
+ inFence = true;
422
+ fenceChar = markerChar;
423
+ fenceLength = markerLength;
424
+ out.push(line);
425
+ continue;
426
+ }
427
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
428
+ inFence = false;
429
+ fenceChar = undefined;
430
+ fenceLength = 0;
431
+ }
432
+ out.push(line);
433
+ continue;
434
+ }
435
+ if (inFence) {
436
+ out.push(line);
437
+ continue;
438
+ }
439
+ const imageMatch = line.trim().match(/^!\[[^\]]*\]\((?:<[^>]+>|[^)]+)\)(\{[^}]*\})\s*$/);
440
+ if (!imageMatch) {
441
+ out.push(line);
442
+ continue;
443
+ }
444
+ const attrs = imageMatch[1] ?? "";
445
+ const alignMatch = attrs.match(/(?:^|\s)fig-align\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s}]+))/i);
446
+ const alignValue = String(alignMatch?.[1] ?? alignMatch?.[2] ?? alignMatch?.[3] ?? "").trim().toLowerCase();
447
+ if (alignValue !== "center" && alignValue !== "right") {
448
+ out.push(line);
449
+ continue;
450
+ }
451
+ const block = {
452
+ align: alignValue,
453
+ markerId: markerId += 1,
454
+ };
455
+ blocks.push(block);
456
+ out.push(`PISTUDIOPDFALIGNSTART${block.align.toUpperCase()}${block.markerId}`);
457
+ out.push(line);
458
+ out.push(`PISTUDIOPDFALIGNEND${block.align.toUpperCase()}${block.markerId}`);
459
+ }
460
+ return { markdown: out.join("\n"), blocks };
461
+ }
462
+ function replacePrototypePdfCalloutBlocksInGeneratedLatex(latex, blocks) {
463
+ if (blocks.length === 0)
464
+ return latex;
465
+ let transformed = String(latex ?? "");
466
+ for (const block of blocks) {
467
+ const startMarker = `PISTUDIOPDFCALLOUTSTART${block.kind.toUpperCase()}${block.markerId}`;
468
+ const endMarker = `PISTUDIOPDFCALLOUTEND${block.kind.toUpperCase()}${block.markerId}`;
469
+ const startIndex = transformed.indexOf(startMarker);
470
+ if (startIndex < 0)
471
+ continue;
472
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
473
+ if (endIndex < 0)
474
+ continue;
475
+ const inner = transformed.slice(startIndex + startMarker.length, endIndex).trim();
476
+ const label = block.kind === "note"
477
+ ? "Note"
478
+ : block.kind === "tip"
479
+ ? "Tip"
480
+ : block.kind === "warning"
481
+ ? "Warning"
482
+ : block.kind === "important"
483
+ ? "Important"
484
+ : "Caution";
485
+ const replacement = `\\begin{studiocallout}{${label}}\n${inner}\n\\end{studiocallout}`;
486
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
487
+ }
488
+ return transformed;
489
+ }
490
+ function replacePrototypePdfAlignedImageBlocksInGeneratedLatex(latex, blocks) {
491
+ if (blocks.length === 0)
492
+ return latex;
493
+ let transformed = String(latex ?? "");
494
+ for (const block of blocks) {
495
+ const startMarker = `PISTUDIOPDFALIGNSTART${block.align.toUpperCase()}${block.markerId}`;
496
+ const endMarker = `PISTUDIOPDFALIGNEND${block.align.toUpperCase()}${block.markerId}`;
497
+ const startIndex = transformed.indexOf(startMarker);
498
+ if (startIndex < 0)
499
+ continue;
500
+ const endIndex = transformed.indexOf(endMarker, startIndex + startMarker.length);
501
+ if (endIndex < 0)
502
+ continue;
503
+ const inner = transformed.slice(startIndex + startMarker.length, endIndex).trim();
504
+ const env = block.align === "right" ? "flushright" : "center";
505
+ const replacement = `\\begin{${env}}\n${inner}\n\\end{${env}}`;
506
+ transformed = transformed.slice(0, startIndex) + replacement + transformed.slice(endIndex + endMarker.length);
507
+ }
508
+ return transformed;
509
+ }
510
+ function stripPrototypeMarkdownHtmlCommentsInSegment(markdown) {
511
+ const source = String(markdown ?? "");
512
+ let out = "";
513
+ let i = 0;
514
+ let codeSpanFenceLength = 0;
515
+ let inHtmlComment = false;
516
+ while (i < source.length) {
517
+ if (inHtmlComment) {
518
+ if (source.startsWith("-->", i)) {
519
+ inHtmlComment = false;
520
+ i += 3;
521
+ continue;
522
+ }
523
+ const ch = source[i];
524
+ if (ch === "\n" || ch === "\r")
525
+ out += ch;
526
+ i += 1;
527
+ continue;
528
+ }
529
+ if (codeSpanFenceLength > 0) {
530
+ const fence = "`".repeat(codeSpanFenceLength);
531
+ if (source.startsWith(fence, i)) {
532
+ out += fence;
533
+ i += codeSpanFenceLength;
534
+ codeSpanFenceLength = 0;
535
+ continue;
536
+ }
537
+ out += source[i];
538
+ i += 1;
539
+ continue;
540
+ }
541
+ const backtickMatch = source.slice(i).match(/^`+/);
542
+ if (backtickMatch) {
543
+ const fence = backtickMatch[0];
544
+ codeSpanFenceLength = fence.length;
545
+ out += fence;
546
+ i += fence.length;
547
+ continue;
548
+ }
549
+ if (source.startsWith("<!--", i)) {
550
+ inHtmlComment = true;
551
+ i += 4;
552
+ continue;
553
+ }
554
+ out += source[i];
555
+ i += 1;
556
+ }
557
+ return out;
558
+ }
559
+ function stripPrototypeMarkdownHtmlComments(markdown) {
560
+ const lines = String(markdown ?? "").split("\n");
561
+ const out = [];
562
+ let plainBuffer = [];
563
+ let inFence = false;
564
+ let fenceChar;
565
+ let fenceLength = 0;
566
+ const flushPlain = () => {
567
+ if (plainBuffer.length === 0)
568
+ return;
569
+ out.push(stripPrototypeMarkdownHtmlCommentsInSegment(plainBuffer.join("\n")));
570
+ plainBuffer = [];
571
+ };
572
+ for (const line of lines) {
573
+ const trimmed = line.trimStart();
574
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
575
+ if (fenceMatch) {
576
+ const marker = fenceMatch[1];
577
+ const markerChar = marker[0];
578
+ const markerLength = marker.length;
579
+ if (!inFence) {
580
+ flushPlain();
581
+ inFence = true;
582
+ fenceChar = markerChar;
583
+ fenceLength = markerLength;
584
+ out.push(line);
585
+ continue;
586
+ }
587
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
588
+ inFence = false;
589
+ fenceChar = undefined;
590
+ fenceLength = 0;
591
+ }
592
+ out.push(line);
593
+ continue;
594
+ }
595
+ if (inFence) {
596
+ out.push(line);
597
+ }
598
+ else {
599
+ plainBuffer.push(line);
600
+ }
601
+ }
602
+ flushPlain();
603
+ return out.join("\n");
604
+ }
605
+ function isLikelyMathExpression(expr) {
606
+ const content = expr.trim();
607
+ if (content.length === 0)
608
+ return false;
609
+ if (/\\[a-zA-Z]+/.test(content))
610
+ return true;
611
+ if (/[0-9]/.test(content))
612
+ return true;
613
+ if (/[=+\-*/^_<>≤≥±×÷]/u.test(content))
614
+ return true;
615
+ if (/[{}]/.test(content))
616
+ return true;
617
+ if (/[α-ωΑ-Ω]/u.test(content))
618
+ return true;
619
+ if (/^[A-Za-z]$/.test(content))
620
+ return true;
621
+ if (/^[A-Za-z][A-Za-z\s'".,:;!?-]*[A-Za-z]$/.test(content))
622
+ return false;
623
+ return false;
624
+ }
625
+ function collapseDisplayMathContent(expr) {
626
+ let content = expr.trim();
627
+ if (/\\begin\{[^}]+\}|\\end\{[^}]+\}/.test(content)) {
628
+ return content;
629
+ }
630
+ if (content.includes("\\\\") || content.includes("\n")) {
631
+ content = content.replace(/\\\\\s*/g, " ");
632
+ content = content.replace(/\s*\n\s*/g, " ");
633
+ content = content.replace(/\s{2,}/g, " ").trim();
634
+ }
635
+ return content;
636
+ }
637
+ function normalizeMathDelimitersInSegment(markdown) {
638
+ let normalized = markdown.replace(/\$\s*\\\(([\s\S]*?)\\\)\s*\$/g, (match, expr) => {
639
+ if (!isLikelyMathExpression(expr))
640
+ return match;
641
+ const content = expr.trim();
642
+ return content.length > 0 ? `\\(${content}\\)` : "\\(\\)";
643
+ });
644
+ normalized = normalized.replace(/\$\s*\\\[\s*([\s\S]*?)\s*\\\]\s*\$/g, (match, expr) => {
645
+ if (!isLikelyMathExpression(expr))
646
+ return match;
647
+ const content = collapseDisplayMathContent(expr);
648
+ return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
649
+ });
650
+ normalized = normalized.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (match, expr) => {
651
+ if (!isLikelyMathExpression(expr))
652
+ return `[${expr.trim()}]`;
653
+ const content = collapseDisplayMathContent(expr);
654
+ return content.length > 0 ? `\\[${content}\\]` : "\\[\\]";
655
+ });
656
+ normalized = normalized.replace(/\\\(([\s\S]*?)\\\)/g, (match, expr) => {
657
+ if (!isLikelyMathExpression(expr))
658
+ return `(${expr})`;
659
+ const content = expr.trim();
660
+ return content.length > 0 ? `\\(${content}\\)` : "\\(\\)";
661
+ });
662
+ return normalized;
663
+ }
664
+ function normalizeMathDelimiters(markdown) {
665
+ const lines = markdown.split("\n");
666
+ const out = [];
667
+ let plainBuffer = [];
668
+ let inFence = false;
669
+ let fenceChar;
670
+ let fenceLength = 0;
671
+ const flushPlain = () => {
672
+ if (plainBuffer.length === 0)
673
+ return;
674
+ out.push(normalizeMathDelimitersInSegment(plainBuffer.join("\n")));
675
+ plainBuffer = [];
676
+ };
677
+ for (const line of lines) {
678
+ const trimmed = line.trimStart();
679
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
680
+ if (fenceMatch) {
681
+ const marker = fenceMatch[1];
682
+ const markerChar = marker[0];
683
+ const markerLength = marker.length;
684
+ if (!inFence) {
685
+ flushPlain();
686
+ inFence = true;
687
+ fenceChar = markerChar;
688
+ fenceLength = markerLength;
689
+ out.push(line);
690
+ continue;
691
+ }
692
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
693
+ inFence = false;
694
+ fenceChar = undefined;
695
+ fenceLength = 0;
696
+ }
697
+ out.push(line);
698
+ continue;
699
+ }
700
+ if (inFence) {
701
+ out.push(line);
702
+ }
703
+ else {
704
+ plainBuffer.push(line);
705
+ }
706
+ }
707
+ flushPlain();
708
+ return out.join("\n");
709
+ }
710
+ function normalizeObsidianImages(markdown) {
711
+ return markdown
712
+ .replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g, (_m, path, alt) => `![${alt}](<${path}>)`)
713
+ .replace(/!\[\[([^\]]+)\]\]/g, (_m, path) => `![](<${path}>)`);
714
+ }
715
+ function buildPrototypePdfPreamble() {
716
+ return `\\usepackage{titlesec}
717
+ \\titleformat{\\section}{\\Large\\bfseries\\sffamily}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
718
+ \\titleformat{\\subsection}{\\large\\bfseries\\sffamily}{}{0pt}{}
719
+ \\titleformat{\\subsubsection}{\\normalsize\\bfseries\\sffamily}{}{0pt}{}
720
+ \\titlespacing*{\\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
721
+ \\titlespacing*{\\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.6ex plus 0.1ex}
722
+ \\usepackage{xcolor}
723
+ \\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
724
+ \\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
725
+ \\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
726
+ \\newcommand{\\studioannotation}[1]{\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}}\\endgroup}
727
+ \\newenvironment{studiocallout}[1]{\\par\\vspace{0.22em}\\noindent\\begingroup\\color{StudioAnnotationBorder}\\hrule height 0.45pt\\color{black}\\vspace{0.08em}\\noindent{\\sffamily\\bfseries\\textcolor{StudioAnnotationText}{#1}}\\par\\vspace{0.02em}\\leftskip=0.7em\\rightskip=0pt\\parindent=0pt\\parskip=0.15em}{\\par\\vspace{0.02em}\\noindent\\color{StudioAnnotationBorder}\\hrule height 0.45pt\\par\\endgroup\\vspace{0.22em}}
728
+ \\usepackage{caption}
729
+ \\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
730
+ \\usepackage{enumitem}
731
+ \\setlist[itemize]{nosep, leftmargin=1.5em}
732
+ \\setlist[enumerate]{nosep, leftmargin=1.5em}
733
+ \\usepackage{parskip}
734
+ \\usepackage{fvextra}
735
+ \\makeatletter
736
+ \\@ifundefined{Highlighting}{%
737
+ \\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere}%
738
+ }{%
739
+ \\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere}%
740
+ }
741
+ \\makeatother
742
+ `;
743
+ }
744
+ function buildPrototypePdfPandocVariableArgs(allowAltDocumentClass = false) {
745
+ const args = [];
746
+ if (allowAltDocumentClass) {
747
+ args.push("-V", "documentclass=article");
748
+ }
749
+ args.push("-V", "geometry:margin=2.2cm");
750
+ args.push("-V", "fontsize=11pt");
751
+ args.push("-V", "linestretch=1.25");
752
+ return args;
753
+ }
754
+ function preparePrototypePdfMarkdown(markdown, isLatex, editorLanguage) {
755
+ if (isLatex)
756
+ return markdown;
757
+ const effectiveEditorLanguage = inferPrototypePdfLanguage(markdown, editorLanguage);
758
+ const source = effectiveEditorLanguage && effectiveEditorLanguage !== "markdown" && effectiveEditorLanguage !== "latex"
759
+ && !isPrototypeSingleFencedCodeBlock(markdown)
760
+ ? wrapPrototypeCodeAsMarkdown(markdown, effectiveEditorLanguage)
761
+ : markdown;
762
+ const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
763
+ ? replacePrototypeAnnotationMarkersForPdf(source)
764
+ : source;
765
+ const commentStrippedSource = stripPrototypeMarkdownHtmlComments(annotationReadySource);
766
+ return normalizeObsidianImages(normalizeMathDelimiters(commentStrippedSource));
767
+ }
768
+ async function runPrototypePandocPdfExport(markdown, inputFormat, pandocCommand, pdfEngine, resourcePath, bibliographyArgs) {
769
+ const tempDir = join(tmpdir(), `pi-studio-opencode-pdf-${Date.now()}-${randomUUID()}`);
770
+ const preamblePath = join(tempDir, "_pdf_preamble.tex");
771
+ const outputPath = join(tempDir, "studio-export.pdf");
772
+ const pandocWorkingDir = await resolvePrototypePdfWorkingDir(resourcePath);
773
+ await mkdir(tempDir, { recursive: true });
774
+ await writeFile(preamblePath, buildPrototypePdfPreamble(), "utf8");
775
+ const args = [
776
+ "-f", inputFormat,
777
+ "-o", outputPath,
778
+ `--pdf-engine=${pdfEngine}`,
779
+ ...buildPrototypePdfPandocVariableArgs(inputFormat !== "latex"),
780
+ "-V", "urlcolor=blue",
781
+ "-V", "linkcolor=blue",
782
+ "--include-in-header", preamblePath,
783
+ ...bibliographyArgs,
784
+ ];
785
+ if (resourcePath)
786
+ args.push(`--resource-path=${resourcePath}`);
787
+ try {
788
+ await new Promise((resolvePromise, rejectPromise) => {
789
+ const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
790
+ const stderrChunks = [];
791
+ let settled = false;
792
+ const fail = (error) => {
793
+ if (settled)
794
+ return;
795
+ settled = true;
796
+ rejectPromise(error);
797
+ };
798
+ child.stderr.on("data", (chunk) => {
799
+ stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
800
+ });
801
+ child.once("error", (error) => {
802
+ const errno = error;
803
+ if (errno.code === "ENOENT") {
804
+ const commandHint = pandocCommand === "pandoc"
805
+ ? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
806
+ : `${pandocCommand} was not found. Check PANDOC_PATH.`;
807
+ fail(new Error(commandHint));
808
+ return;
809
+ }
810
+ fail(error);
811
+ });
812
+ child.once("close", (code) => {
813
+ if (settled)
814
+ return;
815
+ if (code === 0) {
816
+ settled = true;
817
+ resolvePromise();
818
+ return;
819
+ }
820
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
821
+ const hint = stderr.includes("not found") || stderr.includes("xelatex") || stderr.includes("pdflatex")
822
+ ? "\nPDF export requires a LaTeX engine. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE."
823
+ : "";
824
+ fail(new Error(`pandoc PDF export failed with exit code ${code}${stderr ? `: ${stderr}` : ""}${hint}`));
825
+ });
826
+ child.stdin.end(markdown);
827
+ });
828
+ return await readFile(outputPath);
829
+ }
830
+ finally {
831
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
832
+ }
833
+ }
834
+ async function renderPrototypePdfFromGeneratedLatex(markdown, inputFormat, pandocCommand, pdfEngine, resourcePath, bibliographyArgs, calloutBlocks, alignedImageBlocks) {
835
+ const tempDir = join(tmpdir(), `pi-studio-opencode-pdf-${Date.now()}-${randomUUID()}`);
836
+ const preamblePath = join(tempDir, "_pdf_preamble.tex");
837
+ const latexPath = join(tempDir, "studio-export.tex");
838
+ const outputPath = join(tempDir, "studio-export.pdf");
839
+ const pandocWorkingDir = await resolvePrototypePdfWorkingDir(resourcePath);
840
+ await mkdir(tempDir, { recursive: true });
841
+ await writeFile(preamblePath, buildPrototypePdfPreamble(), "utf8");
842
+ const pandocArgs = [
843
+ "-f", inputFormat,
844
+ "-t", "latex",
845
+ "-s",
846
+ "-o", latexPath,
847
+ ...buildPrototypePdfPandocVariableArgs(inputFormat !== "latex"),
848
+ "-V", "urlcolor=blue",
849
+ "-V", "linkcolor=blue",
850
+ "--include-in-header", preamblePath,
851
+ ...bibliographyArgs,
852
+ ];
853
+ if (resourcePath)
854
+ pandocArgs.push(`--resource-path=${resourcePath}`);
855
+ try {
856
+ await new Promise((resolvePromise, rejectPromise) => {
857
+ const child = spawn(pandocCommand, pandocArgs, { stdio: ["pipe", "pipe", "pipe"], cwd: pandocWorkingDir });
858
+ const stderrChunks = [];
859
+ let settled = false;
860
+ const fail = (error) => {
861
+ if (settled)
862
+ return;
863
+ settled = true;
864
+ rejectPromise(error);
865
+ };
866
+ child.stderr.on("data", (chunk) => {
867
+ stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
868
+ });
869
+ child.once("error", (error) => {
870
+ const errno = error;
871
+ if (errno.code === "ENOENT") {
872
+ const commandHint = pandocCommand === "pandoc"
873
+ ? "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary."
874
+ : `${pandocCommand} was not found. Check PANDOC_PATH.`;
875
+ fail(new Error(commandHint));
876
+ return;
877
+ }
878
+ fail(error);
879
+ });
880
+ child.once("close", (code) => {
881
+ if (settled)
882
+ return;
883
+ if (code === 0) {
884
+ settled = true;
885
+ resolvePromise();
886
+ return;
887
+ }
888
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
889
+ fail(new Error(`pandoc LaTeX generation failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
890
+ });
891
+ child.stdin.end(markdown);
892
+ });
893
+ const generatedLatex = await readFile(latexPath, "utf8");
894
+ const calloutReadyLatex = replacePrototypePdfCalloutBlocksInGeneratedLatex(generatedLatex, calloutBlocks);
895
+ const alignedReadyLatex = replacePrototypePdfAlignedImageBlocksInGeneratedLatex(calloutReadyLatex, alignedImageBlocks);
896
+ await writeFile(latexPath, alignedReadyLatex, "utf8");
897
+ await new Promise((resolvePromise, rejectPromise) => {
898
+ const child = spawn(pdfEngine, [
899
+ "-interaction=nonstopmode",
900
+ "-halt-on-error",
901
+ `-output-directory=${tempDir}`,
902
+ latexPath,
903
+ ], { stdio: ["ignore", "pipe", "pipe"], cwd: pandocWorkingDir });
904
+ const stdoutChunks = [];
905
+ const stderrChunks = [];
906
+ let settled = false;
907
+ const fail = (error) => {
908
+ if (settled)
909
+ return;
910
+ settled = true;
911
+ rejectPromise(error);
912
+ };
913
+ child.stdout.on("data", (chunk) => {
914
+ stdoutChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
915
+ });
916
+ child.stderr.on("data", (chunk) => {
917
+ stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
918
+ });
919
+ child.once("error", (error) => {
920
+ const errno = error;
921
+ if (errno.code === "ENOENT") {
922
+ fail(new Error(`${pdfEngine} was not found. Install TeX Live (e.g. brew install --cask mactex) or set PANDOC_PDF_ENGINE.`));
923
+ return;
924
+ }
925
+ fail(error);
926
+ });
927
+ child.once("close", (code) => {
928
+ if (settled)
929
+ return;
930
+ if (code === 0) {
931
+ settled = true;
932
+ resolvePromise();
933
+ return;
934
+ }
935
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
936
+ const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
937
+ const errorMatch = stdout.match(/^! .+$/m);
938
+ const hint = errorMatch ? `: ${errorMatch[0]}` : (stderr ? `: ${stderr}` : "");
939
+ fail(new Error(`${pdfEngine} PDF export failed with exit code ${code}${hint}`));
940
+ });
941
+ });
942
+ return await readFile(outputPath);
943
+ }
944
+ finally {
945
+ await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
946
+ }
947
+ }
948
+ export async function renderPrototypePdfWithPandoc(markdown, options = {}) {
949
+ const source = String(markdown ?? "");
950
+ const isLatex = options.isLatex === true;
951
+ const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
952
+ const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
953
+ const effectiveEditorLanguage = inferPrototypePdfLanguage(source, options.editorPdfLanguage);
954
+ const pdfCalloutTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
955
+ ? preprocessPrototypeMarkdownCalloutsForPdf(source)
956
+ : { markdown: source, blocks: [] };
957
+ const pdfAlignedImageTransform = !isLatex && (!effectiveEditorLanguage || effectiveEditorLanguage === "markdown")
958
+ ? preprocessPrototypeMarkdownImageAlignmentForPdf(pdfCalloutTransform.markdown)
959
+ : { markdown: pdfCalloutTransform.markdown, blocks: [] };
960
+ const bibliographyArgs = await buildPrototypePandocBibliographyArgs(source, isLatex, options.resourcePath);
961
+ const inputFormat = isLatex
962
+ ? "latex"
963
+ : "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash+autolink_bare_uris+superscript+subscript-raw_html";
964
+ const normalizedMarkdown = preparePrototypePdfMarkdown(pdfAlignedImageTransform.markdown, isLatex, effectiveEditorLanguage);
965
+ if (!isLatex && (pdfCalloutTransform.blocks.length > 0 || pdfAlignedImageTransform.blocks.length > 0)) {
966
+ return {
967
+ pdf: await renderPrototypePdfFromGeneratedLatex(normalizedMarkdown, inputFormat, pandocCommand, pdfEngine, options.resourcePath, bibliographyArgs, pdfCalloutTransform.blocks, pdfAlignedImageTransform.blocks),
968
+ };
969
+ }
970
+ return {
971
+ pdf: await runPrototypePandocPdfExport(normalizedMarkdown, inputFormat, pandocCommand, pdfEngine, options.resourcePath, bibliographyArgs),
972
+ };
973
+ }
974
+ export function sanitizePrototypePdfFilename(input) {
975
+ const fallback = "studio-preview.pdf";
976
+ const raw = String(input ?? "").trim();
977
+ if (!raw)
978
+ return fallback;
979
+ const noPath = raw.split(/[\\/]/).pop() ?? raw;
980
+ const cleaned = noPath
981
+ .replace(/[\x00-\x1f\x7f]+/g, "")
982
+ .replace(/[<>:\"|?*]+/g, "-")
983
+ .trim();
984
+ if (!cleaned)
985
+ return fallback;
986
+ const ensuredExt = cleaned.toLowerCase().endsWith(".pdf") ? cleaned : `${cleaned}.pdf`;
987
+ if (ensuredExt.length <= 160)
988
+ return ensuredExt;
989
+ return `${ensuredExt.slice(0, 156)}.pdf`;
990
+ }
991
+ //# sourceMappingURL=prototype-pdf.js.map