pi-studio 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/index.ts +337 -7
  3. package/package.json +9 -1
package/CHANGELOG.md CHANGED
@@ -43,6 +43,9 @@ All notable changes to `pi-studio` are documented here.
43
43
  - If `dompurify` is unavailable, preview now falls back to escaped plain markdown instead of injecting unsanitized HTML.
44
44
  - Preview sanitization now preserves MathML profile and strips MathML annotation tags to avoid duplicate raw TeX text beside rendered equations.
45
45
 
46
+ ### Changed
47
+ - Added npm metadata fields (`repository`, `homepage`, `bugs`) so npm package page links to GitHub.
48
+
46
49
  ## [0.1.0-alpha.1] - 2026-02-26
47
50
 
48
51
  Initial alpha baseline.
package/index.ts CHANGED
@@ -1143,10 +1143,91 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1143
1143
  font-size: 12px;
1144
1144
  }
1145
1145
 
1146
+ .editor-highlight-wrap {
1147
+ position: relative;
1148
+ display: flex;
1149
+ flex: 1 1 auto;
1150
+ min-height: 0;
1151
+ max-height: none;
1152
+ border: 1px solid var(--border);
1153
+ border-radius: 8px;
1154
+ background: var(--panel-2);
1155
+ overflow: hidden;
1156
+ }
1157
+
1158
+ .editor-highlight {
1159
+ position: absolute;
1160
+ inset: 0;
1161
+ margin: 0;
1162
+ border: 0;
1163
+ border-radius: 8px;
1164
+ padding: 10px;
1165
+ overflow: auto;
1166
+ pointer-events: none;
1167
+ white-space: pre-wrap;
1168
+ word-break: break-word;
1169
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1170
+ font-size: 13px;
1171
+ line-height: 1.45;
1172
+ color: var(--text);
1173
+ background: transparent;
1174
+ }
1175
+
1146
1176
  #sourceText {
1177
+ position: relative;
1178
+ z-index: 1;
1147
1179
  flex: 1 1 auto;
1148
1180
  min-height: 180px;
1149
1181
  max-height: none;
1182
+ border: 0;
1183
+ border-radius: 0;
1184
+ background: transparent;
1185
+ resize: none;
1186
+ }
1187
+
1188
+ #sourceText.highlight-active {
1189
+ color: transparent;
1190
+ -webkit-text-fill-color: transparent;
1191
+ caret-color: var(--text);
1192
+ background: transparent;
1193
+ }
1194
+
1195
+ #sourceText.highlight-active::selection {
1196
+ background: var(--accent-soft);
1197
+ color: transparent;
1198
+ -webkit-text-fill-color: transparent;
1199
+ }
1200
+
1201
+ .hl-heading {
1202
+ color: var(--accent);
1203
+ font-weight: 700;
1204
+ }
1205
+
1206
+ .hl-fence {
1207
+ color: var(--muted);
1208
+ }
1209
+
1210
+ .hl-code {
1211
+ color: var(--ok);
1212
+ }
1213
+
1214
+ .hl-list {
1215
+ color: var(--accent);
1216
+ font-weight: 600;
1217
+ }
1218
+
1219
+ .hl-quote {
1220
+ color: var(--muted);
1221
+ font-style: italic;
1222
+ }
1223
+
1224
+ .hl-link {
1225
+ color: var(--accent);
1226
+ text-decoration: underline;
1227
+ }
1228
+
1229
+ .hl-url {
1230
+ color: var(--muted);
1150
1231
  }
1151
1232
 
1152
1233
  #sourcePreview {
@@ -1407,9 +1488,16 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1407
1488
  <button id="critiqueBtn" type="button">Critique editor text</button>
1408
1489
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
1409
1490
  <button id="copyDraftBtn" type="button">Copy editor</button>
