pi-studio 0.5.35 → 0.5.36

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.
@@ -241,8 +241,11 @@
241
241
  let responseHighlightEnabled = false;
242
242
  let editorHighlightRenderRaf = null;
243
243
  let annotationsEnabled = true;
244
- const ANNOTATION_MARKER_REGEX = /\[an:\s*([^\]]+?)\]/gi;
245
244
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
245
+ const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
246
+ if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
247
+ throw new Error("Studio annotation helpers failed to load.");
248
+ }
246
249
  const EMPTY_OVERLAY_LINE = "\u200b";
247
250
  const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
248
251
  const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
@@ -1163,15 +1166,11 @@
1163
1166
  }
1164
1167
 
1165
1168
  function hasAnnotationMarkers(text) {
1166
- const source = String(text || "");
1167
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
1168
- const hasMarker = ANNOTATION_MARKER_REGEX.test(source);
1169
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
1170
- return hasMarker;
1169
+ return annotationHelpers.hasAnnotationMarkers(text);
1171
1170
  }
1172
1171
 
1173
1172
  function stripAnnotationMarkers(text) {
1174
- return String(text || "").replace(ANNOTATION_MARKER_REGEX, "");
1173
+ return annotationHelpers.stripAnnotationMarkers(text);
1175
1174
  }
1176
1175
 
1177
1176
  function prepareEditorTextForSend(text) {
@@ -1184,78 +1183,8 @@
1184
1183
  return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
1185
1184
  }
1186
1185
 
1187
- function normalizePreviewAnnotationLabel(text) {
1188
- return String(text || "")
1189
- .replace(/\r\n/g, "\n")
1190
- .replace(/\s*\n\s*/g, " ")
1191
- .replace(/\s{2,}/g, " ")
1192
- .trim();
1193
- }
1194
-
1195
1186
  function prepareMarkdownForPandocPreview(markdown) {
1196
- const source = String(markdown || "").replace(/\r\n/g, "\n");
1197
- const placeholders = [];
1198
- if (!source) {
1199
- return { markdown: source, placeholders: placeholders };
1200
- }
1201
-
1202
- const lines = source.split("\n");
1203
- const out = [];
1204
- let plainBuffer = [];
1205
- let inFence = false;
1206
- let fenceChar = null;
1207
- let fenceLength = 0;
1208
-
1209
- function flushPlain() {
1210
- if (plainBuffer.length === 0) return;
1211
- const segment = plainBuffer.join("\n").replace(/\[an:\s*([^\]]+?)\]/gi, function(_match, markerText) {
1212
- const label = normalizePreviewAnnotationLabel(markerText);
1213
- if (!label) return "";
1214
- const token = PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX + placeholders.length + "TOKEN";
1215
- placeholders.push({ token: token, text: label, title: "[an: " + label + "]" });
1216
- return token;
1217
- });
1218
- out.push(segment);
1219
- plainBuffer = [];
1220
- }
1221
-
1222
- for (const line of lines) {
1223
- const trimmed = line.trimStart();
1224
- const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
1225
-
1226
- if (fenceMatch) {
1227
- const marker = fenceMatch[1] || "";
1228
- const markerChar = marker.charAt(0);
1229
- const markerLength = marker.length;
1230
-
1231
- if (!inFence) {
1232
- flushPlain();
1233
- inFence = true;
1234
- fenceChar = markerChar;
1235
- fenceLength = markerLength;
1236
- out.push(line);
1237
- continue;
1238
- }
1239
-
1240
- if (fenceChar === markerChar && markerLength >= fenceLength) {
1241
- inFence = false;
1242
- fenceChar = null;
1243
- fenceLength = 0;
1244
- }
1245
-
1246
- out.push(line);
1247
- continue;
1248
- }
1249
-
1250
- if (inFence) {
1251
- out.push(line);
1252
- } else {
1253
- plainBuffer.push(line);
1254
- }
1255
- }
1256
-
1257
- flushPlain();
1258
- return { markdown: out.join("\n"), placeholders: placeholders };
1187
+ return annotationHelpers.prepareMarkdownForPandocPreview(markdown, PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX);
1259
1188
  }
1260
1189
 
