pi-studio 0.5.46 → 0.5.48

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,25 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.48] — 2026-04-07
8
+
9
+ ### Added
10
+ - Editor-preview comments now also work for code, plain-text, and diff files using line-based preview selection anchors, so code/text review can use the same local comments workflow as Markdown preview.
11
+
12
+ ### Fixed
13
+ - LaTeX editor preview now correctly treats `.tex`/LaTeX editor content as LaTeX in preview even when the document is only a fragment rather than a full `\documentclass` wrapper.
14
+ - Ordinary response preview no longer flips into LaTeX mode just because quoted LaTeX commands like `` `\\documentclass` `` or `` `\\begin{document}` `` appear in a normal response.
15
+
16
+ ### Changed
17
+ - LaTeX preview comments remain disabled for now; the earlier heuristic preview-side mapping attempts were removed in favor of keeping the rendering fixes and waiting for a more pipeline-grounded LaTeX comment design.
18
+
19
+ ## [0.5.47] — 2026-04-07
20
+
21
+ ### Changed
22
+ - Raw-editor and editor-preview commenting now feel more unified: both surfaces use a contextual **Comment** action for selected text, while the dock footer keeps a de-emphasised **Line comment** fallback for current-line comments in **Editor (Raw)**.
23
+ - Preview-side **Comment** affordances now focus the new comment textarea more reliably after creation, including when the comments rail has to open.
24
+ - Clicking comment **Jump** now suppresses the raw-editor selection **Comment** pill for that programmatic selection, so jump-to-highlight does not look like a fresh comment prompt.
25
+
7
26
  ## [0.5.46] — 2026-04-07
8
27
 
9
28
  ### Added
package/README.md CHANGED
@@ -18,7 +18,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
18
18
  - Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces
19
19
  - Runs editor text directly, or asks for structured critique (auto/writing/code focus)
20
20
  - Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
21
- - Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor and editor-preview selections plus optional inline `[an: ...]` toggles when you want them in the document text
21
+ - Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor selections plus editor-preview selections for Markdown and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
22
22
  - Browses response history (`Prev/Next/Last`) and loads either:
23
23
  - response text
24
24
  - critique notes/full critique
@@ -306,6 +306,9 @@
306
306
  let pendingReviewNoteFocusId = null;
307
307
  let pendingReviewNoteInlineFocusId = null;
308
308
  let activePreviewCommentSelection = null;
309
+ let suppressEditorSelectionComment = false;
310
+ let suppressedEditorSelectionStart = null;
311
+ let suppressedEditorSelectionEnd = null;
309
312
  const previewJumpHighlightState = new WeakMap();
310
313
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
311
314
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
@@ -1531,19 +1534,25 @@
1531
1534
 
1532
1535
  function sanitizeRenderedHtml(html, markdown) {
1533
1536
  const rawHtml = typeof html === "string" ? html : "";
1534
- const mathAnnotationStripped = rawHtml
1535
- .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
1536
- .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
1537
+ const mathAnnotationPreserved = rawHtml.replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (match, attrs, inner) => {
1538
+ const texAnnotationMatch = String(inner || "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
1539
+ const texSource = texAnnotationMatch ? String(texAnnotationMatch[1] || "").trim() : "";
1540
+ const cleanedInner = String(inner || "")
1541
+ .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
1542
+ .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
1543
+ const texAttr = texSource ? (" data-tex-source=\"" + escapeHtml(texSource) + "\"") : "";
1544
+ return "<math" + attrs + texAttr + ">" + cleanedInner + "</math>";
1545
+ });
1537
1546
 
1538
1547
  if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") {
1539
- return window.DOMPurify.sanitize(mathAnnotationStripped, {
1548
+ return window.DOMPurify.sanitize(mathAnnotationPreserved, {
1540
1549
  USE_PROFILES: {
1541
1550
  html: true,
1542
1551
  mathMl: true,
1543
1552
  svg: true,
1544
1553
  },
1545
1554
  ADD_TAGS: ["embed"],
1546
- ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
1555
+ ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align", "data-tex-source"],
1547
1556
  ADD_DATA_URI_TAGS: ["embed"],
1548
1557
  });
1549
1558
  }
@@ -2272,7 +2281,7 @@
2272
2281
  }
2273
2282
  }