1491
+ <select id="highlightSelect" aria-label="Editor syntax highlighting">
1492
+ <option value="off" selected>Highlight editor: Off</option>
1493
+ <option value="on">Highlight editor: On</option>
1494
+ </select>
1410
1495
  </div>
1411
1496
  </div>
1412
- <textarea id="sourceText" placeholder="Paste or edit text here.">${initialText}</textarea>
1497
+ <div id="sourceEditorWrap" class="editor-highlight-wrap">
1498
+ <pre id="sourceHighlight" class="editor-highlight" aria-hidden="true"></pre>
1499
+ <textarea id="sourceText" placeholder="Paste or edit text here.">${initialText}</textarea>
1500
+ </div>
1413
1501
  <div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
1414
1502
  </div>
1415
1503
  </section>
@@ -1467,7 +1555,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1467
1555
  });
1468
1556
 
1469
1557
  try {
1558
+ const sourceEditorWrapEl = document.getElementById("sourceEditorWrap");
1470
1559
  const sourceTextEl = document.getElementById("sourceText");
1560
+ const sourceHighlightEl = document.getElementById("sourceHighlight");
1471
1561
  const sourcePreviewEl = document.getElementById("sourcePreview");
1472
1562
  const leftPaneEl = document.getElementById("leftPane");
1473
1563
  const rightPaneEl = document.getElementById("rightPane");
@@ -1494,6 +1584,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1494
1584
  const sendEditorBtn = document.getElementById("sendEditorBtn");
1495
1585
  const sendRunBtn = document.getElementById("sendRunBtn");
1496
1586
  const copyDraftBtn = document.getElementById("copyDraftBtn");
1587
+ const highlightSelect = document.getElementById("highlightSelect");
1497
1588
 
1498
1589
  const initialSourceState = {
1499
1590
  source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
@@ -1524,9 +1615,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1524
1615
  };
1525
1616
  let activePane = "left";
1526
1617
  let paneFocusTarget = "off";
1618
+ const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
1619
+ const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
1527
1620
  let sourcePreviewRenderTimer = null;
1528
1621
  let sourcePreviewRenderNonce = 0;
1529
1622
  let responsePreviewRenderNonce = 0;
1623
+ let editorHighlightEnabled = false;
1624
+ let editorHighlightRenderRaf = null;
1530
1625
 
1531
1626
  function getIdleStatus() {
1532
1627
  return "Ready. Edit text, then run or critique (insert annotation header if needed).";
@@ -1829,7 +1924,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1829
1924
  }
1830
1925
 
1831
1926
  function renderSourcePreview() {
1832
- scheduleSourcePreviewRender(0);
1927
+ if (editorView === "preview") {
1928
+ scheduleSourcePreviewRender(0);
1929
+ }
1930
+ if (editorHighlightEnabled && editorView === "markdown") {
1931
+ scheduleEditorHighlightRender();
1932
+ }
1833
1933
  }
1834
1934
 
1835
1935
  function renderActiveResult() {
@@ -1901,6 +2001,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1901
2001
  sendEditorBtn.disabled = uiBusy;
1902
2002
  sendRunBtn.disabled = uiBusy;
1903
2003
  copyDraftBtn.disabled = uiBusy;
2004
+ if (highlightSelect) highlightSelect.disabled = uiBusy;
1904
2005
  editorViewSelect.disabled = uiBusy;
1905
2006
  rightViewSelect.disabled = uiBusy;
1906
2007
  followSelect.disabled = uiBusy;
@@ -1928,17 +2029,23 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1928
2029
  function setEditorView(nextView) {
1929
2030
  editorView = nextView === "preview" ? "preview" : "markdown";
1930
2031
  editorViewSelect.value = editorView;
1931
- sourceTextEl.hidden = editorView === "preview";
1932
- sourcePreviewEl.hidden = editorView !== "preview";
1933
2032
 
1934
- if (editorView !== "preview" && sourcePreviewRenderTimer) {
2033
+ const showPreview = editorView === "preview";
2034
+ if (sourceEditorWrapEl) {
2035
+ sourceEditorWrapEl.style.display = showPreview ? "none" : "flex";
2036
+ }
2037
+ sourcePreviewEl.hidden = !showPreview;
2038
+
2039
+ if (!showPreview && sourcePreviewRenderTimer) {
1935
2040
  window.clearTimeout(sourcePreviewRenderTimer);
1936
2041
  sourcePreviewRenderTimer = null;
1937
2042
  }
1938
2043
 
1939
- if (editorView === "preview") {
2044
+ if (showPreview) {
1940
2045
  renderSourcePreview();
1941
2046
  }
2047
+
2048
+ updateEditorHighlightState();
1942
2049
  }
1943
2050
 
1944
2051
  function setRightView(nextView) {
@@ -1969,6 +2076,213 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1969
2076
  .replace(/'/g, "&#39;");
1970
2077
  }
1971
2078
 
2079
+ function wrapHighlight(className, text) {
2080
+ return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
2081
+ }
2082
+
2083
+ function highlightInlineMarkdown(text) {
2084
+ const source = String(text || "");
2085
+ const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))/g;
2086
+ let lastIndex = 0;
2087
+ let out = "";
2088
+
2089
+ let match;
2090
+ while ((match = pattern.exec(source)) !== null) {
2091
+ const token = match[0] || "";
2092
+ const start = typeof match.index === "number" ? match.index : 0;
2093
+
2094
+ if (start > lastIndex) {
2095
+ out += escapeHtml(source.slice(lastIndex, start));
2096
+ }
2097
+
2098
+ if (match[1]) {
2099
+ out += wrapHighlight("hl-code", token);
2100
+ } else if (match[2]) {
2101
+ const linkMatch = token.match(/^\\[([^\\]]+)\\]\\(([^)]+)\\)$/);
2102
+ if (linkMatch) {
2103
+ out += wrapHighlight("hl-link", "[" + linkMatch[1] + "]");
2104
+ out += "(" + wrapHighlight("hl-url", linkMatch[2]) + ")";
2105
+ } else {
2106
+ out += escapeHtml(token);
2107
+ }
2108
+ } else {
2109
+ out += escapeHtml(token);
2110
+ }
2111
+
2112
+ lastIndex = start + token.length;
2113
+ }
2114
+
2115
+ if (lastIndex < source.length) {
2116
+ out += escapeHtml(source.slice(lastIndex));
2117
+ }
2118
+
2119
+ return out;
2120
+ }
2121
+
2122
+ function highlightMarkdown(text) {
2123
+ const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
2124
+ const out = [];
2125
+ let inFence = false;
2126
+ let fenceChar = null;
2127
+ let fenceLength = 0;
2128
+
2129
+ for (const line of lines) {
2130
+ const fenceMatch = line.match(/^(\\s*)([\\x60]{3,}|~{3,})(.*)$/);
2131
+ if (fenceMatch) {
2132
+ const marker = fenceMatch[2] || "";
2133
+ const markerChar = marker.charAt(0);
2134
+ const markerLength = marker.length;
2135
+
2136
+ if (!inFence) {
2137
+ inFence = true;
2138
+ fenceChar = markerChar;
2139
+ fenceLength = markerLength;
2140
+ } else if (fenceChar === markerChar && markerLength >= fenceLength) {
2141
+ inFence = false;
2142
+ fenceChar = null;
2143
+ fenceLength = 0;
2144
+ }
2145
+
2146
+ out.push(wrapHighlight("hl-fence", line));
2147
+ continue;
2148
+ }
2149
+
2150
+ if (inFence) {
2151
+ out.push(line.length > 0 ? wrapHighlight("hl-code", line) : "");
2152
+ continue;
2153
+ }
2154
+
2155
+ const headingMatch = line.match(/^(\\s{0,3})(#{1,6}\\s+)(.*)$/);
2156
+ if (headingMatch) {
2157
+ out.push(escapeHtml(headingMatch[1] || "") + wrapHighlight("hl-heading", (headingMatch[2] || "") + (headingMatch[3] || "")));
2158
+ continue;
2159
+ }
2160
+
2161
+ const quoteMatch = line.match(/^(\\s{0,3}>\\s?)(.*)$/);
2162
+ if (quoteMatch) {
2163
+ out.push(wrapHighlight("hl-quote", quoteMatch[1] || "") + highlightInlineMarkdown(quoteMatch[2] || ""));
2164
+ continue;
2165
+ }
2166
+
2167
+ const listMatch = line.match(/^(\\s*)([-*+]|\\d+\\.)(\\s+)(.*)$/);
2168
+ if (listMatch) {
2169
+ out.push(
2170
+ escapeHtml(listMatch[1] || "")
2171
+ + wrapHighlight("hl-list", listMatch[2] || "")
2172
+ + escapeHtml(listMatch[3] || "")
2173
+ + highlightInlineMarkdown(listMatch[4] || ""),
2174
+ );
2175
+ continue;
2176
+ }
2177
+
2178
+ out.push(highlightInlineMarkdown(line));
2179
+ }
2180
+
2181
+ return out.join("<br>");
2182
+ }
2183
+
2184
+ function renderEditorHighlightNow() {
2185
+ if (!sourceHighlightEl) return;
2186
+ if (!editorHighlightEnabled || editorView !== "markdown") {
2187
+ sourceHighlightEl.innerHTML = "";
2188
+ return;
2189
+ }
2190
+
2191
+ const text = sourceTextEl.value || "";
2192
+ if (text.length > EDITOR_HIGHLIGHT_MAX_CHARS) {
2193
+ sourceHighlightEl.textContent = text;
2194
+ syncEditorHighlightScroll();
2195
+ return;
2196
+ }
2197
+
2198
+ sourceHighlightEl.innerHTML = highlightMarkdown(text);
2199
+ syncEditorHighlightScroll();
2200
+ }
2201
+
2202
+ function scheduleEditorHighlightRender() {
2203
+ if (editorHighlightRenderRaf !== null) {
2204
+ if (typeof window.cancelAnimationFrame === "function") {
2205
+ window.cancelAnimationFrame(editorHighlightRenderRaf);
2206
+ } else {
2207
+ window.clearTimeout(editorHighlightRenderRaf);
2208
+ }
2209
+ editorHighlightRenderRaf = null;
2210
+ }
2211
+
2212
+ const schedule = typeof window.requestAnimationFrame === "function"
2213
+ ? window.requestAnimationFrame.bind(window)
2214
+ : (cb) => window.setTimeout(cb, 16);
2215
+
2216
+ editorHighlightRenderRaf = schedule(() => {
2217
+ editorHighlightRenderRaf = null;
2218
+ renderEditorHighlightNow();
2219
+ });
2220
+ }
2221
+
2222
+ function syncEditorHighlightScroll() {
2223
+ if (!sourceHighlightEl) return;
2224
+ sourceHighlightEl.scrollTop = sourceTextEl.scrollTop;
2225
+ sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
2226
+ }
2227
+
2228
+ function readStoredEditorHighlightEnabled() {
2229
+ if (!window.localStorage) return false;
2230
+ try {
2231
+ return window.localStorage.getItem(EDITOR_HIGHLIGHT_STORAGE_KEY) === "on";
2232
+ } catch {
2233
+ return false;
2234
+ }
2235
+ }
2236
+
2237
+ function persistEditorHighlightEnabled(enabled) {
2238
+ if (!window.localStorage) return;
2239
+ try {
2240
+ window.localStorage.setItem(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled ? "on" : "off");
2241
+ } catch {
2242
+ // ignore storage failures
2243
+ }
2244
+ }
2245
+
2246
+ function updateEditorHighlightState() {
2247
+ const enabled = editorHighlightEnabled && editorView === "markdown";
2248
+
2249
+ sourceTextEl.classList.toggle("highlight-active", enabled);
2250
+
2251
+ if (sourceHighlightEl) {
2252
+ sourceHighlightEl.hidden = !enabled;
2253
+ }
2254
+
2255
+ if (!enabled) {
2256
+ if (editorHighlightRenderRaf !== null) {
2257
+ if (typeof window.cancelAnimationFrame === "function") {
2258
+ window.cancelAnimationFrame(editorHighlightRenderRaf);
2259
+ } else {
2260
+ window.clearTimeout(editorHighlightRenderRaf);
2261
+ }
2262
+ editorHighlightRenderRaf = null;
2263
+ }
2264
+
2265
+ if (sourceHighlightEl) {
2266
+ sourceHighlightEl.innerHTML = "";
2267
+ sourceHighlightEl.scrollTop = 0;
2268
+ sourceHighlightEl.scrollLeft = 0;
2269
+ }
2270
+ return;
2271
+ }
2272
+
2273
+ scheduleEditorHighlightRender();
2274
+ syncEditorHighlightScroll();
2275
+ }
2276
+
2277
+ function setEditorHighlightEnabled(enabled) {
2278
+ editorHighlightEnabled = Boolean(enabled);
2279
+ persistEditorHighlightEnabled(editorHighlightEnabled);
2280
+ if (highlightSelect) {
2281
+ highlightSelect.value = editorHighlightEnabled ? "on" : "off";
2282
+ }
2283
+ updateEditorHighlightState();
2284
+ }
2285
+
1972
2286
  function extractSection(markdown, title) {
1973
2287
  if (!markdown || !title) return "";
1974
2288
 
@@ -2406,6 +2720,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2406
2720
  updateResultActionButtons();
2407
2721
  });
2408
2722
 
2723
+ if (highlightSelect) {
2724
+ highlightSelect.addEventListener("change", () => {
2725
+ setEditorHighlightEnabled(highlightSelect.value === "on");
2726
+ });
2727
+ }
2728
+
2409
2729
  pullLatestBtn.addEventListener("click", () => {
2410
2730
  if (queuedLatestResponse) {
2411
2731
  if (applyLatestPayload(queuedLatestResponse)) {
@@ -2419,10 +2739,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2419
2739
  });
2420
2740
 
2421
2741
  sourceTextEl.addEventListener("input", () => {
2422
- scheduleSourcePreviewRender();
2742
+ renderSourcePreview();
2423
2743
  updateResultActionButtons();
2424
2744
  });
2425
2745
 
2746
+ sourceTextEl.addEventListener("scroll", () => {
2747
+ if (!editorHighlightEnabled || editorView !== "markdown") return;
2748
+ syncEditorHighlightScroll();
2749
+ });
2750
+
2426
2751
  insertHeaderBtn.addEventListener("click", () => {
2427
2752
  insertOrUpdateAnnotationHeader();
2428
2753
  });
@@ -2647,6 +2972,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2647
2972
  setSourceState(initialSourceState);
2648
2973
  refreshResponseUi();
2649
2974
  setActivePane("left");
2975
+
2976
+ const initialHighlightEnabled = readStoredEditorHighlightEnabled()
2977
+ || Boolean(highlightSelect && highlightSelect.value === "on");
2978
+ setEditorHighlightEnabled(initialHighlightEnabled);
2979
+
2650
2980
  setEditorView(editorView);
2651
2981
  setRightView(rightView);
2652
2982
  renderSourcePreview();
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/omaclaren/pi-studio.git"
10
+ },
11
+ "homepage": "https://github.com/omaclaren/pi-studio#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/omaclaren/pi-studio/issues"
14
+ },
7
15
  "keywords": [
8
16
  "pi-package"
9
17
  ],