1261
1190
  function wrapAsFencedCodeBlock(text, language) {
@@ -1774,8 +1703,9 @@
1774
1703
  if (entry) {
1775
1704
  const markerEl = document.createElement("span");
1776
1705
  markerEl.className = "annotation-preview-marker";
1777
- markerEl.textContent = typeof entry.text === "string" ? entry.text : token;
1778
- markerEl.title = typeof entry.title === "string" ? entry.title : markerEl.textContent;
1706
+ const markerText = typeof entry.text === "string" ? entry.text : token;
1707
+ markerEl.title = typeof entry.title === "string" ? entry.title : markerText;
1708
+ setAnnotationPreviewMarkerContent(markerEl, markerText);
1779
1709
  fragment.appendChild(markerEl);
1780
1710
  } else {
1781
1711
  fragment.appendChild(document.createTextNode(token));
@@ -1819,33 +1749,28 @@
1819
1749
  for (const textNode of textNodes) {
1820
1750
  const text = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
1821
1751
  if (!text) continue;
1822
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
1823
- if (!ANNOTATION_MARKER_REGEX.test(text)) continue;
1824
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
1752
+ const markers = annotationHelpers.collectInlineAnnotationMarkers(text);
1753
+ if (markers.length === 0) continue;
1825
1754
 
1826
1755
  const fragment = document.createDocumentFragment();
1827
1756
  let lastIndex = 0;
1828
- let match;
1829
- while ((match = ANNOTATION_MARKER_REGEX.exec(text)) !== null) {
1830
- const token = match[0] || "";
1831
- const start = typeof match.index === "number" ? match.index : 0;
1832
- if (start > lastIndex) {
1833
- fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
1757
+ markers.forEach(function(marker) {
1758
+ const token = marker.raw || "";
1759
+ if (marker.start > lastIndex) {
1760
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, marker.start)));
1834
1761
  }
1835
1762
 
1836
1763
  if (mode === "highlight") {
1837
1764
  const markerEl = document.createElement("span");
1838
1765
  markerEl.className = "annotation-preview-marker";
1839
- markerEl.textContent = typeof match[1] === "string" ? match[1].trim() : token;
1766
+ const markerText = annotationHelpers.normalizePreviewAnnotationLabel(marker.body) || token;
1840
1767
  markerEl.title = token;
1768
+ setAnnotationPreviewMarkerContent(markerEl, markerText);
1841
1769
  fragment.appendChild(markerEl);
1842
1770
  }
1843
1771
 
1844
- lastIndex = start + token.length;
1845
- if (token.length === 0) {
1846
- ANNOTATION_MARKER_REGEX.lastIndex += 1;
1847
- }
1848
- }
1772
+ lastIndex = marker.end;
1773
+ });
1849
1774
 
1850
1775
  if (lastIndex < text.length) {
1851
1776
  fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
@@ -2732,50 +2657,44 @@
2732
2657
  return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
2733
2658
  }
2734
2659
 
2735
- function wrapHighlightWithTitle(className, text, title) {
2660
+ function buildAnnotationPreviewMarkerHtml(text, title) {
2736
2661
  const titleAttr = title ? " title='" + escapeHtml(String(title)) + "'" : "";
2737
- return "<span class='" + className + "'" + titleAttr + ">" + escapeHtml(String(text || "")) + "</span>";
2662
+ const rendered = typeof annotationHelpers.renderPreviewAnnotationHtml === "function"
2663
+ ? annotationHelpers.renderPreviewAnnotationHtml(text)
2664
+ : escapeHtml(String(text || ""));
2665
+ return "<span class='annotation-preview-marker'" + titleAttr + ">" + rendered + "</span>";
2666
+ }
2667
+
2668
+ function setAnnotationPreviewMarkerContent(markerEl, text) {
2669
+ if (!markerEl) return;
2670
+ const rendered = typeof annotationHelpers.renderPreviewAnnotationHtml === "function"
2671
+ ? annotationHelpers.renderPreviewAnnotationHtml(text)
2672
+ : escapeHtml(String(text || ""));
2673
+ markerEl.innerHTML = rendered;
2738
2674
  }
2739
2675
 
2740
2676
  function highlightInlineAnnotations(text, mode) {
2741
2677
  const source = String(text || "");
2742
2678
  const renderMode = mode === "preview" ? "preview" : "overlay";
2743
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
2744
- let lastIndex = 0;
2745
- let out = "";
2746
-
2747
- let match;
2748
- while ((match = ANNOTATION_MARKER_REGEX.exec(source)) !== null) {
2749
- const token = match[0] || "";
2750
- const start = typeof match.index === "number" ? match.index : 0;
2751
- const markerText = typeof match[1] === "string" ? match[1].trim() : token;
2752
-
2753
- if (start > lastIndex) {
2754
- out += escapeHtml(source.slice(lastIndex, start));
2755
- }
2756
-
2757
- if (renderMode === "preview") {
2758
- out += wrapHighlightWithTitle("annotation-preview-marker", markerText || token, token);
2759
- } else {
2760
- out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
2761
- }
2762
- lastIndex = start + token.length;
2763
- if (token.length === 0) {
2764
- ANNOTATION_MARKER_REGEX.lastIndex += 1;
2765
- }
2766
- }
2767
-
2768
- ANNOTATION_MARKER_REGEX.lastIndex = 0;
2769
- if (lastIndex < source.length) {
2770
- out += escapeHtml(source.slice(lastIndex));
2771
- }
2772
-
2773
- return out;
2679
+ return annotationHelpers.replaceInlineAnnotationMarkers(
2680
+ source,
2681
+ function(marker) {
2682
+ const token = marker.raw || "";
2683
+ const markerText = annotationHelpers.normalizePreviewAnnotationLabel(marker.body) || token;
2684
+ if (renderMode === "preview") {
2685
+ return buildAnnotationPreviewMarkerHtml(markerText, token);
2686
+ }
2687
+ return wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
2688
+ },
2689
+ function(segment) {
2690
+ return escapeHtml(segment);
2691
+ },
2692
+ );
2774
2693
  }
2775
2694
 
2776
- function highlightInlineMarkdown(text) {
2695
+ function highlightInlineMarkdownWithoutAnnotations(text) {
2777
2696
  const source = String(text || "");
2778
- const pattern = /(\x60[^\x60]*\x60)|(\[[^\]]+\]\([^)]+\))|(\[an:\s*[^\]]+\])/gi;
2697
+ const pattern = /(\x60[^\x60]*\x60)|(\[[^\]]+\]\([^)]+\))/g;
2779
2698
  let lastIndex = 0;