2274
2283
 
2275
- async function renderMarkdownWithPandoc(markdown) {
2284
+ async function renderMarkdownWithPandoc(markdown, options) {
2276
2285
  const token = getToken();
2277
2286
  if (!token) {
2278
2287
  throw new Error("Missing Studio token in URL.");
@@ -2285,20 +2294,26 @@
2285
2294
  const controller = typeof AbortController === "function" ? new AbortController() : null;
2286
2295
  const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
2287
2296
 
2297
+ const previewOptions = options && typeof options === "object" ? options : {};
2298
+
2288
2299
  let response;
2289
2300
  try {
2290
2301
  const effectivePath = getEffectiveSavePath();
2291
2302
  const sourcePath = effectivePath || sourceState.path || "";
2303
+ const payload = {
2304
+ markdown: String(markdown || ""),
2305
+ sourcePath: sourcePath,
2306
+ resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
2307
+ };
2308
+ if (previewOptions.includeEditorLanguage) {
2309
+ payload.editorLanguage = String(editorLanguage || "");
2310
+ }
2292
2311
  response = await fetch("/render-preview?token=" + encodeURIComponent(token), {
2293
2312
  method: "POST",
2294
2313
  headers: {
2295
2314
  "Content-Type": "application/json",
2296
2315
  },
2297
- body: JSON.stringify({
2298
- markdown: String(markdown || ""),
2299
- sourcePath: sourcePath,
2300
- resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
2301
- }),
2316
+ body: JSON.stringify(payload),
2302
2317
  signal: controller ? controller.signal : undefined,
2303
2318
  });
2304
2319
  } catch (error) {
@@ -2523,7 +2538,9 @@
2523
2538
  : { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
2524
2539
 
2525
2540
  try {
2526
- const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown);
2541
+ const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown, {
2542
+ includeEditorLanguage: pane === "source" || rightView === "editor-preview",
2543
+ });
2527
2544
 
2528
2545
  if (pane === "source") {
2529
2546
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -2588,9 +2605,8 @@
2588
2605
  function renderSourcePreviewNow() {
2589
2606
  if (editorView !== "preview") return;
2590
2607
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
2591
- if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
2592
- finishPreviewRender(sourcePreviewEl);
2593
- sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
2608
+ if (supportsCodePreviewCommentsForCurrentEditor()) {
2609
+ renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
2594
2610
  return;
2595
2611
  }
2596
2612
  const nonce = ++sourcePreviewRenderNonce;
@@ -2657,10 +2673,8 @@
2657
2673
  scheduleResponsePaneRepaintNudge();
2658
2674
  return;
2659
2675
  }
2660
- if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
2661
- finishPreviewRender(critiqueViewEl);
2662
- critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
2663
- scheduleResponsePaneRepaintNudge();
2676
+ if (supportsCodePreviewCommentsForCurrentEditor()) {
2677
+ renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
2664
2678
  return;
2665
2679
  }
2666
2680
  const nonce = ++responsePreviewRenderNonce;
@@ -3795,6 +3809,68 @@
3795
3809
  return out.join("<br>");
3796
3810
  }
3797
3811
 
3812
+ function supportsCodePreviewCommentsForCurrentEditor() {
3813
+ return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
3814
+ }
3815
+
3816
+ function getCodePreviewCommentKind(language) {
3817
+ const lang = normalizeFenceLanguage(language || "");
3818
+ if (lang === "diff") return "diff-line";
3819
+ if (lang === "text") return "text-line";
3820
+ return "code-line";
3821
+ }
3822
+
3823
+ function buildCodePreviewHtmlWithCommentBlocks(text, language) {
3824
+ const source = String(text || "").replace(/\r\n/g, "\n");
3825
+ const lines = source.split("\n");
3826
+ const lang = normalizeFenceLanguage(language || "");
3827
+ const kind = getCodePreviewCommentKind(lang);
3828
+ const html = [];
3829
+ let offset = 0;
3830
+
3831
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
3832
+ const line = String(lines[lineIndex] || "");
3833
+ const start = offset;
3834
+ const end = start + line.length;
3835
+ const lineNumber = lineIndex + 1;
3836
+ const lineHtml = line.length === 0
3837
+ ? "<span class='hl-code'>" + EMPTY_OVERLAY_LINE + "</span>"
3838
+ : (lang ? highlightCodeLine(line, lang, "preview") : escapeHtml(line));
3839
+
3840
+ html.push(
3841
+ "<div class='preview-comment-block preview-comment-line-block'"
3842
+ + " data-review-note-start='" + String(start) + "'"
3843
+ + " data-review-note-end='" + String(end) + "'"
3844
+ + " data-review-note-line-start='" + String(lineNumber) + "'"
3845
+ + " data-review-note-line-end='" + String(lineNumber) + "'"
3846
+ + " data-preview-comment-kind='" + escapeHtml(kind) + "'"
3847
+ + ">"
3848
+ + "<div class='preview-comment-controls'>"
3849
+ + "<button type='button' class='preview-comment-summary' hidden></button>"
3850
+ + "<button type='button' class='preview-comment-add'>Comment</button>"
3851
+ + "</div>"
3852
+ + "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
3853
+ + "</div>",
3854
+ );
3855
+
3856
+ offset = end + 1;
3857
+ }
3858
+
3859
+ return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
3860
+ }
3861
+
3862
+ function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
3863
+ if (!targetEl) return;
3864
+ clearPreviewJumpHighlight(targetEl);
3865
+ finishPreviewRender(targetEl);
3866
+ targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
3867
+ updatePreviewCommentBlocksForElement(targetEl);
3868
+ if (pane === "response") {
3869
+ applyPendingResponseScrollReset();
3870
+ scheduleResponsePaneRepaintNudge();
3871
+ }
3872
+ }
3873
+
3798
3874
  function detectLanguageFromName(name) {
3799
3875
  if (!name) return "";
3800
3876
  var dot = name.lastIndexOf(".");
@@ -4358,7 +4434,10 @@
4358
4434
  }
4359
4435
 
4360
4436
  function supportsPreviewCommentsForCurrentEditor() {
4361
- return editorLanguage === "markdown";
4437
+ // LaTeX preview comments are intentionally disabled for now.
4438
+ // The initial client-side source/block heuristics were too unreliable on complex real documents.
4439
+ // Keep the independent LaTeX rendering fixes, but only enable preview comments where mapping is robust.
4440
+ return editorLanguage === "markdown" || supportsCodePreviewCommentsForCurrentEditor();
4362
4441
  }
4363
4442
 
4364
4443
  function getPreviewCommentBlockKindLabel(kind) {
@@ -4367,11 +4446,20 @@
4367
4446
  if (kind === "list") return "list";
4368
4447
  if (kind === "code") return "code block";
4369
4448
  if (kind === "table") return "table";
4449
+ if (kind === "code-line") return "code line";
4450
+ if (kind === "diff-line") return "diff line";
4451
+ if (kind === "text-line") return "text line";
4370
4452
  return "paragraph";
4371
4453
  }
4372
4454
 
4373
4455
  function supportsPreviewSelectionCommentsForBlockKind(kind) {
4374
- return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
4456
+ return kind === "paragraph"
4457
+ || kind === "heading"
4458
+ || kind === "blockquote"
4459
+ || kind === "list"
4460
+ || kind === "code-line"
4461
+ || kind === "diff-line"
4462
+ || kind === "text-line";
4375
4463
  }
4376
4464
 
4377
4465
  function normalizeVisiblePreviewText(text) {
@@ -4708,6 +4796,11 @@
4708
4796
  return safeStartA < safeEndB && safeStartB < safeEndA;
4709
4797
  }
4710
4798
 
4799
+ function scanSourcePreviewCommentBlocks(markdown) {
4800
+ if (editorLanguage !== "markdown") return [];
4801
+ return scanMarkdownPreviewCommentBlocks(markdown);
4802
+ }
4803
+
4711
4804
  function scanMarkdownPreviewCommentBlocks(markdown) {
4712
4805
  const source = String(markdown || "").replace(/\r\n/g, "\n");
4713
4806
  const lines = source.split("\n");
@@ -5074,7 +5167,7 @@
5074
5167
 
5075
5168
  function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5076
5169
  if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5077
- const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
5170
+ const sourceBlocks = scanSourcePreviewCommentBlocks(sourceText);
5078
5171
  const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5079
5172
  if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
5080
5173
 
@@ -5268,12 +5361,43 @@
5268
5361
  return bestBlock;
5269
5362
  }
5270
5363
 
5364
+ function getPreviewNoteNormalizedSelectionText(note) {
5365
+ const direct = normalizeVisiblePreviewText(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "");
5366
+ if (direct) return direct;
5367
+ return "";
5368
+ }
5369
+
5370
+ function findPreviewCommentBlockForNoteText(targetEl, note) {
5371
+ if (!targetEl || !note || typeof targetEl.querySelectorAll !== "function") return null;
5372
+ const selectionText = getPreviewNoteNormalizedSelectionText(note);
5373
+ if (!selectionText) return null;
5374
+
5375
+ let bestBlock = null;
5376
+ let bestScore = Number.NEGATIVE_INFINITY;
5377
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5378
+ const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5379
+ const blockText = normalizeVisiblePreviewText(buildNormalizedDomTextMap(contentEl).text);
5380
+ if (!blockText) return;
5381
+ const matchIndex = blockText.indexOf(selectionText);
5382
+ if (matchIndex < 0) return;
5383
+ const lineStart = Math.max(1, Number(blockEl.dataset && blockEl.dataset.reviewNoteLineStart) || 1);
5384
+ const desiredLine = Math.max(1, Number(note && note.lineStart) || 1);
5385
+ const proximityPenalty = Math.abs(lineStart - desiredLine);
5386
+ const score = 1000000 - (matchIndex * 4) - proximityPenalty - Math.max(0, blockText.length - selectionText.length);
5387
+ if (score > bestScore) {
5388
+ bestScore = score;
5389
+ bestBlock = blockEl;
5390
+ }
5391
+ });
5392
+ return bestBlock;
5393
+ }
5394
+
5271
5395
  function revealReviewNoteInPreviewElement(targetEl, note) {
5272
5396
  if (!targetEl || !note) return false;
5273
5397
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5274
5398
  const range = resolveReviewNoteRange(note, source);
5275
5399
  if (!range) return false;
5276
- const blockEl = findPreviewCommentBlockForRange(targetEl, range);
5400
+ const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
5277
5401
  if (!blockEl) return false;
5278
5402
  const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5279
5403
  const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
@@ -5285,6 +5409,7 @@
5285
5409
  }
