pi-studio 0.1.5 → 0.1.7
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 +2 -0
- package/README.md +3 -2
- package/index.ts +176 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -23,6 +23,7 @@ All notable changes to `pi-studio` are documented here.
|
|
|
23
23
|
- **Load file in editor** action in top controls (browser file picker into editor).
|
|
24
24
|
- README screenshot gallery for dark/light workspace and critique/annotation views.
|
|
25
25
|
- Response-side markdown highlighting toggle (`Highlight markdown: Off|On`) in `Response: Markdown` view, with local preference persistence.
|
|
26
|
+
- Markdown highlighter now applies lightweight fenced-code token colors for common languages (`js/ts`, `python`, `bash/sh`, `json`).
|
|
26
27
|
- Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
|
|
27
28
|
|
|
28
29
|
### Changed
|
|
@@ -36,6 +37,7 @@ All notable changes to `pi-studio` are documented here.
|
|
|
36
37
|
- Critique-specific load actions now focus on notes/full views and are only shown for structured critique responses.
|
|
37
38
|
- Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
|
|
38
39
|
- Preview pane typography/style now follows the higher-fidelity `/preview-browser` rendering style more closely.
|
|
40
|
+
- Preview mode now uses pandoc code highlighting output for syntax-colored code blocks.
|
|
39
41
|
- Hardened Studio preview HTTP handling and added client-side preview-request timeout to avoid stuck "Rendering preview…" states.
|
|
40
42
|
|
|
41
43
|
### Fixed
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Status: experimental alpha.
|
|
|
33
33
|
- critique: **Load critique (notes)** / **Load critique (full)**
|
|
34
34
|
- File actions: **Save As…**, **Save file**, **Load file in editor**
|
|
35
35
|
- View toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
|
|
36
|
-
- Optional markdown highlighting toggles for editor and response markdown views
|
|
36
|
+
- Optional markdown highlighting toggles for editor and response markdown views (including fenced-code token colors for common languages)
|
|
37
37
|
- Theme-aware browser UI based on current pi theme
|
|
38
38
|
|
|
39
39
|
## Commands
|
|
@@ -72,8 +72,9 @@ pi -e https://github.com/omaclaren/pi-studio
|
|
|
72
72
|
|
|
73
73
|
- Local-only server (`127.0.0.1`) with rotating session tokens.
|
|
74
74
|
- One studio request at a time.
|
|
75
|
+
- Pi Studio is currently optimized for markdown workflows (model responses, plans, and notes), including fenced code blocks. Pure code files are supported, but highlighting is tuned for markdown and fenced blocks rather than full-file language mode.
|
|
75
76
|
- Studio URLs include a token query parameter; avoid sharing full Studio URLs.
|
|
76
|
-
- Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), sanitized in-browser with `dompurify`.
|
|
77
|
+
- Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`.
|
|
77
78
|
- Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
|
|
78
79
|
- Install pandoc for full preview rendering (`brew install pandoc` on macOS).
|
|
79
80
|
- If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
|
package/index.ts
CHANGED
|
@@ -499,7 +499,7 @@ function normalizeObsidianImages(markdown: string): string {
|
|
|
499
499
|
|
|
500
500
|
async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string> {
|
|
501
501
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
502
|
-
const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml"
|
|
502
|
+
const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml"];
|
|
503
503
|
const normalizedMarkdown = normalizeObsidianImages(normalizeMathDelimiters(markdown));
|
|
504
504
|
|
|
505
505
|
return await new Promise<string>((resolve, reject) => {
|
|
@@ -1217,6 +1217,29 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1217
1217
|
color: var(--ok);
|
|
1218
1218
|
}
|
|
1219
1219
|
|
|
1220
|
+
.hl-code-kw {
|
|
1221
|
+
color: var(--accent);
|
|
1222
|
+
font-weight: 600;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
.hl-code-str {
|
|
1226
|
+
color: var(--ok);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.hl-code-num {
|
|
1230
|
+
color: var(--warn);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
.hl-code-com {
|
|
1234
|
+
color: var(--muted);
|
|
1235
|
+
font-style: italic;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.hl-code-var,
|
|
1239
|
+
.hl-code-key {
|
|
1240
|
+
color: var(--accent);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1220
1243
|
.hl-list {
|
|
1221
1244
|
color: var(--accent);
|
|
1222
1245
|
font-weight: 600;
|
|
@@ -1329,6 +1352,48 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
1329
1352
|
padding: 0.12em 0.35em;
|
|
1330
1353
|
}
|
|
1331
1354
|
|
|
1355
|
+
.rendered-markdown code span.kw,
|
|
1356
|
+
.rendered-markdown code span.cf,
|
|
1357
|
+
.rendered-markdown code span.im,
|
|
1358
|
+
.rendered-markdown code span.dt {
|
|
1359
|
+
color: var(--accent);
|
|
1360
|
+
font-weight: 600;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.rendered-markdown code span.fu,
|
|
1364
|
+
.rendered-markdown code span.bu,
|
|
1365
|
+
.rendered-markdown code span.va,
|
|
1366
|
+
.rendered-markdown code span.ot {
|
|
1367
|
+
color: var(--accent);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.rendered-markdown code span.st,
|
|
1371
|
+
.rendered-markdown code span.ss,
|
|
1372
|
+
.rendered-markdown code span.sc {
|
|
1373
|
+
color: var(--ok);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.rendered-markdown code span.dv,
|
|
1377
|
+
.rendered-markdown code span.bn,
|
|
1378
|
+
.rendered-markdown code span.fl {
|
|
1379
|
+
color: var(--warn);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
.rendered-markdown code span.co {
|
|
1383
|
+
color: var(--muted);
|
|
1384
|
+
font-style: italic;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
.rendered-markdown code span.op {
|
|
1388
|
+
color: var(--text);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.rendered-markdown code span.er,
|
|
1392
|
+
.rendered-markdown code span.al {
|
|
1393
|
+
color: var(--error);
|
|
1394
|
+
font-weight: 600;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1332
1397
|
.rendered-markdown table {
|
|
1333
1398
|
border-collapse: collapse;
|
|
1334
1399
|
display: block;
|
|
@@ -2173,12 +2238,119 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2173
2238
|
return out;
|
|
2174
2239
|
}
|
|
2175
2240
|
|
|
2241
|
+
function normalizeFenceLanguage(info) {
|
|
2242
|
+
const raw = String(info || "").trim();
|
|
2243
|
+
if (!raw) return "";
|
|
2244
|
+
|
|
2245
|
+
const first = raw.split(/\\s+/)[0].replace(/^\\./, "").toLowerCase();
|
|
2246
|
+
|
|
2247
|
+
if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
|
|
2248
|
+
if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
|
|
2249
|
+
if (first === "py" || first === "python") return "python";
|
|
2250
|
+
if (first === "sh" || first === "bash" || first === "zsh" || first === "shell") return "bash";
|
|
2251
|
+
if (first === "json" || first === "jsonc") return "json";
|
|
2252
|
+
|
|
2253
|
+
return "";
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function highlightCodeTokens(line, pattern, classifyMatch) {
|
|
2257
|
+
const source = String(line || "");
|
|
2258
|
+
let out = "";
|
|
2259
|
+
let lastIndex = 0;
|
|
2260
|
+
pattern.lastIndex = 0;
|
|
2261
|
+
|
|
2262
|
+
let match;
|
|
2263
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
2264
|
+
const token = match[0] || "";
|
|
2265
|
+
const start = typeof match.index === "number" ? match.index : 0;
|
|
2266
|
+
|
|
2267
|
+
if (start > lastIndex) {
|
|
2268
|
+
out += escapeHtml(source.slice(lastIndex, start));
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const className = classifyMatch(match) || "hl-code";
|
|
2272
|
+
out += wrapHighlight(className, token);
|
|
2273
|
+
|
|
2274
|
+
lastIndex = start + token.length;
|
|
2275
|
+
if (token.length === 0) {
|
|
2276
|
+
pattern.lastIndex += 1;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
if (lastIndex < source.length) {
|
|
2281
|
+
out += escapeHtml(source.slice(lastIndex));
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
return out;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function highlightCodeLine(line, language) {
|
|
2288
|
+
const source = String(line || "");
|
|
2289
|
+
const lang = normalizeFenceLanguage(language);
|
|
2290
|
+
|
|
2291
|
+
if (!lang) {
|
|
2292
|
+
return wrapHighlight("hl-code", source);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (lang === "javascript" || lang === "typescript") {
|
|
2296
|
+
const jsPattern = /(\\/\\/.*$)|("(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*')|(\\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|from|export|default|async|await|true|false|null|undefined|typeof|instanceof)\\b)|(\\b\\d+(?:\\.\\d+)?\\b)/g;
|
|
2297
|
+
const highlighted = highlightCodeTokens(source, jsPattern, (match) => {
|
|
2298
|
+
if (match[1]) return "hl-code-com";
|
|
2299
|
+
if (match[2]) return "hl-code-str";
|
|
2300
|
+
if (match[3]) return "hl-code-kw";
|
|
2301
|
+
if (match[4]) return "hl-code-num";
|
|
2302
|
+
return "hl-code";
|
|
2303
|
+
});
|
|
2304
|
+
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (lang === "python") {
|
|
2308
|
+
const pyPattern = /(#.*$)|("(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*')|(\\b(?:def|class|return|if|elif|else|for|while|try|except|finally|import|from|as|with|lambda|yield|True|False|None|and|or|not|in|is|pass|break|continue|raise|global|nonlocal|assert)\\b)|(\\b\\d+(?:\\.\\d+)?\\b)/g;
|
|
2309
|
+
const highlighted = highlightCodeTokens(source, pyPattern, (match) => {
|
|
2310
|
+
if (match[1]) return "hl-code-com";
|
|
2311
|
+
if (match[2]) return "hl-code-str";
|
|
2312
|
+
if (match[3]) return "hl-code-kw";
|
|
2313
|
+
if (match[4]) return "hl-code-num";
|
|
2314
|
+
return "hl-code";
|
|
2315
|
+
});
|
|
2316
|
+
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (lang === "bash") {
|
|
2320
|
+
const shPattern = /(#.*$)|("(?:[^"\\\\]|\\\\.)*"|'[^']*')|(\\$\\{[^}]+\\}|\\$[A-Za-z_][A-Za-z0-9_]*)|(\\b(?:if|then|else|fi|for|in|do|done|case|esac|function|local|export|readonly|return|break|continue|while|until)\\b)|(\\b\\d+\\b)/g;
|
|
2321
|
+
const highlighted = highlightCodeTokens(source, shPattern, (match) => {
|
|
2322
|
+
if (match[1]) return "hl-code-com";
|
|
2323
|
+
if (match[2]) return "hl-code-str";
|
|
2324
|
+
if (match[3]) return "hl-code-var";
|
|
2325
|
+
if (match[4]) return "hl-code-kw";
|
|
2326
|
+
if (match[5]) return "hl-code-num";
|
|
2327
|
+
return "hl-code";
|
|
2328
|
+
});
|
|
2329
|
+
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (lang === "json") {
|
|
2333
|
+
const jsonPattern = /("(?:[^"\\\\]|\\\\.)*"\\s*:)|("(?:[^"\\\\]|\\\\.)*")|(\\b(?:true|false|null)\\b)|(\\b-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)/g;
|
|
2334
|
+
const highlighted = highlightCodeTokens(source, jsonPattern, (match) => {
|
|
2335
|
+
if (match[1]) return "hl-code-key";
|
|
2336
|
+
if (match[2]) return "hl-code-str";
|
|
2337
|
+
if (match[3]) return "hl-code-kw";
|
|
2338
|
+
if (match[4]) return "hl-code-num";
|
|
2339
|
+
return "hl-code";
|
|
2340
|
+
});
|
|
2341
|
+
return "<span class='hl-code'>" + highlighted + "</span>";
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
return wrapHighlight("hl-code", source);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2176
2347
|
function highlightMarkdown(text) {
|
|
2177
2348
|
const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
|
|
2178
2349
|
const out = [];
|
|
2179
2350
|
let inFence = false;
|
|
2180
2351
|
let fenceChar = null;
|
|
2181
2352
|
let fenceLength = 0;
|
|
2353
|
+
let fenceLanguage = "";
|
|
2182
2354
|
|
|
2183
2355
|
for (const line of lines) {
|
|
2184
2356
|
const fenceMatch = line.match(/^(\\s*)([\\x60]{3,}|~{3,})(.*)$/);
|
|
@@ -2191,10 +2363,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2191
2363
|
inFence = true;
|
|
2192
2364
|
fenceChar = markerChar;
|
|
2193
2365
|
fenceLength = markerLength;
|
|
2366
|
+
fenceLanguage = normalizeFenceLanguage(fenceMatch[3] || "");
|
|
2194
2367
|
} else if (fenceChar === markerChar && markerLength >= fenceLength) {
|
|
2195
2368
|
inFence = false;
|
|
2196
2369
|
fenceChar = null;
|
|
2197
2370
|
fenceLength = 0;
|
|
2371
|
+
fenceLanguage = "";
|
|
2198
2372
|
}
|
|
2199
2373
|
|
|
2200
2374
|
out.push(wrapHighlight("hl-fence", line));
|
|
@@ -2202,7 +2376,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
|
|
|
2202
2376
|
}
|
|
2203
2377
|
|
|
2204
2378
|
if (inFence) {
|
|
2205
|
-
out.push(line.length > 0 ?
|
|
2379
|
+
out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : "");
|
|
2206
2380
|
continue;
|
|
2207
2381
|
}
|
|
2208
2382
|
|