pi-studio 0.9.29 → 0.9.30

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,11 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.30] — 2026-06-09
8
+
9
+ ### Fixed
10
+ - Rendered inline strikethrough, emphasis, and code inside `[an: ...]` annotation badges consistently across Studio preview and HTML/PDF export paths.
11
+
7
12
  ## [0.9.29] — 2026-06-09
8
13
 
9
14
  ### Added
@@ -443,6 +443,13 @@
443
443
  let index = 0;
444
444
 
445
445
  while (index < source.length) {
446
+ const strikeMatch = readAnnotationEmphasisSpanAt(source, index, "~~", "s");
447
+ if (strikeMatch) {
448
+ out += strikeMatch.html;
449
+ index = strikeMatch.end;
450
+ continue;
451
+ }
452
+
446
453
  const strongMatch = readAnnotationEmphasisSpanAt(source, index, "**", "strong")
447
454
  || readAnnotationEmphasisSpanAt(source, index, "__", "strong");
448
455
  if (strongMatch) {
package/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
31
31
  import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
32
32
  import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
33
+ import { renderStudioAnnotationInlineHtml } from "./shared/studio-annotation-render.js";
33
34
 
34
35
  type Lens = "writing" | "code";
35
36
  type RequestedLens = Lens | "auto";
@@ -1086,6 +1087,7 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble
1086
1087
  \\titlespacing*{\\subparagraph}{0pt}{0.7ex plus 0.2ex minus 0.1ex}{0.7em}
1087
1088
  \\usepackage{xcolor}
1088
1089
  \\usepackage{varwidth}
1090
+ \\usepackage[normalem]{ulem}
1089
1091
  \\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
1090
1092
  \\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
1091
1093
  \\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
@@ -5031,6 +5033,13 @@ function renderStudioAnnotationPlainTextPdfLatex(text: string): string {
5031
5033
  let index = 0;
5032
5034
 
5033
5035
  while (index < source.length) {
5036
+ const strikeMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "~~", "sout");
5037
+ if (strikeMatch) {
5038
+ out += strikeMatch.latex;
5039
+ index = strikeMatch.end;
5040
+ continue;
5041
+ }
5042
+
5034
5043
  const strongMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "**", "textbf")
5035
5044
  ?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "__", "textbf");
5036
5045
  if (strongMatch) {
@@ -5648,13 +5657,14 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
5648
5657
  ? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
5649
5658
  : input;
5650
5659
  const fenceNormalizedSource = effectiveEditorLanguage === "latex" ? source : normalizeStudioMarkdownSmartFences(source);
5651
- const annotationReadySource = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex"
5652
- ? replaceStudioAnnotationMarkersForPdf(fenceNormalizedSource)
5653
- : fenceNormalizedSource;
5654
- const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(annotationReadySource);
5655
- return prepareStudioMarkdownForPandoc(commentStrippedSource, {
5656
- preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
5660
+ const annotationReadyLanguage = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex";
5661
+ const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(fenceNormalizedSource);
5662
+ const pandocReadySource = prepareStudioMarkdownForPandoc(commentStrippedSource, {
5663
+ preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(fenceNormalizedSource),
5657
5664
  });
5665
+ return annotationReadyLanguage
5666
+ ? replaceStudioAnnotationMarkersForPdf(pandocReadySource)
5667
+ : pandocReadySource;
5658
5668
  }
5659
5669
 
5660
5670
  function stripMathMlAnnotationTags(html: string): string {
@@ -6118,7 +6128,7 @@ function applyStudioAnnotationPlaceholdersToHtml(html: string, placeholders: Stu
6118
6128
  let transformed = String(html ?? "");
6119
6129
  for (const placeholder of placeholders) {
6120
6130
  const tokenPattern = new RegExp(escapeStudioRegExpLiteral(placeholder.token), "g");
6121
- const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${escapeStudioHtmlText(placeholder.text)}</span>`;
6131
+ const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${renderStudioAnnotationInlineHtml(placeholder.text)}</span>`;
6122
6132
  transformed = transformed.replace(tokenPattern, markerHtml);
6123
6133
  }
6124
6134
  return transformed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.29",
3
+ "version": "0.9.30",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,148 @@
1
+ import {
2
+ advancePastStudioInlineBacktickSpan,
3
+ isStudioAnnotationWordChar,
4
+ normalizeStudioAnnotationText,
5
+ readStudioAnnotationProtectedTokenAt,
6
+ } from "./studio-annotation-scanner.js";
7
+
8
+ function escapeHtml(text) {
9
+ return String(text ?? "")
10
+ .replace(/&/g, "&amp;")
11
+ .replace(/</g, "&lt;")
12
+ .replace(/>/g, "&gt;")
13
+ .replace(/\"/g, "&quot;")
14
+ .replace(/'/g, "&#39;");
15
+ }
16
+
17
+ function canOpenAnnotationInlineDelimiter(source, startIndex, delimiter) {
18
+ const text = String(source || "");
19
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
20
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
21
+ const next = text[startIndex + delimiter.length] || "";
22
+ if (!next || /\s/.test(next)) return false;
23
+ return !isStudioAnnotationWordChar(prev);
24
+ }
25
+
26
+ function canCloseAnnotationInlineDelimiter(source, startIndex, delimiter) {
27
+ const text = String(source || "");
28
+ if (text.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
29
+ const prev = startIndex > 0 ? text[startIndex - 1] : "";
30
+ const next = text[startIndex + delimiter.length] || "";
31
+ if (!prev || /\s/.test(prev)) return false;
32
+ return !isStudioAnnotationWordChar(next);
33
+ }
34
+
35
+ function readAnnotationInlineSpanAt(source, startIndex, delimiter, tagName) {
36
+ const text = String(source || "");
37
+ if (!canOpenAnnotationInlineDelimiter(text, startIndex, delimiter)) return null;
38
+
39
+ let index = startIndex + delimiter.length;
40
+ while (index < text.length) {
41
+ if (text[index] === "\\") {
42
+ index = Math.min(text.length, index + 2);
43
+ continue;
44
+ }
45
+
46
+ const protectedToken = readStudioAnnotationProtectedTokenAt(text, index);
47
+ if (protectedToken) {
48
+ index = protectedToken.end;
49
+ continue;
50
+ }
51
+
52
+ if (canCloseAnnotationInlineDelimiter(text, index, delimiter)) {
53
+ const inner = text.slice(startIndex + delimiter.length, index);
54
+ return {
55
+ end: index + delimiter.length,
56
+ html: `<${tagName}>${renderAnnotationPlainInlineHtml(inner)}</${tagName}>`,
57
+ };
58
+ }
59
+
60
+ index += 1;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ function renderAnnotationCodeSpanHtml(rawToken) {
67
+ const raw = String(rawToken || "");
68
+ if (!raw || raw[0] !== "`") return escapeHtml(raw);
69
+
70
+ let fenceLength = 1;
71
+ while (raw[fenceLength] === "`") fenceLength += 1;
72
+ const fence = "`".repeat(fenceLength);
73
+ if (raw.length < fenceLength * 2 || raw.slice(raw.length - fenceLength) !== fence) {
74
+ return escapeHtml(raw);
75
+ }
76
+
77
+ return `<code>${escapeHtml(raw.slice(fenceLength, raw.length - fenceLength))}</code>`;
78
+ }
79
+
80
+ function renderAnnotationPlainInlineHtml(text) {
81
+ const source = String(text || "");
82
+ let out = "";
83
+ let index = 0;
84
+
85
+ while (index < source.length) {
86
+ const strikeMatch = readAnnotationInlineSpanAt(source, index, "~~", "s");
87
+ if (strikeMatch) {
88
+ out += strikeMatch.html;
89
+ index = strikeMatch.end;
90
+ continue;
91
+ }
92
+
93
+ const strongMatch = readAnnotationInlineSpanAt(source, index, "**", "strong")
94
+ || readAnnotationInlineSpanAt(source, index, "__", "strong");
95
+ if (strongMatch) {
96
+ out += strongMatch.html;
97
+ index = strongMatch.end;
98
+ continue;
99
+ }
100
+
101
+ const emphasisMatch = readAnnotationInlineSpanAt(source, index, "*", "em")
102
+ || readAnnotationInlineSpanAt(source, index, "_", "em");
103
+ if (emphasisMatch) {
104
+ out += emphasisMatch.html;
105
+ index = emphasisMatch.end;
106
+ continue;
107
+ }
108
+
109
+ out += escapeHtml(source[index]);
110
+ index += 1;
111
+ }
112
+
113
+ return out;
114
+ }
115
+
116
+ export function renderStudioAnnotationInlineHtml(text) {
117
+ const source = normalizeStudioAnnotationText(text);
118
+ let out = "";
119
+ let plainStart = 0;
120
+ let index = 0;
121
+
122
+ while (index < source.length) {
123
+ const token = readStudioAnnotationProtectedTokenAt(source, index);
124
+ if (!token) {
125
+ index += 1;
126
+ continue;
127
+ }
128
+
129
+ if (index > plainStart) {
130
+ out += renderAnnotationPlainInlineHtml(source.slice(plainStart, index));
131
+ }
132
+
133
+ if (token.type === "code") {
134
+ out += renderAnnotationCodeSpanHtml(token.raw);
135
+ } else {
136
+ out += escapeHtml(token.raw);
137
+ }
138
+
139
+ index = token.end;
140
+ plainStart = index;
141
+ }
142
+
143
+ if (plainStart < source.length) {
144
+ out += renderAnnotationPlainInlineHtml(source.slice(plainStart));
145
+ }
146
+
147
+ return out;
148
+ }