2780
2699
  let out = "";
2781
2700
 
@@ -2798,8 +2717,6 @@
2798
2717
  } else {
2799
2718
  out += escapeHtml(token);
2800
2719
  }
2801
- } else if (match[3]) {
2802
- out += highlightInlineAnnotations(token);
2803
2720
  } else {
2804
2721
  out += escapeHtml(token);
2805
2722
  }
@@ -2814,6 +2731,18 @@
2814
2731
  return out;
2815
2732
  }
2816
2733
 
2734
+ function highlightInlineMarkdown(text) {
2735
+ return annotationHelpers.replaceInlineAnnotationMarkers(
2736
+ String(text || ""),
2737
+ function(marker) {
2738
+ return highlightInlineAnnotations(marker.raw || "");
2739
+ },
2740
+ function(segment) {
2741
+ return highlightInlineMarkdownWithoutAnnotations(segment);
2742
+ },
2743
+ );
2744
+ }
2745
+
2817
2746
  function normalizeFenceLanguage(info) {
2818
2747
  const raw = String(info || "").trim();
2819
2748
  if (!raw) return "";
package/client/studio.css CHANGED
@@ -523,6 +523,14 @@
523
523
  vertical-align: baseline;
524
524
  }
525
525
 
526
+ .annotation-preview-marker code {
527
+ font-family: var(--mono-font, ui-monospace, SFMono-Regular, Menlo, monospace);
528
+ font-size: 0.95em;
529
+ background: rgba(0, 0, 0, 0.08);
530
+ border-radius: 3px;
531
+ padding: 0 0.2em;
532
+ }
533
+
526
534
  #sourcePreview {
527
535
  flex: 1 1 auto;
528
536
  min-height: 0;
package/index.ts CHANGED
@@ -8,6 +8,16 @@ import { homedir, tmpdir } from "node:os";
8
8
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import { URL, pathToFileURL } from "node:url";
10
10
  import { WebSocketServer, WebSocket, type RawData } from "ws";