5286
5410
 
5287
5411
  function revealReviewNoteInPreview(note) {
5412
+ if (!supportsPreviewCommentsForCurrentEditor()) return;
5288
5413
  if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
5289
5414
  revealReviewNoteInPreviewElement(critiqueViewEl, note);
5290
5415
  }
@@ -5407,7 +5532,8 @@
5407
5532
  function updateEditorSelectionCommentUi() {
5408
5533
  if (!editorSelectionCommentBtn) return;
5409
5534
  const hasSelection = Boolean(
5410
- editorView === "markdown"
5535
+ !suppressEditorSelectionComment
5536
+ && editorView === "markdown"
5411
5537
  && document.activeElement === sourceTextEl
5412
5538
  && typeof sourceTextEl.selectionStart === "number"
5413
5539
  && typeof sourceTextEl.selectionEnd === "number"
@@ -5420,6 +5546,14 @@
5420
5546
  }
5421
5547
  }
5422
5548
 
5549
+ function clearSuppressedEditorSelectionComment() {
5550
+ if (!suppressEditorSelectionComment) return;
5551
+ suppressEditorSelectionComment = false;
5552
+ suppressedEditorSelectionStart = null;
5553
+ suppressedEditorSelectionEnd = null;
5554
+ updateEditorSelectionCommentUi();
5555
+ }
5556
+
5423
5557
  function updateReviewNotesUi() {
5424
5558
  const descriptor = getCurrentStudioDocumentDescriptor();
5425
5559
  const count = reviewNotes.length;
@@ -5667,9 +5801,11 @@
5667
5801
  if (editorSelectionCommentBtn) {
5668
5802
  editorSelectionCommentBtn.hidden = true;
5669
5803
  }
5804
+ const shouldOpenReviewNotes = !isReviewNotesOpen();
5670
5805
  pendingReviewNoteFocusId = note.id;
5671
5806
  setReviewNotes(reviewNotes.concat([note]));
5672
- if (!isReviewNotesOpen()) {
5807
+ if (shouldOpenReviewNotes) {
5808
+ pendingReviewNoteFocusId = note.id;
5673
5809
  openReviewNotes();
5674
5810
  }
5675
5811
  const schedule = typeof window.requestAnimationFrame === "function"
@@ -5713,6 +5849,10 @@
5713
5849
  setStatus("Could not find the anchored location for this comment.", "warning");
5714
5850
  return;
5715
5851
  }
5852
+ suppressEditorSelectionComment = true;
5853
+ suppressedEditorSelectionStart = range.start;
5854
+ suppressedEditorSelectionEnd = range.end;
5855
+ updateEditorSelectionCommentUi();
5716
5856
  setEditorView("markdown");
5717
5857
  setActivePane("left");
5718
5858
  sourceTextEl.focus();
@@ -5723,6 +5863,7 @@
5723
5863
  schedule(() => {
5724
5864
  scrollEditorRangeIntoView(range);
5725
5865
  revealReviewNoteInPreview(note);
5866
+ updateEditorSelectionCommentUi();
5726
5867
  });
5727
5868
  }
