pi-studio 0.5.53 → 0.5.54

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,12 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.54] — 2026-04-13
8
+
9
+ ### Fixed
10
+ - Markdown editor-preview comment mapping now keeps standalone markdown image blocks aligned with rendered preview figures, preventing later preview comment targets from drifting after figures.
11
+ - Rendered Markdown fenced code blocks now once again support preview-side text-selection comments.
12
+
7
13
  ## [0.5.53] — 2026-04-10
8
14
 
9
15
  ### Added
@@ -187,7 +187,9 @@
187
187
 
188
188
  if (index >= text.length || text[index] !== "]" || text[index + 1] !== "(") return null;
189
189
 
190
+ const label = text.slice(startIndex + 1, index);
190
191
  index += 2;
192
+ const destinationStart = index;
191
193
  let parenDepth = 0;
192
194
  while (index < text.length) {
193
195
  const ch = text[index];
@@ -210,6 +212,8 @@
210
212
  type: "literal",
211
213
  raw: text.slice(startIndex, index + 1),
212
214
  end: index + 1,
215
+ label: label,
216
+ destination: text.slice(destinationStart, index),
213
217
  };
214
218
  }
215
219
  parenDepth -= 1;
@@ -223,6 +227,68 @@
223
227
  return null;
224
228
  }
225
229
 
230
+ function readMarkdownAttributeBlockAt(source, startIndex) {
231
+ const text = String(source || "");
232
+ if (text[startIndex] !== "{") return null;
233
+
234
+ let depth = 0;
235
+ for (let index = startIndex; index < text.length; index += 1) {
236
+ const ch = text[index];
237
+ if (ch === "\\") {
238
+ index += 1;
239
+ continue;
240
+ }
241
+ if (ch === "\n") return null;
242
+ if (ch === "{") {
243
+ depth += 1;
244
+ continue;
245
+ }
246
+ if (ch === "}") {
247
+ depth -= 1;
248
+ if (depth === 0) {
249
+ return {
250
+ raw: text.slice(startIndex, index + 1),
251
+ end: index + 1,
252
+ };
253
+ }
254
+ }
255
+ }
256
+
257
+ return null;
258
+ }
259
+
260
+ function extractStandaloneMarkdownImageCaptionText(text) {
261
+ const source = String(text || "").replace(/\r\n/g, "\n").trim();
262
+ if (!source) return null;
263
+
264
+ const captions = [];
265
+ let index = 0;
266
+ let sawImage = false;
267
+
268
+ while (index < source.length) {
269
+ while (index < source.length && /\s/.test(source[index])) index += 1;
270
+ if (index >= source.length) break;
271
+ if (source[index] !== "!") return null;
272
+
273
+ const imageToken = readInlineMarkdownLinkAt(source, index + 1);
274
+ if (!imageToken) return null;
275
+
276
+ sawImage = true;
277
+ captions.push(normalizePreviewAnnotationLabel(imageToken.label || ""));
278
+ index = imageToken.end;
279
+
280
+ while (index < source.length && /\s/.test(source[index])) index += 1;
281
+ if (index < source.length && source[index] === "{") {
282
+ const attributeBlock = readMarkdownAttributeBlockAt(source, index);
283
+ if (!attributeBlock) return null;
284
+ index = attributeBlock.end;
285
+ }
286
+ }
287
+
288
+ if (!sawImage) return null;
289
+ return captions.filter(Boolean).join(" ").trim();
290
+ }
291
+
226
292
  function readDelimitedPreviewTokenAt(source, startIndex, open, close, allowNewlines) {
227
293
  const text = String(source || "");
228
294
  if (text.slice(startIndex, startIndex + open.length) !== open) return null;
@@ -596,6 +662,7 @@
596
662
  collectInlineAnnotationMarkers: collectInlineAnnotationMarkers,
597
663
  hasAnnotationMarkers: hasAnnotationMarkers,
598
664
  normalizePreviewAnnotationLabel: normalizePreviewAnnotationLabel,
665
+ extractStandaloneMarkdownImageCaptionText: extractStandaloneMarkdownImageCaptionText,
599
666
  prepareMarkdownForPandocPreview: prepareMarkdownForPandocPreview,
600
667
  readInlineAnnotationMarkerAt: readInlineAnnotationMarkerAt,
601
668
  renderPreviewAnnotationHtml: renderPreviewAnnotationHtml,
@@ -5309,6 +5309,7 @@
5309
5309
  || kind === "blockquote"
5310
5310
  || kind === "list"
5311
5311
  || kind === "math"
5312
+ || kind === "code"
5312
5313
  || kind === "code-line"
5313
5314
  || kind === "diff-line"
5314
5315
  || kind === "text-line";
@@ -6422,7 +6423,7 @@
6422
6423
 
6423
6424
  function buildPreviewSelectionDisplayMap(blockText, kind) {
6424
6425
  const body = buildPreviewSelectionSourceBody(blockText, kind);
6425
- if (kind === "code-line" || kind === "diff-line" || kind === "text-line") {
6426
+ if (kind === "code" || kind === "code-line" || kind === "diff-line" || kind === "text-line") {
6426
6427
  return buildLiteralPreviewDisplayMap(body.text, body.rawOffsets);
6427
6428
  }
6428
6429
  const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
@@ -6694,6 +6695,15 @@
6694
6695
  };
6695
6696
  }
6696
6697
 
6698
+ function getChunkText(startLineIndex, endLineIndex) {
6699
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
6700
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
6701
+ return source.slice(
6702
+ lineOffsets[safeStartLine] || 0,
6703
+ (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length,
6704
+ );
6705
+ }
6706
+
6697
6707
  const blocks = [];
6698
6708
  let index = 0;
6699
6709
 
@@ -6826,7 +6836,11 @@
6826
6836
  }
6827
6837
  endParagraph = i;
6828
6838
  }