11
+ import {
12
+ collectStudioInlineAnnotationMarkers,
13
+ hasStudioMarkdownAnnotationMarkers,
14
+ isStudioAnnotationWordChar,
15
+ normalizeStudioAnnotationText,
16
+ readStudioAnnotationProtectedTokenAt,
17
+ replaceStudioInlineAnnotationMarkers,
18
+ transformStudioMarkdownOutsideFences,
19
+ } from "./shared/studio-annotation-scanner.js";
20
+ import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
11
21
 
12
22
  type Lens = "writing" | "code";
13
23
  type RequestedLens = Lens | "auto";
@@ -18,6 +28,7 @@ type StudioPromptMode = "response" | "run" | "effective";
18
28
  type StudioPromptTriggerKind = "run" | "steer";
19
29
 
20
30
  const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
31
+ const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
21
32
  const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
22
33
 
23
34
  interface StudioServerState {
@@ -3023,14 +3034,6 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
3023
3034
  return undefined;
3024
3035
  }
3025
3036
 
3026
- function escapeStudioPdfLatexTextFragment(text: string): string {
3027
- return String(text ?? "")
3028
- .replace(/\\/g, "\\textbackslash{}")
3029
- .replace(/([{}%#$&_])/g, "\\$1")
3030
- .replace(/~/g, "\\textasciitilde{}")
3031
- .replace(/\^/g, "\\textasciicircum{}");
3032
- }
3033
-
3034
3037
  function escapeStudioPdfLatexText(text: string): string {
3035
3038
  const normalized = String(text ?? "")
3036
3039
  .replace(/\r\n/g, "\n")
@@ -3085,71 +3088,158 @@ function escapeStudioPdfLatexText(text: string): string {
3085
3088
  return out.trim();
3086
3089
  }
3087
3090
 
3088
- function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3089
- return String(text ?? "")
3090
- .replace(/\[an:\s*([^\]]+?)\]/gi, (_match, markerText: string) => {
3091
- const cleaned = escapeStudioPdfLatexText(markerText);
3092
- if (!cleaned) return "";
3093
- return `\\studioannotation{${cleaned}}`;
3094
- })
3095
- .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
3096
- const cleaned = escapeStudioPdfLatexText(markerText);
3097
- if (!cleaned) return "";
3098
- return `\\studioannotation{${cleaned}}`;
3099
- });
3091
+ function renderStudioAnnotationCodeSpanPdfLatex(rawToken: string): string {
3092
+ const raw = String(rawToken ?? "");
3093
+ if (!raw || raw[0] !== "`") return escapeStudioPdfLatexTextFragment(raw);
3094
+
3095
+ let fenceLength = 1;
3096
+ while (raw[fenceLength] === "`") fenceLength += 1;
3097
+ const fence = "`".repeat(fenceLength);
3098
+ if (raw.length < fenceLength * 2 || raw.slice(raw.length - fenceLength) !== fence) {
3099
+ return escapeStudioPdfLatexTextFragment(raw);
3100
+ }
3101
+
3102
+ return `\\texttt{${escapeStudioPdfLatexTextFragment(raw.slice(fenceLength, raw.length - fenceLength))}}`;
3100
3103
  }
3101
3104
 
3102
- function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
3103
- const lines = String(markdown ?? "").split("\n");
3104
- const out: string[] = [];
3105
- let plainBuffer: string[] = [];
3106
- let inFence = false;
3107
- let fenceChar: "`" | "~" | undefined;
3108
- let fenceLength = 0;
3105
+ function canOpenStudioAnnotationEmphasisDelimiter(source: string, startIndex: number, delimiter: string): boolean {
3106
+ if (source.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
3107
+ const prev = startIndex > 0 ? source[startIndex - 1] ?? "" : "";
3108
+ const next = source[startIndex + delimiter.length] ?? "";
3109
+ if (!next || /\s/.test(next)) return false;
3110
+ return !isStudioAnnotationWordChar(prev);
3111
+ }
3109
3112
 
3110
- const flushPlain = () => {
3111
- if (plainBuffer.length === 0) return;
3112
- out.push(replaceStudioAnnotationMarkersForPdfInSegment(plainBuffer.join("\n")));
3113
- plainBuffer = [];
3114
- };
3113
+ function canCloseStudioAnnotationEmphasisDelimiter(source: string, startIndex: number, delimiter: string): boolean {
3114
+ if (source.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
3115
+ const prev = startIndex > 0 ? source[startIndex - 1] ?? "" : "";
3116
+ const next = source[startIndex + delimiter.length] ?? "";
3117
+ if (!prev || /\s/.test(prev)) return false;
3118
+ return !isStudioAnnotationWordChar(next);
3119
+ }
3115
3120
 
3116
- for (const line of lines) {
3117
- const trimmed = line.trimStart();
3118
- const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
3121
+ function renderStudioAnnotationPdfLatexContent(text: string): string {
3122
+ const source = String(text ?? "");
3123
+ let out = "";
3124
+ let plainStart = 0;
3125
+ let index = 0;
3119
3126
 
3120
- if (fenceMatch) {
3121
- const marker = fenceMatch[1]!;
3122
- const markerChar = marker[0] as "`" | "~";
3123
- const markerLength = marker.length;
3127
+ while (index < source.length) {
3128
+ const token = readStudioAnnotationProtectedTokenAt(source, index);
3129
+ if (!token) {
3130
+ index += 1;
3131
+ continue;
3132
+ }
3124
3133
 
3125
- if (!inFence) {
3126
- flushPlain();
3127
- inFence = true;
3128
- fenceChar = markerChar;
3129
- fenceLength = markerLength;
3130
- out.push(line);
3131
- continue;
3132
- }
3134
+ if (index > plainStart) {
3135
+ out += renderStudioAnnotationPlainTextPdfLatex(source.slice(plainStart, index));
3136
+ }
3133
3137
 
3134
- if (fenceChar === markerChar && markerLength >= fenceLength) {
3135
- inFence = false;
3136
- fenceChar = undefined;
3137
- fenceLength = 0;
3138
- }
3138
+ if (token.type === "code") {
3139
+ out += renderStudioAnnotationCodeSpanPdfLatex(token.raw);
3140
+ } else if (token.type === "math") {
3141
+ out += escapeStudioPdfLatexText(token.raw);
3142
+ } else {
3143
+ out += escapeStudioPdfLatexTextFragment(token.raw);
3144
+ }
3139
3145
 
3140
- out.push(line);
3146
+ index = token.end;
3147
+ plainStart = index;
3148
+ }
3149
+
3150
+ if (plainStart < source.length) {
3151
+ out += renderStudioAnnotationPlainTextPdfLatex(source.slice(plainStart));
3152
+ }
3153
+
3154
+ return out;
3155
+ }
3156
+
3157
+ function readStudioAnnotationPdfEmphasisSpanAt(source: string, startIndex: number, delimiter: string, commandName: string): { end: number; latex: string } | null {
3158
+ if (!canOpenStudioAnnotationEmphasisDelimiter(source, startIndex, delimiter)) return null;
3159
+
3160
+ let index = startIndex + delimiter.length;
3161
+ while (index < source.length) {
3162
+ if (source[index] === "\\") {
3163
+ index = Math.min(source.length, index + 2);
3141
3164
  continue;
3142
3165
  }
3143
3166
 
3144
- if (inFence) {
3145
- out.push(line);
3146
- } else {
3147
- plainBuffer.push(line);
3167
+ const protectedToken = readStudioAnnotationProtectedTokenAt(source, index);
3168
+ if (protectedToken) {
3169
+ index = protectedToken.end;
3170
+ continue;
3171
+ }
3172
+
3173
+ if (canCloseStudioAnnotationEmphasisDelimiter(source, index, delimiter)) {
3174
+ const inner = source.slice(startIndex + delimiter.length, index);
3175
+ return {
3176
+ end: index + delimiter.length,
3177
+ latex: `\\${commandName}{${renderStudioAnnotationPdfLatexContent(inner)}}`,
3178
+ };
3148
3179
  }
3180
+
3181
+ index += 1;
3149
3182
  }
3150
3183
 
3151
- flushPlain();
3152
- return out.join("\n");
3184
+ return null;
3185
+ }
3186
+
3187
+ function renderStudioAnnotationPlainTextPdfLatex(text: string): string {
3188
+ const source = String(text ?? "");
3189
+ let out = "";
3190
+ let index = 0;
3191
+
3192
+ while (index < source.length) {
3193
+ const strongMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "**", "textbf")
3194
+ ?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "__", "textbf");
3195
+ if (strongMatch) {
3196
+ out += strongMatch.latex;
3197
+ index = strongMatch.end;
3198
+ continue;
3199
+ }
3200
+
3201
+ const emphasisMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "*", "emph")
3202
+ ?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "_", "emph");
3203
+ if (emphasisMatch) {
3204
+ out += emphasisMatch.latex;
3205
+ index = emphasisMatch.end;
3206
+ continue;
3207
+ }
3208
+
3209
+ out += escapeStudioPdfLatexTextFragment(source[index] ?? "");
3210
+ index += 1;
3211
+ }
3212
+
3213
+ return out;
3214
+ }
3215
+
3216
+ function renderStudioAnnotationPdfLatex(text: string): string {
3217
+ const normalized = normalizeStudioAnnotationText(text);
3218
+ if (!normalized) return "";
3219
+ return renderStudioAnnotationPdfLatexContent(normalized).trim();
3220
+ }
3221
+
3222
+ function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3223
+ const replaced = replaceStudioInlineAnnotationMarkers(
3224
+ String(text ?? ""),
3225
+ (marker) => {
3226
+ const cleaned = renderStudioAnnotationPdfLatex(marker.body);
3227
+ if (!cleaned) return "";
3228
+ return `\\studioannotation{${cleaned}}`;
3229
+ },
3230
+ );
3231
+
3232
+ return String(replaced ?? "")
3233
+ .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
3234
+ const cleaned = renderStudioAnnotationPdfLatex(markerText);
3235
+ if (!cleaned) return "";
3236
+ return `\\studioannotation{${cleaned}}`;
3237
+ });
3238
+ }
3239
+
3240
+ function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
3241
+ if (!hasStudioMarkdownAnnotationMarkers(markdown)) return String(markdown ?? "");
3242
+ return transformStudioMarkdownOutsideFences(markdown, (segment) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
3153
3243
  }
