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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.36] — 2026-03-28
8
+
9
+ ### Changed
10
+ - Annotation pills in Studio preview now render a small safe subset of inline formatting inside `[an: ...]` notes — emphasis/bold, inline code, and math — while still keeping bare URLs and markdown links inert literal text so annotation notes remain robust and self-contained.
11
+ - PDF/export-side annotation handling now follows the same bracket-aware parsing model as the preview for raw Markdown annotation markers, so markdown-ish note content is treated as one annotation body instead of being cut off at the first `]`.
12
+
13
+ ### Fixed
14
+ - Preview-side annotation placeholder insertion now keeps inline-code examples such as `` `[an: prefer \`npm test\` here]` `` from desynchronizing later annotation parsing and leaking raw `PISTUDIOANNOT...TOKEN` placeholders.
15
+ - `/studio-pdf` and generated-LaTeX annotation rewriting now handle markdown links, inline code, emphasis markers, escaped backticks, and multiple annotations more reliably inside `[an: ...]` markers, while still leaving fenced-code literals untouched.
16
+
7
17
  ## [0.5.35] — 2026-03-27
8
18
 
9
19
  ### Fixed
@@ -0,0 +1,543 @@
1
+ (() => {
2
+ function normalizePreviewAnnotationLabel(text) {
3
+ return String(text || "")
4
+ .replace(/\r\n/g, "\n")
5
+ .replace(/\s*\n\s*/g, " ")
6
+ .replace(/\s{2,}/g, " ")
7
+ .trim();
8
+ }
9
+
10
+ function advancePastBacktickSpan(source, startIndex) {
11
+ let fenceLength = 1;
12
+ while (source[startIndex + fenceLength] === "`") fenceLength += 1;
13
+
14
+ let index = startIndex + fenceLength;
15
+ while (index < source.length) {
16
+ const ch = source[index];
17
+ if (ch === "\\") {
18
+ index = Math.min(source.length, index + 2);
19
+ continue;
20
+ }
21
+ if (ch === "`") {
22
+ let runLength = 1;
23
+ while (source[index + runLength] === "`") runLength += 1;
24
+ if (runLength === fenceLength) {
25
+ return index + runLength;
26
+ }
27
+ index += runLength;
28
+ continue;
29
+ }
30
+ if (ch === "\n") {
31
+ return index + 1;
32
+ }
33
+ index += 1;
34
+ }
35
+
36
+ return source.length;
37
+ }
38
+
39
+ function readInlineAnnotationMarkerAt(source, startIndex) {
40
+ const text = String(source || "");
41
+ if (startIndex < 0 || startIndex + 4 > text.length) return null;
42
+ if (text[startIndex] !== "[" || text.slice(startIndex, startIndex + 4).toLowerCase() !== "[an:") {
43
+ return null;
44
+ }
45
+
46
+ let index = startIndex + 4;
47
+ while (index < text.length && /\s/.test(text[index])) index += 1;
48
+ const bodyStart = index;
49
+ let squareDepth = 0;
50
+
51
+ while (index < text.length) {
52
+ const ch = text[index];
53
+ if (ch === "\\") {
54
+ index = Math.min(text.length, index + 2);
55
+ continue;
56
+ }
57
+ if (ch === "`") {
58
+ index = advancePastBacktickSpan(text, index);
59
+ continue;
60
+ }
61
+ if (ch === "[") {
62
+ squareDepth += 1;
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (ch === "]") {
67
+ if (squareDepth === 0) {
68
+ const end = index + 1;
69
+ return {
70
+ start: startIndex,
71
+ end: end,
72
+ raw: text.slice(startIndex, end),
73
+ body: text.slice(bodyStart, index),
74
+ };
75
+ }
76
+ squareDepth -= 1;
77
+ index += 1;
78
+ continue;
79
+ }
80
+ index += 1;
81
+ }
82
+
83
+ return null;
84
+ }
85
+
86
+ function collectInlineAnnotationMarkers(text) {
87
+ const source = String(text || "");
88
+ const markers = [];
89
+ let index = 0;
90
+
91
+ while (index < source.length) {
92
+ const ch = source[index];
93
+ if (ch === "\\") {
94
+ index = Math.min(source.length, index + 2);
95
+ continue;
96
+ }
97
+ if (ch === "`") {
98
+ index = advancePastBacktickSpan(source, index);
99
+ continue;
100
+ }
101
+ if (ch === "[" && source.slice(index, index + 4).toLowerCase() === "[an:") {
102
+ const marker = readInlineAnnotationMarkerAt(source, index);
103
+ if (marker) {
104
+ markers.push(marker);
105
+ index = marker.end;
106
+ continue;
107
+ }
108
+ }
109
+ index += 1;
110
+ }
111
+
112
+ return markers;
113
+ }
114
+
115
+ function replaceInlineAnnotationMarkers(text, annotationReplacer, textReplacer) {
116
+ const source = String(text || "");
117
+ const markers = collectInlineAnnotationMarkers(source);
118
+ const replaceAnnotation = typeof annotationReplacer === "function"
119
+ ? annotationReplacer
120
+ : function(marker) { return marker.raw; };
121
+ const replaceText = typeof textReplacer === "function"
122
+ ? textReplacer
123
+ : function(segment) { return segment; };
124
+
125
+ if (markers.length === 0) {
126
+ return replaceText(source);
127
+ }
128
+
129
+ let out = "";
130
+ let lastIndex = 0;
131
+ markers.forEach(function(marker) {
132
+ if (marker.start > lastIndex) {
133
+ out += String(replaceText(source.slice(lastIndex, marker.start)) ?? "");
134
+ }
135
+ out += String(replaceAnnotation(marker) ?? "");
136
+ lastIndex = marker.end;
137
+ });
138
+ if (lastIndex < source.length) {
139
+ out += String(replaceText(source.slice(lastIndex)) ?? "");
140
+ }
141
+ return out;
142
+ }
143
+
144
+ function escapeHtml(text) {
145
+ return String(text || "")
146
+ .replace(/&/g, "&amp;")
147
+ .replace(/</g, "&lt;")
148
+ .replace(/>/g, "&gt;")
149
+ .replace(/"/g, "&quot;")
150
+ .replace(/'/g, "&#39;");
151
+ }
152
+
153
+ function isWordChar(ch) {
154
+ return typeof ch === "string" && /[A-Za-z0-9]/.test(ch);
155
+ }
156
+
157
+ function readInlineMarkdownLinkAt(source, startIndex) {
158
+ const text = String(source || "");
159
+ if (text[startIndex] !== "[") return null;
160
+
161
+ let index = startIndex + 1;
162
+ let squareDepth = 0;
163
+ while (index < text.length) {
164
+ const ch = text[index];
165
+ if (ch === "\\") {
166
+ index = Math.min(text.length, index + 2);
167
+ continue;
168
+ }
169
+ if (ch === "`") {
170
+ index = advancePastBacktickSpan(text, index);
171
+ continue;
172
+ }
173
+ if (ch === "[") {
174
+ squareDepth += 1;
175
+ index += 1;
176
+ continue;
177
+ }
178
+ if (ch === "]") {
179
+ if (squareDepth === 0) break;
180
+ squareDepth -= 1;
181
+ index += 1;
182
+ continue;
183
+ }
184
+ if (ch === "\n") return null;
185
+ index += 1;
186
+ }
187
+
188
+ if (index >= text.length || text[index] !== "]" || text[index + 1] !== "(") return null;
189
+
190
+ index += 2;
191
+ let parenDepth = 0;
192
+ while (index < text.length) {
193
+ const ch = text[index];
194
+ if (ch === "\\") {
195
+ index = Math.min(text.length, index + 2);
196
+ continue;
197
+ }
198
+ if (ch === "`") {
199
+ index = advancePastBacktickSpan(text, index);
200
+ continue;
201
+ }
202
+ if (ch === "(") {
203
+ parenDepth += 1;
204
+ index += 1;
205
+ continue;
206
+ }
207
+ if (ch === ")") {
208
+ if (parenDepth === 0) {
209
+ return {
210
+ type: "literal",
211
+ raw: text.slice(startIndex, index + 1),
212
+ end: index + 1,
213
+ };
214
+ }
215
+ parenDepth -= 1;
216
+ index += 1;
217
+ continue;
218
+ }
219
+ if (ch === "\n") return null;
220
+ index += 1;
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ function readDelimitedPreviewTokenAt(source, startIndex, open, close, allowNewlines) {
227
+ const text = String(source || "");
228
+ if (text.slice(startIndex, startIndex + open.length) !== open) return null;
229
+
230
+ let index = startIndex + open.length;
231
+ while (index < text.length) {
232
+ const ch = text[index];
233
+ if (!allowNewlines && ch === "\n") return null;
234
+ if (ch === "\\") {
235
+ index = Math.min(text.length, index + 2);
236
+ continue;
237
+ }
238
+ if (text.slice(index, index + close.length) === close) {
239
+ return {
240
+ type: "math",
241
+ raw: text.slice(startIndex, index + close.length),
242
+ end: index + close.length,
243
+ };
244
+ }
245
+ index += 1;
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ function readInlineMathTokenAt(source, startIndex) {
252
+ const text = String(source || "");
253
+ if (text[startIndex] === "\\" && text[startIndex + 1] === "(") {
254
+ return readDelimitedPreviewTokenAt(text, startIndex, "\\(", "\\)", true);
255
+ }
256
+ if (text[startIndex] === "\\" && text[startIndex + 1] === "[") {
257
+ return readDelimitedPreviewTokenAt(text, startIndex, "\\[", "\\]", true);
258
+ }
259
+ if (text[startIndex] === "$" && text[startIndex + 1] === "$") {
260
+ return readDelimitedPreviewTokenAt(text, startIndex, "$$", "$$", true);
261
+ }
262
+ if (text[startIndex] === "$" && text[startIndex + 1] !== "$" && text[startIndex + 1] && !/\s/.test(text[startIndex + 1])) {
263
+ const token = readDelimitedPreviewTokenAt(text, startIndex, "$", "$", false);
264
+ if (token && token.raw.length > 2) return token;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ function readBareUrlTokenAt(source, startIndex) {
270
+ const text = String(source || "").slice(startIndex);
271
+ const match = text.match(/^https?:\/\/[^\s<]+/i);
272
+ if (!match) return null;
273
+ return {
274
+ type: "literal",
275
+ raw: match[0],
276
+ end: startIndex + match[0].length,
277
+ };
278
+ }
279
+
280
+ function readAnnotationPreviewProtectedTokenAt(source, startIndex) {
281
+ const text = String(source || "");
282
+ if (startIndex < 0 || startIndex >= text.length) return null;
283
+
284
+ if (text[startIndex] === "`") {
285
+ const end = advancePastBacktickSpan(text, startIndex);
286
+ return {
287
+ type: "code",
288
+ raw: text.slice(startIndex, end),
289
+ end: end,
290
+ };
291
+ }
292
+
293
+ const linkToken = text[startIndex] === "["
294
+ ? readInlineMarkdownLinkAt(text, startIndex)
295
+ : null;
296
+ if (linkToken) return linkToken;
297
+
298
+ const mathToken = (text[startIndex] === "$" || text[startIndex] === "\\")
299
+ ? readInlineMathTokenAt(text, startIndex)
300
+ : null;
301
+ if (mathToken) return mathToken;
302
+
303
+ const urlToken = text[startIndex].toLowerCase() === "h"
304
+ ? readBareUrlTokenAt(text, startIndex)
305
+ : null;
306
+ if (urlToken) return urlToken;
307
+
308
+ return null;
309
+ }
310
+
311
+ function canOpenEmphasisDelimiter(source, startIndex, delimiter) {
312
+ const text = String(source || "");
313
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
314
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
315
+ const next = text[startIndex + delimiter.length] || "";
316
+ if (!next || /\s/.test(next)) return false;
317
+ return !isWordChar(prev);
318
+ }
319
+
320
+ function canCloseEmphasisDelimiter(source, startIndex, delimiter) {
321
+ const text = String(source || "");
322
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
323
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
324
+ const next = text[startIndex + delimiter.length] || "";
325
+ if (!prev || /\s/.test(prev)) return false;
326
+ return !isWordChar(next);
327
+ }
328
+
329
+ function readAnnotationEmphasisSpanAt(source, startIndex, delimiter, tagName) {
330
+ const text = String(source || "");
331
+ if (!canOpenEmphasisDelimiter(text, startIndex, delimiter)) return null;
332
+
333
+ let index = startIndex + delimiter.length;
334
+ while (index < text.length) {
335
+ if (text[index] === "\\") {
336
+ index = Math.min(text.length, index + 2);
337
+ continue;
338
+ }
339
+
340
+ const protectedToken = readAnnotationPreviewProtectedTokenAt(text, index);
341
+ if (protectedToken) {
342
+ index = protectedToken.end;
343
+ continue;
344
+ }
345
+
346
+ if (canCloseEmphasisDelimiter(text, index, delimiter)) {
347
+ const inner = text.slice(startIndex + delimiter.length, index);
348
+ return {
349
+ end: index + delimiter.length,
350
+ html: "<" + tagName + ">" + renderAnnotationPlainTextHtml(inner) + "</" + tagName + ">",
351
+ };
352
+ }
353
+
354
+ index += 1;
355
+ }
356
+
357
+ return null;
358
+ }
359
+
360
+ function renderAnnotationCodeSpanHtml(rawToken) {
361
+ const raw = String(rawToken || "");
362
+ if (!raw || raw[0] !== "`") return escapeHtml(raw);
363
+
364
+ let fenceLength = 1;
365
+ while (raw[fenceLength] === "`") fenceLength += 1;
366
+ const fence = "`".repeat(fenceLength);
367
+ if (raw.length < fenceLength * 2 || raw.slice(raw.length - fenceLength) !== fence) {
368
+ return escapeHtml(raw);
369
+ }
370
+
371
+ return "<code>" + escapeHtml(raw.slice(fenceLength, raw.length - fenceLength)) + "</code>";
372
+ }
373
+
374
+ function renderAnnotationPlainTextHtml(text) {
375
+ const source = String(text || "");
376
+ let out = "";
377
+ let index = 0;
378
+
379
+ while (index < source.length) {
380
+ const strongMatch = readAnnotationEmphasisSpanAt(source, index, "**", "strong")
381
+ || readAnnotationEmphasisSpanAt(source, index, "__", "strong");
382
+ if (strongMatch) {
383
+ out += strongMatch.html;
384
+ index = strongMatch.end;
385
+ continue;
386
+ }
387
+
388
+ const emphasisMatch = readAnnotationEmphasisSpanAt(source, index, "*", "em")
389
+ || readAnnotationEmphasisSpanAt(source, index, "_", "em");
390
+ if (emphasisMatch) {
391
+ out += emphasisMatch.html;
392
+ index = emphasisMatch.end;
393
+ continue;
394
+ }
395
+
396
+ out += escapeHtml(source[index]);
397
+ index += 1;
398
+ }
399
+
400
+ return out;
401
+ }
402
+
403
+ function renderPreviewAnnotationHtml(text) {
404
+ const source = normalizePreviewAnnotationLabel(text);
405
+ let out = "";
406
+ let plainStart = 0;
407
+ let index = 0;
408
+
409
+ while (index < source.length) {
410
+ const token = readAnnotationPreviewProtectedTokenAt(source, index);
411
+ if (!token) {
412
+ index += 1;
413
+ continue;
414
+ }
415
+
416
+ if (index > plainStart) {
417
+ out += renderAnnotationPlainTextHtml(source.slice(plainStart, index));
418
+ }
419
+
420
+ if (token.type === "code") {
421
+ out += renderAnnotationCodeSpanHtml(token.raw);
422
+ } else {
423
+ out += escapeHtml(token.raw);
424
+ }
425
+
426
+ index = token.end;
427
+ plainStart = index;
428
+ }
429
+
430
+ if (plainStart < source.length) {
431
+ out += renderAnnotationPlainTextHtml(source.slice(plainStart));
432
+ }
433
+
434
+ return out;
435
+ }
436
+
437
+ function transformMarkdownOutsideFences(text, plainTransformer) {
438
+ const source = String(text || "").replace(/\r\n/g, "\n");
439
+ if (!source) return source;
440
+
441
+ const transformPlain = typeof plainTransformer === "function"
442
+ ? plainTransformer
443
+ : function(segment) { return segment; };
444
+ const lines = source.split("\n");
445
+ const out = [];
446
+ let plainBuffer = [];
447
+ let inFence = false;
448
+ let fenceChar = null;
449
+ let fenceLength = 0;
450
+
451
+ function flushPlain() {
452
+ if (plainBuffer.length === 0) return;
453
+ const transformed = transformPlain(plainBuffer.join("\n"));
454
+ out.push(typeof transformed === "string" ? transformed : String(transformed ?? ""));
455
+ plainBuffer = [];
456
+ }
457
+
458
+ lines.forEach(function(line) {
459
+ const trimmed = line.trimStart();
460
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
461
+ if (fenceMatch) {
462
+ const marker = fenceMatch[1] || "";
463
+ const markerChar = marker.charAt(0);
464
+ const markerLength = marker.length;
465
+
466
+ if (!inFence) {
467
+ flushPlain();
468
+ inFence = true;
469
+ fenceChar = markerChar;
470
+ fenceLength = markerLength;
471
+ out.push(line);
472
+ return;
473
+ }
474
+
475
+ if (fenceChar === markerChar && markerLength >= fenceLength) {
476
+ inFence = false;
477
+ fenceChar = null;
478
+ fenceLength = 0;
479
+ }
480
+
481
+ out.push(line);
482
+ return;
483
+ }
484
+
485
+ if (inFence) {
486
+ out.push(line);
487
+ } else {
488
+ plainBuffer.push(line);
489
+ }
490
+ });
491
+
492
+ flushPlain();
493
+ return out.join("\n");
494
+ }
495
+
496
+ function hasAnnotationMarkers(text) {
497
+ let found = false;
498
+ transformMarkdownOutsideFences(text, function(segment) {
499
+ if (!found && collectInlineAnnotationMarkers(segment).length > 0) {
500
+ found = true;
501
+ }
502
+ return segment;
503
+ });
504
+ return found;
505
+ }
506
+
507
+ function stripAnnotationMarkers(text) {
508
+ return transformMarkdownOutsideFences(text, function(segment) {
509
+ return replaceInlineAnnotationMarkers(segment, function() { return ""; });
510
+ });
511
+ }
512
+
513
+ function prepareMarkdownForPandocPreview(markdown, placeholderPrefix) {
514
+ const placeholders = [];
515
+ const prefix = typeof placeholderPrefix === "string" && placeholderPrefix
516
+ ? placeholderPrefix
517
+ : "PISTUDIOANNOT";
518
+ const prepared = transformMarkdownOutsideFences(markdown, function(segment) {
519
+ return replaceInlineAnnotationMarkers(segment, function(marker) {
520
+ const label = normalizePreviewAnnotationLabel(marker.body);
521
+ if (!label) return "";
522
+ const token = prefix + placeholders.length + "TOKEN";
523
+ placeholders.push({ token: token, text: label, title: "[an: " + label + "]" });
524
+ return token;
525
+ });
526
+ });
527
+ return { markdown: prepared, placeholders: placeholders };
528
+ }
529
+
530
+ const helpers = Object.freeze({
531
+ collectInlineAnnotationMarkers: collectInlineAnnotationMarkers,
532
+ hasAnnotationMarkers: hasAnnotationMarkers,
533
+ normalizePreviewAnnotationLabel: normalizePreviewAnnotationLabel,
534
+ prepareMarkdownForPandocPreview: prepareMarkdownForPandocPreview,
535
+ readInlineAnnotationMarkerAt: readInlineAnnotationMarkerAt,
536
+ renderPreviewAnnotationHtml: renderPreviewAnnotationHtml,
537
+ replaceInlineAnnotationMarkers: replaceInlineAnnotationMarkers,
538
+ stripAnnotationMarkers: stripAnnotationMarkers,
539
+ transformMarkdownOutsideFences: transformMarkdownOutsideFences,
540
+ });
541
+
542
+ globalThis.PiStudioAnnotationHelpers = helpers;
543
+ })();