pi-studio 0.9.29 → 0.9.31
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 +11 -0
- package/client/studio-annotation-helpers.js +7 -0
- package/index.ts +41 -18
- package/package.json +1 -1
- package/shared/studio-annotation-render.js +148 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.31] — 2026-06-09
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Render long PDF annotations as full-width display-style boxes at their marker position so exported feedback avoids clipping while preserving the surrounding annotation order.
|
|
11
|
+
- Avoided annotation placeholder collisions in HTML exports with ten or more annotations.
|
|
12
|
+
|
|
13
|
+
## [0.9.30] — 2026-06-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Rendered inline strikethrough, emphasis, and code inside `[an: ...]` annotation badges consistently across Studio preview and HTML/PDF export paths.
|
|
17
|
+
|
|
7
18
|
## [0.9.29] — 2026-06-09
|
|
8
19
|
|
|
9
20
|
### 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}
|
|
@@ -1110,6 +1112,7 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble
|
|
|
1110
1112
|
\\definecolor{StudioCalloutCautionText}{HTML}{A40E26}
|
|
1111
1113
|
\\definecolor{StudioCalloutCautionLabelBg}{HTML}{FDEBEC}
|
|
1112
1114
|
\\newcommand{\\studioannotation}[1]{\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\begin{varwidth}{\\dimexpr\\linewidth-2\\fboxsep-2\\fboxrule\\relax}\\raggedright\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}\\end{varwidth}}\\endgroup}
|
|
1115
|
+
\\newcommand{\\studioblockannotation}[1]{\\par\\smallskip\\noindent\\begingroup\\setlength{\\fboxsep}{1.5pt}\\fcolorbox{StudioAnnotationBorder}{StudioAnnotationBg}{\\begin{minipage}{\\dimexpr\\linewidth-2\\fboxsep-2\\fboxrule\\relax}\\raggedright\\textcolor{StudioAnnotationText}{\\sffamily\\footnotesize\\strut #1}\\end{minipage}}\\endgroup\\par\\smallskip\\noindent\\ignorespaces}
|
|
1113
1116
|
\\newcommand{\\StudioDiffAddTok}[1]{\\textcolor{StudioDiffAddText}{#1}}
|
|
1114
1117
|
\\newcommand{\\StudioDiffDelTok}[1]{\\textcolor{StudioDiffDelText}{#1}}
|
|
1115
1118
|
\\newcommand{\\StudioDiffMetaTok}[1]{\\textcolor{StudioDiffMetaText}{#1}}
|
|
@@ -5031,6 +5034,13 @@ function renderStudioAnnotationPlainTextPdfLatex(text: string): string {
|
|
|
5031
5034
|
let index = 0;
|
|
5032
5035
|
|
|
5033
5036
|
while (index < source.length) {
|
|
5037
|
+
const strikeMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "~~", "sout");
|
|
5038
|
+
if (strikeMatch) {
|
|
5039
|
+
out += strikeMatch.latex;
|
|
5040
|
+
index = strikeMatch.end;
|
|
5041
|
+
continue;
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5034
5044
|
const strongMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "**", "textbf")
|
|
5035
5045
|
?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "__", "textbf");
|
|
5036
5046
|
if (strongMatch) {
|
|
@@ -5060,22 +5070,25 @@ function renderStudioAnnotationPdfLatex(text: string): string {
|
|
|
5060
5070
|
return renderStudioAnnotationPdfLatexContent(normalized).trim();
|
|
5061
5071
|
}
|
|
5062
5072
|
|
|
5073
|
+
function renderStudioAnnotationPdfBox(markerText: string, block = false): string {
|
|
5074
|
+
const cleaned = renderStudioAnnotationPdfLatex(markerText);
|
|
5075
|
+
if (!cleaned) return "";
|
|
5076
|
+
return block ? `\\studioblockannotation{${cleaned}}` : `\\studioannotation{${cleaned}}`;
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5063
5079
|
function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
|
|
5080
|
+
const renderMarker = (markerText: string): string => {
|
|
5081
|
+
const label = normalizeStudioAnnotationText(markerText);
|
|
5082
|
+
if (!label) return "";
|
|
5083
|
+
return renderStudioAnnotationPdfBox(label, shouldRenderStudioAnnotationAsPdfBlock(label));
|
|
5084
|
+
};
|
|
5064
5085
|
const replaced = replaceStudioInlineAnnotationMarkers(
|
|
5065
5086
|
String(text ?? ""),
|
|
5066
|
-
(marker: { body: string }) =>
|
|
5067
|
-
const cleaned = renderStudioAnnotationPdfLatex(marker.body);
|
|
5068
|
-
if (!cleaned) return "";
|
|
5069
|
-
return `\\studioannotation{${cleaned}}`;
|
|
5070
|
-
},
|
|
5087
|
+
(marker: { body: string }) => renderMarker(marker.body),
|
|
5071
5088
|
);
|
|
5072
5089
|
|
|
5073
5090
|
return String(replaced ?? "")
|
|
5074
|
-
.replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) =>
|
|
5075
|
-
const cleaned = renderStudioAnnotationPdfLatex(markerText);
|
|
5076
|
-
if (!cleaned) return "";
|
|
5077
|
-
return `\\studioannotation{${cleaned}}`;
|
|
5078
|
-
});
|
|
5091
|
+
.replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => renderMarker(markerText));
|
|
5079
5092
|
}
|
|
5080
5093
|
|
|
5081
5094
|
function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
|
|
@@ -5648,13 +5661,14 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
5648
5661
|
? wrapStudioCodeAsMarkdown(input, effectiveEditorLanguage)
|
|
5649
5662
|
: input;
|
|
5650
5663
|
const fenceNormalizedSource = effectiveEditorLanguage === "latex" ? source : normalizeStudioMarkdownSmartFences(source);
|
|
5651
|
-
const
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
return prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
5656
|
-
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(annotationReadySource),
|
|
5664
|
+
const annotationReadyLanguage = !effectiveEditorLanguage || effectiveEditorLanguage === "markdown" || effectiveEditorLanguage === "latex";
|
|
5665
|
+
const commentStrippedSource = stripStudioMarkdownHtmlCommentsPreservingYamlFrontMatter(fenceNormalizedSource);
|
|
5666
|
+
const pandocReadySource = prepareStudioMarkdownForPandoc(commentStrippedSource, {
|
|
5667
|
+
preserveLiteralLatexCommands: !hasStudioYamlHeaderIncludes(fenceNormalizedSource),
|
|
5657
5668
|
});
|
|
5669
|
+
return annotationReadyLanguage
|
|
5670
|
+
? replaceStudioAnnotationMarkersForPdf(pandocReadySource)
|
|
5671
|
+
: pandocReadySource;
|
|
5658
5672
|
}
|
|
5659
5673
|
|
|
5660
5674
|
function stripMathMlAnnotationTags(html: string): string {
|
|
@@ -6086,6 +6100,15 @@ function parseStudioHtmlPdfBlockOptions(body: string): StudioHtmlPdfBlockOptions
|
|
|
6086
6100
|
return options;
|
|
6087
6101
|
}
|
|
6088
6102
|
|
|
6103
|
+
// PDF/LaTeX cannot fully mimic the browser's inline-block wrapping for long
|
|
6104
|
+
// annotation chips, so long annotations switch to a display box at the marker.
|
|
6105
|
+
const STUDIO_PDF_ANNOTATION_DISPLAY_THRESHOLD_CHARS = 115;
|
|
6106
|
+
|
|
6107
|
+
function shouldRenderStudioAnnotationAsPdfBlock(text: string): boolean {
|
|
6108
|
+
const normalized = normalizeStudioAnnotationText(text);
|
|
6109
|
+
return normalized.length > STUDIO_PDF_ANNOTATION_DISPLAY_THRESHOLD_CHARS;
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6089
6112
|
function prepareStudioPdfBlocksForHtml(markdown: string): { markdown: string; blocks: StudioHtmlPdfBlock[] } {
|
|
6090
6113
|
const blocks: StudioHtmlPdfBlock[] = [];
|
|
6091
6114
|
const prefix = `PISTUDIOHTMLPDF${Date.now().toString(36)}${randomUUID().replace(/-/g, "")}TOKEN`;
|
|
@@ -6116,9 +6139,9 @@ function prepareStudioAnnotationMarkersForHtml(markdown: string): { markdown: st
|
|
|
6116
6139
|
|
|
6117
6140
|
function applyStudioAnnotationPlaceholdersToHtml(html: string, placeholders: StudioHtmlAnnotationPlaceholder[]): string {
|
|
6118
6141
|
let transformed = String(html ?? "");
|
|
6119
|
-
for (const placeholder of placeholders) {
|
|
6142
|
+
for (const placeholder of [...placeholders].sort((a, b) => b.token.length - a.token.length)) {
|
|
6120
6143
|
const tokenPattern = new RegExp(escapeStudioRegExpLiteral(placeholder.token), "g");
|
|
6121
|
-
const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${
|
|
6144
|
+
const markerHtml = `<span class="annotation-preview-marker" title="${escapeStudioHtmlText(placeholder.title)}">${renderStudioAnnotationInlineHtml(placeholder.text)}</span>`;
|
|
6122
6145
|
transformed = transformed.replace(tokenPattern, markerHtml);
|
|
6123
6146
|
}
|
|
6124
6147
|
return transformed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.31",
|
|
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, "&")
|
|
11
|
+
.replace(/</g, "<")
|
|
12
|
+
.replace(/>/g, ">")
|
|
13
|
+
.replace(/\"/g, """)
|
|
14
|
+
.replace(/'/g, "'");
|
|
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
|
+
}
|