3154
3244
 
3155
3245
  interface StudioPdfRenderOptions {
@@ -4146,38 +4236,19 @@ function replaceStudioAnnotationMarkersInDiffTokenLine(line: string, macroName:
4146
4236
  if (!tokenMatch) return line;
4147
4237
 
4148
4238
  const body = tokenMatch[1] ?? "";
4149
- const markerPattern = /\[an:\s*([^\]]+?)\]/gi;
4150
- let lastIndex = 0;
4151
- let rewritten = "";
4152
- let match: RegExpExecArray | null;
4153
-
4154
4239
  const wrapText = (text: string): string => text ? `\\${macroName}{${text}}` : "";
4240
+ const rewritten = replaceStudioInlineAnnotationMarkers(
4241
+ body,
4242
+ (marker) => {
4243
+ const markerText = decodeStudioGeneratedCodeLatexText(normalizeStudioAnnotationText(marker.body));
4244
+ const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(renderStudioAnnotationPdfLatex(markerText));
4245
+ if (!cleaned) return "";
4246
+ return `\\studioannotation{${cleaned}}`;
4247
+ },
4248
+ (segment) => wrapText(segment),
4249
+ );
4155
4250
 
4156
- while ((match = markerPattern.exec(body)) !== null) {
4157
- const token = match[0] ?? "";
4158
- const start = match.index;
4159
- if (start > lastIndex) {
4160
- rewritten += wrapText(body.slice(lastIndex, start));
4161
- }
4162
-
4163
- const markerText = decodeStudioGeneratedCodeLatexText((match[1] ?? "").replace(/\s{2,}/g, " ").trim());
4164
- const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(escapeStudioPdfLatexText(markerText));
4165
- if (cleaned) {
4166
- rewritten += `\\studioannotation{${cleaned}}`;
4167
- }
4168
-
4169
- lastIndex = start + token.length;
4170
- if (token.length === 0) {
4171
- markerPattern.lastIndex += 1;
4172
- }
4173
- }
4174
-
4175
- if (lastIndex === 0) return line;
4176
- if (lastIndex < body.length) {
4177
- rewritten += wrapText(body.slice(lastIndex));
4178
- }
4179
-
4180
- return rewritten || wrapText(body);
4251
+ return rewritten === body ? line : (rewritten || wrapText(body));
4181
4252
  }