5728
5869
 
@@ -7218,6 +7359,7 @@
7218
7359
  if (activePreviewCommentSelection) {
7219
7360
  clearPreviewCommentSelection();
7220
7361
  }
7362
+ clearSuppressedEditorSelectionComment();
7221
7363
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
7222
7364
  scheduleEditorMetaUpdate();
7223
7365
  updateEditorSelectionCommentUi();
@@ -7228,6 +7370,14 @@
7228
7370
  });
7229
7371
 
7230
7372
  sourceTextEl.addEventListener("select", () => {
7373
+ if (suppressEditorSelectionComment) {
7374
+ const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
7375
+ const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
7376
+ const matchesSuppressedSelection = selectionStart === suppressedEditorSelectionStart && selectionEnd === suppressedEditorSelectionEnd;
7377
+ if (!matchesSuppressedSelection && selectionEnd > selectionStart) {
7378
+ clearSuppressedEditorSelectionComment();
7379
+ }
7380
+ }
7231
7381
  updateEditorSelectionCommentUi();
7232
7382
  });
7233
7383
 
@@ -7661,7 +7811,7 @@
7661
7811
  event.preventDefault();
7662
7812
  event.stopPropagation();
7663
7813
  const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7664
- if (mode !== "selection") return;
7814
+ if (!mode || !mode.startsWith("selection")) return;
7665
7815
  addReviewNoteFromPreviewSelection(blockEl);