6829
- blocks.push(makeBlock("paragraph", index, endParagraph));
6839
+ const paragraphText = getChunkText(index, endParagraph);
6840
+ const markdownFigureCaption = annotationHelpers && typeof annotationHelpers.extractStandaloneMarkdownImageCaptionText === "function"
6841
+ ? annotationHelpers.extractStandaloneMarkdownImageCaptionText(paragraphText)
6842
+ : null;
6843
+ blocks.push(makeBlock(markdownFigureCaption != null ? "figure" : "paragraph", index, endParagraph));
6830
6844
  index = endParagraph + 1;
6831
6845
  }
6832
6846
 
@@ -7145,6 +7159,42 @@
7145
7159
  });
7146
7160
  }
7147
7161
 
7162
+ function isPreviewMediaOnlyParagraphElement(element) {
7163
+ if (!element || !(element instanceof Element)) return false;
7164
+ if ((element.tagName ? element.tagName.toUpperCase() : "") !== "P") return false;
7165
+
7166
+ let hasMedia = false;
7167
+ for (const childNode of Array.from(element.childNodes || [])) {
7168
+ if (!childNode) continue;
7169
+ if (childNode.nodeType === Node.TEXT_NODE) {
7170
+ if (normalizeVisiblePreviewText(childNode.nodeValue || "")) {
7171
+ return false;
7172
+ }
7173
+ continue;
7174
+ }
7175
+ if (!(childNode instanceof Element)) continue;
7176
+
7177
+ const childTag = childNode.tagName ? childNode.tagName.toUpperCase() : "";
7178
+ if (childTag === "BR") continue;
7179
+ if (childTag === "IMG" || childTag === "EMBED" || childTag === "OBJECT" || childTag === "IFRAME" || childTag === "CANVAS") {
7180
+ hasMedia = true;
7181
+ continue;
7182
+ }
7183
+
7184
+ const nestedMedia = typeof childNode.querySelector === "function"
7185
+ ? childNode.querySelector("img, embed, object, iframe, canvas")
7186
+ : null;
7187
+ if (nestedMedia && !buildNormalizedPreviewSearchText(childNode)) {
7188
+ hasMedia = true;
7189
+ continue;
7190
+ }
7191
+
7192
+ return false;
7193
+ }
7194
+
7195
+ return hasMedia;
7196
+ }
7197
+
7148
7198
  function getPreviewCommentTargetKind(element) {
7149
7199
  if (!element || !(element instanceof Element)) return "";
7150
7200
  if (element.classList && element.classList.contains("studio-mathjax-fallback-display")) {
@@ -7155,12 +7205,12 @@
7155
7205
  }
7156
7206
  const tag = element.tagName ? element.tagName.toUpperCase() : "";
7157
7207
  if (/^H[1-6]$/.test(tag)) return "heading";
7158
- if (tag === "P") return "paragraph";
7208
+ if (tag === "P") return isPreviewMediaOnlyParagraphElement(element) ? "figure" : "paragraph";
7159
7209
  if (tag === "FIGURE") {
7160
7210
  if (element.classList && element.classList.contains("studio-algorithm-block")) {
7161
7211
  return "algorithm";
7162
7212
  }
7163
- return editorLanguage === "latex" ? "figure" : "";
7213
+ return "figure";
7164
7214
  }
7165
7215
  if (tag === "DIV" && element.classList) {
7166
7216
  if (element.classList.contains("studio-display-equation")) {
@@ -7267,6 +7317,14 @@
7267
7317
  const match = blockText.trim().match(/^\\(newpage|pagebreak|clearpage)/i);
7268
7318
  return match ? String(match[1] || "").toLowerCase() : "page-break";
7269
7319
  }
7320
+ if (sourceBlock.kind === "figure") {
7321
+ const figureCaption = annotationHelpers && typeof annotationHelpers.extractStandaloneMarkdownImageCaptionText === "function"
7322
+ ? annotationHelpers.extractStandaloneMarkdownImageCaptionText(blockText)
7323
+ : null;
7324
+ if (figureCaption != null) {
7325
+ return normalizeVisiblePreviewText(figureCaption);
7326
+ }
7327
+ }
7270
7328
  if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
7271
7329
  return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
7272
7330
  }
@@ -7287,6 +7345,23 @@
7287
7345
  return normalizeVisiblePreviewText(blockText);
7288
7346
  }