4182
4253
 
4183
4254
  function rewriteStudioGeneratedDiffHighlighting(latex: string): string {
@@ -4496,7 +4567,7 @@ async function renderStudioPdfWithPandoc(
4496
4567
  }
4497
4568
  };
4498
4569
 
4499
- if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || /\[an:\s*[^\]]+\]/i.test(sourceWithResolvedRefs))) {
4570
+ if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || collectStudioInlineAnnotationMarkers(sourceWithResolvedRefs).length > 0)) {
4500
4571
  return await renderStudioPdfFromGeneratedLatex(
4501
4572
  sourceWithResolvedRefs,
4502
4573
  pandocCommand,
@@ -5627,6 +5698,7 @@ function buildStudioHtml(
5627
5698
  };
5628
5699
  const cssVarsBlock = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
5629
5700
  const stylesheetHref = `/studio.css?token=${encodeURIComponent(studioToken ?? "")}`;
5701
+ const annotationHelpersScriptHref = `/studio-annotation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
5630
5702
  const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
5631
5703
  const faviconHref = buildStudioFaviconDataUri(style);
5632
5704
  const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
@@ -5808,6 +5880,7 @@ ${cssVarsBlock}
5808
5880
  <script>
5809
5881
  window.__PI_STUDIO_BOOT__ = ${bootConfigJson};
5810
5882
  </script>
5883
+ <script src="${annotationHelpersScriptHref}"></script>
5811
5884
  <script src="${clientScriptHref}"></script>
5812
5885
  </body>
5813
5886
  </html>`;
@@ -7353,7 +7426,7 @@ export default function (pi: ExtensionAPI) {
7353
7426
  return;
7354
7427
  }
7355
7428
 
7356
- if (requestUrl.pathname === "/studio-client.js") {
7429
+ if (requestUrl.pathname === "/studio-annotation-helpers.js" || requestUrl.pathname === "/studio-client.js") {
7357
7430
  const token = requestUrl.searchParams.get("token") ?? "";
7358
7431
  if (token !== serverState.token) {
7359
7432
  respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
@@ -7367,8 +7440,15 @@ export default function (pi: ExtensionAPI) {
7367
7440
  return;
7368
7441
  }
7369
7442
 
7443
+ const targetUrl = requestUrl.pathname === "/studio-annotation-helpers.js"
7444
+ ? STUDIO_ANNOTATION_HELPERS_URL
7445
+ : STUDIO_CLIENT_URL;
7446
+ const targetLabel = requestUrl.pathname === "/studio-annotation-helpers.js"
7447
+ ? "studio annotation helper script"
7448
+ : "studio client script";
7449
+
7370
7450
  try {
7371
- const clientScript = readFileSync(STUDIO_CLIENT_URL, "utf-8");
7451
+ const clientScript = readFileSync(targetUrl, "utf-8");
7372
7452
  res.writeHead(200, {
7373
7453
  "Content-Type": "application/javascript; charset=utf-8",
7374
7454
  "Cache-Control": "no-store",
@@ -7377,7 +7457,7 @@ export default function (pi: ExtensionAPI) {
7377
7457
  });
7378
7458
  res.end(clientScript);
7379
7459
  } catch (error) {
7380
- respondText(res, 500, `Failed to load studio client script: ${error instanceof Error ? error.message : String(error)}`);
7460
+ respondText(res, 500, `Failed to load ${targetLabel}: ${error instanceof Error ? error.message : String(error)}`);
7381
7461
  }
7382
7462
  return;
7383
7463
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,11 +18,15 @@
18
18
  "files": [
19
19
  "index.ts",
20
20
  "client",
21
+ "shared",
21
22
  "README.md",
22
23
  "CHANGELOG.md",
23
24
  "WORKFLOW.md",
24
25
  "assets/screenshots"
25
26
  ],
27
+ "scripts": {
28
+ "test": "node --test"
29
+ },
26
30
  "pi": {
27
31
  "extensions": [
28
32
  "./index.ts"