7666
7816
  }
7667
7817
 
package/client/studio.css CHANGED
@@ -898,6 +898,24 @@
898
898
  scroll-margin-top: 24px;
899
899
  }
900
900
 
901
+ .preview-comment-line-block {
902
+ min-height: 1.5em;
903
+ }
904
+
905
+ .preview-code-lines {
906
+ white-space: pre-wrap;
907
+ word-break: break-word;
908
+ overflow-wrap: anywhere;
909
+ }
910
+
911
+ .preview-code-line-content {
912
+ display: block;
913
+ min-height: 1.5em;
914
+ white-space: inherit;
915
+ word-break: inherit;
916
+ overflow-wrap: inherit;
917
+ }
918
+
901
919
  .preview-comment-controls {
902
920
  position: absolute;
903
921
  top: 0;
package/index.ts CHANGED
@@ -10,6 +10,7 @@ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path
10
10
  import { URL, pathToFileURL } from "node:url";
11
11
  import { WebSocketServer, WebSocket, type RawData } from "ws";
12
12
  import {
13
+ advancePastStudioInlineBacktickSpan,
13
14
  collectStudioInlineAnnotationMarkers,
14
15
  hasStudioMarkdownAnnotationMarkers,
15
16
  isStudioAnnotationWordChar,
@@ -3263,6 +3264,26 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
3263
3264
  return undefined;
3264
3265
  }
3265
3266
 
3267
+ function stripStudioMarkdownInlineCodeSpans(markdown: string): string {
3268
+ const source = String(markdown ?? "");
3269
+ let out = "";
3270
+ let index = 0;
3271
+ while (index < source.length) {
3272
+ if (source[index] === "`") {
3273
+ index = advancePastStudioInlineBacktickSpan(source, index);
3274
+ continue;
3275
+ }
3276
+ out += source[index];
3277
+ index += 1;
3278
+ }
3279
+ return out;
3280
+ }
3281
+
3282
+ function isLikelyStandaloneLatexPreview(markdown: string): boolean {
3283
+ const outsideFences = transformStudioMarkdownOutsideFences(markdown, (segment: string) => stripStudioMarkdownInlineCodeSpans(segment));
3284
+ return /\\documentclass\b|\\begin\{document\}/.test(outsideFences);
3285
+ }
3286
+
3266
3287
  function escapeStudioPdfLatexText(text: string): string {
3267
3288
  const normalized = String(text ?? "")
3268
3289
  .replace(/\r\n/g, "\n")
@@ -4043,9 +4064,15 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
4043
4064
  }
4044
4065
 
4045
4066
  function stripMathMlAnnotationTags(html: string): string {
4046
- return html
4047
- .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
4048
- .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
4067
+ return String(html ?? "").replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (_match, attrs, inner) => {
4068
+ const texAnnotationMatch = String(inner ?? "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
4069
+ const texSource = texAnnotationMatch ? String(texAnnotationMatch[1] ?? "").trim() : "";
4070
+ const cleanedInner = String(inner ?? "")
4071
+ .replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
4072
+ .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
4073
+ const texAttr = texSource ? ` data-tex-source="${escapeStudioHtmlText(texSource)}"` : "";
4074
+ return `<math${attrs}${texAttr}>${cleanedInner}</math>`;
4075
+ });
4049
4076
  }
4050
4077
 
4051
4078
  function normalizeObsidianImages(markdown: string): string {
@@ -7869,8 +7896,17 @@ export default function (pi: ExtensionAPI) {
7869
7896
  parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
7870
7897
  ? (parsedBody as { resourceDir: string }).resourceDir
7871
7898
  : "";
7899
+ const requestedEditorLanguage =
7900
+ parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorLanguage?: unknown }).editorLanguage === "string"
7901
+ ? (parsedBody as { editorLanguage: string }).editorLanguage
7902
+ : "";
7872
7903
  const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
7873
- const isLatex = /\\documentclass\b|\\begin\{document\}/.test(markdown);
7904
+ const editorPreviewLanguage = normalizeStudioEditorLanguage(requestedEditorLanguage);
7905
+ const isLatex = editorPreviewLanguage === "latex"
7906
+ || (
7907
+ (editorPreviewLanguage === undefined || editorPreviewLanguage === "markdown")
7908
+ && isLikelyStandaloneLatexPreview(markdown)
7909
+ );
7874
7910
  const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath, sourcePath || undefined);
7875
7911
  respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
7876
7912
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.46",
3
+ "version": "0.5.48",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",