7289
7347
 
7348
+ function getPreviewFigureSearchText(element) {
7349
+ if (!element || !(element instanceof Element)) return "";
7350
+ const visibleText = buildNormalizedPreviewSearchText(element);
7351
+ if (visibleText) return visibleText;
7352
+
7353
+ const imageNodes = (element.tagName ? element.tagName.toUpperCase() : "") === "IMG"
7354
+ ? [element]
7355
+ : (typeof element.querySelectorAll === "function" ? Array.from(element.querySelectorAll("img[alt], img[title]")) : []);
7356
+ const altText = imageNodes
7357
+ .filter((imageEl) => imageEl instanceof Element)
7358
+ .map((imageEl) => imageEl.getAttribute("alt") || imageEl.getAttribute("title") || "")
7359
+ .map((text) => normalizeVisiblePreviewText(text))
7360
+ .filter(Boolean)
7361
+ .join(" ");
7362
+ return altText;
7363
+ }
7364
+
7290
7365
  function getNormalizedPreviewCommentTargetText(targetEntry) {
7291
7366
  if (!targetEntry) return "";
7292
7367
  if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
@@ -7295,6 +7370,10 @@
7295
7370
  targetEntry.normalizedText = String(element && element.getAttribute ? (element.getAttribute("data-page-break-kind") || "page-break") : "page-break").toLowerCase();
7296
7371
  return targetEntry.normalizedText;
7297
7372
  }
7373
+ if (targetEntry.kind === "figure") {
7374
+ targetEntry.normalizedText = getPreviewFigureSearchText(targetEntry.element);
7375
+ return targetEntry.normalizedText;
7376
+ }
7298
7377
  targetEntry.normalizedText = buildNormalizedPreviewSearchText(targetEntry.element);
7299
7378
  return targetEntry.normalizedText;
7300
7379
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.53",
3
+ "version": "0.5.54",
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",