pi-studio 0.5.45 → 0.5.46

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.
@@ -53,6 +53,7 @@
53
53
  const lineNumberGutterContentEl = document.getElementById("lineNumberGutterContent");
54
54
  const lineNumberMeasureEl = document.getElementById("lineNumberMeasure");
55
55
  const sourcePreviewEl = document.getElementById("sourcePreview");
56
+ const editorSelectionCommentBtn = document.getElementById("editorSelectionCommentBtn");
56
57
  const leftPaneEl = document.getElementById("leftPane");
57
58
  const rightPaneEl = document.getElementById("rightPane");
58
59
  const sourceBadgeEl = document.getElementById("sourceBadge");
@@ -304,6 +305,8 @@
304
305
  let reviewNotesLoadNonce = 0;
305
306
  let pendingReviewNoteFocusId = null;
306
307
  let pendingReviewNoteInlineFocusId = null;
308
+ let activePreviewCommentSelection = null;
309
+ const previewJumpHighlightState = new WeakMap();
307
310
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
308
311
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
309
312
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
@@ -2528,6 +2531,7 @@
2528
2531
  if (nonce !== responsePreviewRenderNonce || (rightView !== "preview" && rightView !== "editor-preview")) return;
2529
2532
  }
2530
2533
 
2534
+ clearPreviewJumpHighlight(targetEl);
2531
2535
  finishPreviewRender(targetEl);
2532
2536
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2533
2537
  applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
@@ -2541,6 +2545,15 @@
2541
2545
  await renderMermaidInElement(targetEl);
2542
2546
  await renderMathFallbackInElement(targetEl);
2543
2547
 
2548
+ const shouldDecoratePreviewComments = supportsPreviewCommentsForCurrentEditor()
2549
+ && (
2550
+ (pane === "source" && editorView === "preview")
2551
+ || (pane === "response" && rightView === "editor-preview")
2552
+ );
2553
+ if (shouldDecoratePreviewComments) {
2554
+ decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
2555
+ }
2556
+
2544
2557
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
2545
2558
  if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
2546
2559
  var hasRelativeImages = /!\[.*?\]\((?!https?:\/\/|data:)[^)]+\)/.test(markdown || "");
@@ -2562,6 +2575,7 @@
2562
2575
  }
2563
2576
 
2564
2577
  const detail = error && error.message ? error.message : String(error || "unknown error");
2578
+ clearPreviewJumpHighlight(targetEl);
2565
2579
  finishPreviewRender(targetEl);
2566
2580
  targetEl.innerHTML = buildPreviewErrorHtml("Preview renderer unavailable (" + detail + "). Showing plain markdown.", markdown);
2567
2581
  if (pane === "response") {
@@ -2905,6 +2919,9 @@
2905
2919
  const value = String(nextText || "");
2906
2920
  const preserveScroll = Boolean(options && options.preserveScroll);
2907
2921
  const preserveSelection = Boolean(options && options.preserveSelection);
2922
+ if (activePreviewCommentSelection) {
2923
+ clearPreviewCommentSelection();
2924
+ }
2908
2925
  const previousScrollTop = sourceTextEl.scrollTop;
2909
2926
  const previousScrollLeft = sourceTextEl.scrollLeft;
2910
2927
  const previousSelectionStart = sourceTextEl.selectionStart;
@@ -2943,6 +2960,7 @@
2943
2960
  if (!options || options.updateMeta !== false) {
2944
2961
  scheduleEditorMetaUpdate();
2945
2962
  }
2963
+ updateEditorSelectionCommentUi();
2946
2964
  }
2947
2965
 
2948
2966
  function setEditorView(nextView) {
@@ -2961,6 +2979,7 @@
2961
2979
  }
2962
2980
 
2963
2981
  if (!showPreview) {
2982
+ clearPreviewJumpHighlight(sourcePreviewEl);
2964
2983
  finishPreviewRender(sourcePreviewEl);
2965
2984
  }
2966
2985
 
@@ -2975,6 +2994,7 @@
2975
2994
  scheduleEditorLineNumberRender();
2976
2995
  }
2977
2996
  updateReviewNotesUi();
2997
+ updateEditorSelectionCommentUi();
2978
2998
  }
2979
2999
 
2980
3000
  function setRightView(nextView) {
@@ -2989,6 +3009,9 @@
2989
3009
  window.clearTimeout(responseEditorPreviewTimer);
2990
3010
  responseEditorPreviewTimer = null;
2991
3011
  }
3012
+ if (rightView !== "editor-preview") {
3013
+ clearPreviewJumpHighlight(critiqueViewEl);
3014
+ }
2992
3015
 
2993
3016
  refreshResponseUi();
2994
3017
  syncActionButtons();
@@ -4082,6 +4105,7 @@
4082
4105
  lineStart,
4083
4106
  lineEnd,
4084
4107
  selectedText: typeof note.selectedText === "string" ? note.selectedText : "",
4108
+ selectedDisplayText: typeof note.selectedDisplayText === "string" ? note.selectedDisplayText : "",
4085
4109
  };
4086
4110
  }
4087
4111
 
@@ -4171,6 +4195,7 @@
4171
4195
  }
4172
4196
  updateReviewNotesUi();
4173
4197
  renderReviewNotesList();
4198
+ refreshRenderedEditorPreviewComments();
4174
4199
  if (editorView === "markdown") {
4175
4200
  scheduleEditorLineNumberRender();
4176
4201
  }
@@ -4192,7 +4217,7 @@
4192
4217
  }
4193
4218
 
4194
4219
  function summarizeReviewNoteQuote(note) {
4195
- const normalized = String(note && note.selectedText ? note.selectedText : "")
4220
+ const normalized = String(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "")
4196
4221
  .replace(/\s+/g, " ")
4197
4222
  .trim();
4198
4223
  if (!normalized) return "Anchor: current line / empty selection";
@@ -4252,6 +4277,7 @@
4252
4277
  lineStart: getLineNumberAtOffset(current, safeStart),
4253
4278
  lineEnd: getLineNumberAtOffset(current, Math.max(safeStart, safeEnd - 1)),
4254
4279
  selectedText: current.slice(safeStart, safeEnd),
4280
+ selectedDisplayText: current.slice(safeStart, safeEnd),
4255
4281
  };
4256
4282
  }
4257
4283
  const lineRange = getLineRangeAtOffset(current, safeStart);
@@ -4261,6 +4287,23 @@
4261
4287
  lineStart: lineRange.lineNumber,
4262
4288
  lineEnd: lineRange.lineNumber,
4263
4289
  selectedText: current.slice(lineRange.start, lineRange.end),
4290
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4291
+ };
4292
+ }
4293
+
4294
+ function getEditorLineAnchorForReviewNote() {
4295
+ const current = String(sourceTextEl.value || "");
4296
+ const caret = typeof sourceTextEl.selectionStart === "number"
4297
+ ? sourceTextEl.selectionStart
4298
+ : 0;
4299
+ const lineRange = getLineRangeAtOffset(current, Math.max(0, Math.min(caret, current.length)));
4300
+ return {
4301
+ selectionStart: lineRange.start,
4302
+ selectionEnd: lineRange.end,
4303
+ lineStart: lineRange.lineNumber,
4304
+ lineEnd: lineRange.lineNumber,
4305
+ selectedText: current.slice(lineRange.start, lineRange.end),
4306
+ selectedDisplayText: current.slice(lineRange.start, lineRange.end),
4264
4307
  };
4265
4308
  }
4266
4309
 
@@ -4314,6 +4357,972 @@
4314
4357
  return lineMap;
4315
4358
  }
4316
4359
 
4360
+ function supportsPreviewCommentsForCurrentEditor() {
4361
+ return editorLanguage === "markdown";
4362
+ }
4363
+
4364
+ function getPreviewCommentBlockKindLabel(kind) {
4365
+ if (kind === "heading") return "heading";
4366
+ if (kind === "blockquote") return "quote block";
4367
+ if (kind === "list") return "list";
4368
+ if (kind === "code") return "code block";
4369
+ if (kind === "table") return "table";
4370
+ return "paragraph";
4371
+ }
4372
+
4373
+ function supportsPreviewSelectionCommentsForBlockKind(kind) {
4374
+ return kind === "paragraph" || kind === "heading" || kind === "blockquote" || kind === "list";
4375
+ }
4376
+
4377
+ function normalizeVisiblePreviewText(text) {
4378
+ return String(text || "").replace(/\s+/g, " ").trim();
4379
+ }
4380
+
4381
+ function appendMappedPreviewSlice(chars, rawOffsets, lineText, lineBaseOffset, start, end) {
4382
+ const safeStart = Math.max(0, Math.min(start, lineText.length));
4383
+ const safeEnd = Math.max(safeStart, Math.min(end, lineText.length));
4384
+ for (let i = safeStart; i < safeEnd; i += 1) {
4385
+ chars.push(lineText[i]);
4386
+ rawOffsets.push(lineBaseOffset + i);
4387
+ }
4388
+ }
4389
+
4390
+ function buildPreviewSelectionSourceBody(blockText, kind) {
4391
+ const source = String(blockText || "");
4392
+ const lines = source.split("\n");
4393
+ const lineOffsets = [];
4394
+ let runningOffset = 0;
4395
+ for (const line of lines) {
4396
+ lineOffsets.push(runningOffset);
4397
+ runningOffset += line.length + 1;
4398
+ }
4399
+
4400
+ const chars = [];
4401
+ const rawOffsets = [];
4402
+
4403
+ function appendLineWithStart(lineIndex, start, end) {
4404
+ const line = lineIndex >= 0 && lineIndex < lines.length ? lines[lineIndex] : "";
4405
+ appendMappedPreviewSlice(chars, rawOffsets, line, lineOffsets[lineIndex] || 0, start, end);
4406
+ if (lineIndex < lines.length - 1) {
4407
+ chars.push("\n");
4408
+ rawOffsets.push((lineOffsets[lineIndex] || 0) + line.length);
4409
+ }
4410
+ }
4411
+
4412
+ if (kind === "heading") {
4413
+ const firstLine = lines[0] || "";
4414
+ const atxMatch = firstLine.match(/^ {0,3}#{1,6}(?:[ \t]+|$)/);
4415
+ if (atxMatch) {
4416
+ const start = atxMatch[0].length;
4417
+ let end = firstLine.length;
4418
+ const closingMatch = firstLine.slice(start).match(/[ \t]+#+[ \t]*$/);
4419
+ if (closingMatch) {
4420
+ end -= closingMatch[0].length;
4421
+ }
4422
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, start, end);
4423
+ return { text: chars.join(""), rawOffsets };
4424
+ }
4425
+ if (lines.length >= 2 && /^ {0,3}(?:={3,}|-{3,})\s*$/.test(lines[1] || "")) {
4426
+ appendMappedPreviewSlice(chars, rawOffsets, firstLine, lineOffsets[0] || 0, 0, firstLine.length);
4427
+ return { text: chars.join(""), rawOffsets };
4428
+ }
4429
+ }
4430
+
4431
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
4432
+ const line = lines[lineIndex] || "";
4433
+ if (kind === "blockquote") {
4434
+ const prefixMatch = line.match(/^ {0,3}> ?/);
4435
+ appendLineWithStart(lineIndex, prefixMatch ? prefixMatch[0].length : 0, line.length);
4436
+ continue;
4437
+ }
4438
+ if (kind === "list") {
4439
+ if (!line.trim()) {
4440
+ appendLineWithStart(lineIndex, 0, 0);
4441
+ continue;
4442
+ }
4443
+ const itemMatch = line.match(/^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/);
4444
+ if (itemMatch) {
4445
+ appendLineWithStart(lineIndex, itemMatch[0].length, line.length);
4446
+ continue;
4447
+ }
4448
+ const continuationMatch = line.match(/^(?: {1,4}|\t)/);
4449
+ appendLineWithStart(lineIndex, continuationMatch ? continuationMatch[0].length : 0, line.length);
4450
+ continue;
4451
+ }
4452
+ appendLineWithStart(lineIndex, 0, line.length);
4453
+ }
4454
+
4455
+ return { text: chars.join(""), rawOffsets };
4456
+ }
4457
+
4458
+ function buildPreviewInlineDisplayMap(text, rawOffsets) {
4459
+ const source = String(text || "");
4460
+ const rawMap = Array.isArray(rawOffsets) ? rawOffsets : [];
4461
+ const displayChars = [];
4462
+ const charStarts = [];
4463
+ const charEnds = [];
4464
+
4465
+ function appendChar(character, rawStart, rawEnd) {
4466
+ displayChars.push(character);
4467
+ charStarts.push(rawStart);
4468
+ charEnds.push(rawEnd);
4469
+ }
4470
+
4471
+ function appendNestedRange(startIndex, endIndex) {
4472
+ const nested = buildPreviewInlineDisplayMap(
4473
+ source.slice(startIndex, endIndex),
4474
+ rawMap.slice(startIndex, endIndex),
4475
+ );
4476
+ for (let i = 0; i < nested.text.length; i += 1) {
4477
+ appendChar(nested.text[i], nested.charStarts[i], nested.charEnds[i]);
4478
+ }
4479
+ }
4480
+
4481
+ let index = 0;
4482
+ while (index < source.length) {
4483
+ const remaining = source.slice(index);
4484
+ const linkMatch = remaining.match(/^!?\[([^\]]*)\]\(([^)]*)\)/);
4485
+ if (linkMatch) {
4486
+ const labelStart = index + (remaining[0] === "!" ? 2 : 1);
4487
+ const labelEnd = labelStart + String(linkMatch[1] || "").length;
4488
+ appendNestedRange(labelStart, labelEnd);
4489
+ index += linkMatch[0].length;
4490
+ continue;
4491
+ }
4492
+
4493
+ if (source[index] === "`") {
4494
+ let tickCount = 1;
4495
+ while (source[index + tickCount] === "`") tickCount += 1;
4496
+ const fence = "`".repeat(tickCount);
4497
+ const closeIndex = source.indexOf(fence, index + tickCount);
4498
+ if (closeIndex >= 0) {
4499
+ for (let i = index + tickCount; i < closeIndex; i += 1) {
4500
+ appendChar(source[i], rawMap[i], rawMap[i] + 1);
4501
+ }
4502
+ index = closeIndex + tickCount;
4503
+ continue;
4504
+ }
4505
+ }
4506
+
4507
+ if (source[index] === "\\" && index + 1 < source.length) {
4508
+ appendChar(source[index + 1], rawMap[index], rawMap[index + 1] + 1);
4509
+ index += 2;
4510
+ continue;
4511
+ }
4512
+
4513
+ const htmlTagMatch = remaining.match(/^<\/?[A-Za-z][^>]*>/);
4514
+ if (htmlTagMatch) {
4515
+ index += htmlTagMatch[0].length;
4516
+ continue;
4517
+ }
4518
+
4519
+ const emphasisMatch = remaining.match(/^(?:\*\*\*|\*\*|\*|___|__|_|~~)/);
4520
+ if (emphasisMatch) {
4521
+ index += emphasisMatch[0].length;
4522
+ continue;
4523
+ }
4524
+
4525
+ appendChar(source[index], rawMap[index], rawMap[index] + 1);
4526
+ index += 1;
4527
+ }
4528
+
4529
+ return {
4530
+ text: displayChars.join(""),
4531
+ charStarts,
4532
+ charEnds,
4533
+ };
4534
+ }
4535
+
4536
+ function buildNormalizedPreviewDisplayMap(displayText, charStarts, charEnds) {
4537
+ const source = String(displayText || "");
4538
+ const outChars = [];
4539
+ const outStarts = [];
4540
+ const outEnds = [];
4541
+ let pendingWhitespaceStart = null;
4542
+ let pendingWhitespaceEnd = null;
4543
+
4544
+ for (let i = 0; i < source.length; i += 1) {
4545
+ const character = source[i];
4546
+ if (/\s/.test(character)) {
4547
+ if (outChars.length === 0) continue;
4548
+ if (pendingWhitespaceStart == null) {
4549
+ pendingWhitespaceStart = charStarts[i];
4550
+ }
4551
+ pendingWhitespaceEnd = charEnds[i];
4552
+ continue;
4553
+ }
4554
+
4555
+ if (pendingWhitespaceStart != null && pendingWhitespaceEnd != null) {
4556
+ outChars.push(" ");
4557
+ outStarts.push(pendingWhitespaceStart);
4558
+ outEnds.push(pendingWhitespaceEnd);
4559
+ pendingWhitespaceStart = null;
4560
+ pendingWhitespaceEnd = null;
4561
+ }
4562
+
4563
+ outChars.push(character);
4564
+ outStarts.push(charStarts[i]);
4565
+ outEnds.push(charEnds[i]);
4566
+ }
4567
+
4568
+ return {
4569
+ text: outChars.join(""),
4570
+ charStarts: outStarts,
4571
+ charEnds: outEnds,
4572
+ };
4573
+ }
4574
+
4575
+ function buildNormalizedDomTextMap(rootEl) {
4576
+ if (!rootEl || typeof document.createTreeWalker !== "function") {
4577
+ return { text: "", charStarts: [], charEnds: [] };
4578
+ }
4579
+ const walker = document.createTreeWalker(rootEl, NodeFilter.SHOW_TEXT);
4580
+ const chars = [];
4581
+ const starts = [];
4582
+ const ends = [];
4583
+ let node = walker.nextNode();
4584
+ while (node) {
4585
+ const textNode = node;
4586
+ const value = typeof textNode.nodeValue === "string" ? textNode.nodeValue : "";
4587
+ for (let i = 0; i < value.length; i += 1) {
4588
+ chars.push(value[i]);
4589
+ starts.push({ node: textNode, offset: i });
4590
+ ends.push({ node: textNode, offset: i + 1 });
4591
+ }
4592
+ node = walker.nextNode();
4593
+ }
4594
+ return buildNormalizedPreviewDisplayMap(chars.join(""), starts, ends);
4595
+ }
4596
+
4597
+ function findPreferredNormalizedTextMatch(haystack, needle, preferredIndex) {
4598
+ const source = String(haystack || "");
4599
+ const query = String(needle || "");
4600
+ if (!source || !query) return -1;
4601
+ let bestIndex = -1;
4602
+ let bestScore = Number.POSITIVE_INFINITY;
4603
+ const desiredIndex = Number.isFinite(preferredIndex) ? Math.max(0, preferredIndex) : 0;
4604
+ for (let matchIndex = source.indexOf(query); matchIndex >= 0; matchIndex = source.indexOf(query, matchIndex + 1)) {
4605
+ const score = Math.abs(matchIndex - desiredIndex);
4606
+ if (score < bestScore) {
4607
+ bestScore = score;
4608
+ bestIndex = matchIndex;
4609
+ }
4610
+ }
4611
+ return bestIndex;
4612
+ }
4613
+
4614
+ function buildPreviewSelectionDisplayMap(blockText, kind) {
4615
+ const body = buildPreviewSelectionSourceBody(blockText, kind);
4616
+ const inlineMap = buildPreviewInlineDisplayMap(body.text, body.rawOffsets);
4617
+ return buildNormalizedPreviewDisplayMap(inlineMap.text, inlineMap.charStarts, inlineMap.charEnds);
4618
+ }
4619
+
4620
+ function getPreviewCommentBlockKey(blockEl) {
4621
+ if (!blockEl || !blockEl.dataset) return "";
4622
+ return [
4623
+ String(blockEl.dataset.reviewNoteStart || ""),
4624
+ String(blockEl.dataset.reviewNoteEnd || ""),
4625
+ String(blockEl.dataset.previewCommentKind || ""),
4626
+ ].join(":");
4627
+ }
4628
+
4629
+ function getPreviewCommentSelectionKey(selection) {
4630
+ if (!selection) return "";
4631
+ return [
4632
+ String(selection.blockKey || ""),
4633
+ String(selection.selectionStart || 0),
4634
+ String(selection.selectionEnd || 0),
4635
+ String(selection.selectedDisplayText || ""),
4636
+ ].join(":");
4637
+ }
4638
+
4639
+ function setActivePreviewCommentSelection(nextSelection) {
4640
+ const currentKey = getPreviewCommentSelectionKey(activePreviewCommentSelection);
4641
+ const nextKey = getPreviewCommentSelectionKey(nextSelection);
4642
+ if (currentKey === nextKey) return;
4643
+ activePreviewCommentSelection = nextSelection || null;
4644
+ refreshRenderedEditorPreviewComments();
4645
+ }
4646
+
4647
+ function clearPreviewCommentSelection() {
4648
+ setActivePreviewCommentSelection(null);
4649
+ }
4650
+
4651
+ function findPreviewCommentBlockFromNode(node) {
4652
+ if (!node) return null;
4653
+ const element = node instanceof Element ? node : node.parentElement;
4654
+ return element && typeof element.closest === "function"
4655
+ ? element.closest(".preview-comment-block")
4656
+ : null;
4657
+ }
4658
+
4659
+ function unwrapPreviewJumpHighlightElement(element) {
4660
+ if (!element || !element.parentNode) return;
4661
+ const parent = element.parentNode;
4662
+ while (element.firstChild) {
4663
+ parent.insertBefore(element.firstChild, element);
4664
+ }
4665
+ parent.removeChild(element);
4666
+ if (typeof parent.normalize === "function") {
4667
+ parent.normalize();
4668
+ }
4669
+ }
4670
+
4671
+ function clearPreviewJumpHighlight(targetEl) {
4672
+ if (!targetEl) return;
4673
+ const state = previewJumpHighlightState.get(targetEl);
4674
+ if (!state) return;
4675
+ if (state.timer != null) {
4676
+ window.clearTimeout(state.timer);
4677
+ }
4678
+ if (state.inlineHighlightEl) {
4679
+ unwrapPreviewJumpHighlightElement(state.inlineHighlightEl);
4680
+ }
4681
+ if (state.contentEl && state.contentEl.classList) {
4682
+ state.contentEl.classList.remove("preview-jump-highlight");
4683
+ }
4684
+ previewJumpHighlightState.delete(targetEl);
4685
+ }
4686
+
4687
+ function setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl) {
4688
+ if (!targetEl || !contentEl) return;
4689
+ clearPreviewJumpHighlight(targetEl);
4690
+ if (contentEl.classList) {
4691
+ contentEl.classList.add("preview-jump-highlight");
4692
+ }
4693
+ const timer = window.setTimeout(() => {
4694
+ clearPreviewJumpHighlight(targetEl);
4695
+ }, 1800);
4696
+ previewJumpHighlightState.set(targetEl, {
4697
+ contentEl,
4698
+ inlineHighlightEl: inlineHighlightEl || null,
4699
+ timer,
4700
+ });
4701
+ }
4702
+
4703
+ function rangesOverlap(startA, endA, startB, endB) {
4704
+ const safeStartA = Math.max(0, Number(startA) || 0);
4705
+ const safeStartB = Math.max(0, Number(startB) || 0);
4706
+ const safeEndA = Math.max(safeStartA + 1, Number(endA) || safeStartA);
4707
+ const safeEndB = Math.max(safeStartB + 1, Number(endB) || safeStartB);
4708
+ return safeStartA < safeEndB && safeStartB < safeEndA;
4709
+ }
4710
+
4711
+ function scanMarkdownPreviewCommentBlocks(markdown) {
4712
+ const source = String(markdown || "").replace(/\r\n/g, "\n");
4713
+ const lines = source.split("\n");
4714
+ const lineOffsets = [];
4715
+ let runningOffset = 0;
4716
+ for (const line of lines) {
4717
+ lineOffsets.push(runningOffset);
4718
+ runningOffset += line.length + 1;
4719
+ }
4720
+
4721
+ function getLine(index) {
4722
+ return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
4723
+ }
4724
+
4725
+ function isBlankLine(index) {
4726
+ return /^\s*$/.test(getLine(index));
4727
+ }
4728
+
4729
+ function lineStartsFence(index) {
4730
+ return getLine(index).match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
4731
+ }
4732
+
4733
+ function isAtxHeadingLine(index) {
4734
+ return /^ {0,3}#{1,6}(?:[ \t]+|$)/.test(getLine(index));
4735
+ }
4736
+
4737
+ function isSetextUnderlineLine(index) {
4738
+ return /^ {0,3}(?:={3,}|-{3,})\s*$/.test(getLine(index));
4739
+ }
4740
+
4741
+ function isThematicBreakLine(index) {
4742
+ return /^ {0,3}(?:(?:-\s*){3,}|(?:_\s*){3,}|(?:\*\s*){3,})$/.test(getLine(index));
4743
+ }
4744
+
4745
+ function isBlockquoteLine(index) {
4746
+ return /^ {0,3}> ?/.test(getLine(index));
4747
+ }
4748
+
4749
+ function isListLine(index) {
4750
+ return /^ {0,3}(?:[*+-]|\d+[.)])(?:[ \t]+|$)/.test(getLine(index));
4751
+ }
4752
+
4753
+ function isContinuationIndentedLine(index) {
4754
+ return /^(?: {2,}|\t+)/.test(getLine(index));
4755
+ }
4756
+
4757
+ function isPotentialTableRow(index) {
4758
+ const line = getLine(index);
4759
+ return /\|/.test(line) && !/^\s*</.test(line);
4760
+ }
4761
+
4762
+ function isTableDividerLine(index) {
4763
+ return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/.test(getLine(index));
4764
+ }
4765
+
4766
+ function isHtmlCommentStart(index) {
4767
+ return /^\s*<!--/.test(getLine(index));
4768
+ }
4769
+
4770
+ function makeBlock(kind, startLineIndex, endLineIndex) {
4771
+ const safeStartLine = Math.max(0, Math.min(startLineIndex, Math.max(0, lines.length - 1)));
4772
+ const safeEndLine = Math.max(safeStartLine, Math.min(endLineIndex, Math.max(0, lines.length - 1)));
4773
+ const start = lineOffsets[safeStartLine] || 0;
4774
+ const end = (lineOffsets[safeEndLine] || 0) + getLine(safeEndLine).length;
4775
+ return {
4776
+ kind,
4777
+ start,
4778
+ end,
4779
+ lineStart: safeStartLine + 1,
4780
+ lineEnd: safeEndLine + 1,
4781
+ };
4782
+ }
4783
+
4784
+ const blocks = [];
4785
+ let index = 0;
4786
+
4787
+ if (/^\s*---\s*$/.test(getLine(0))) {
4788
+ for (let i = 1; i < Math.min(lines.length, 80); i += 1) {
4789
+ if (/^\s*(?:---|\.\.\.)\s*$/.test(getLine(i))) {
4790
+ index = i + 1;
4791
+ break;
4792
+ }
4793
+ }
4794
+ }
4795
+
4796
+ while (index < lines.length) {
4797
+ if (isBlankLine(index)) {
4798
+ index += 1;
4799
+ continue;
4800
+ }
4801
+
4802
+ if (isHtmlCommentStart(index)) {
4803
+ let endComment = index;
4804
+ while (endComment < lines.length && getLine(endComment).indexOf("-->") === -1) {
4805
+ endComment += 1;
4806
+ }
4807
+ index = Math.min(lines.length, endComment + 1);
4808
+ continue;
4809
+ }
4810
+
4811
+ if (isThematicBreakLine(index)) {
4812
+ index += 1;
4813
+ continue;
4814
+ }
4815
+
4816
+ const fenceMatch = lineStartsFence(index);
4817
+ if (fenceMatch) {
4818
+ const marker = fenceMatch[1] || "";
4819
+ const markerChar = marker[0] || "`";
4820
+ const markerLength = marker.length;
4821
+ let endFence = index;
4822
+ for (let i = index + 1; i < lines.length; i += 1) {
4823
+ const closingMatch = getLine(i).match(/^ {0,3}(`{3,}|~{3,})\s*$/);
4824
+ if (closingMatch && closingMatch[1] && closingMatch[1][0] === markerChar && closingMatch[1].length >= markerLength) {
4825
+ endFence = i;
4826
+ break;
4827
+ }
4828
+ endFence = i;
4829
+ }
4830
+ blocks.push(makeBlock("code", index, endFence));
4831
+ index = endFence + 1;
4832
+ continue;
4833
+ }
4834
+
4835
+ if (isAtxHeadingLine(index)) {
4836
+ blocks.push(makeBlock("heading", index, index));
4837
+ index += 1;
4838
+ continue;
4839
+ }
4840
+
4841
+ if (!isBlankLine(index) && index + 1 < lines.length && isSetextUnderlineLine(index + 1)) {
4842
+ blocks.push(makeBlock("heading", index, index + 1));
4843
+ index += 2;
4844
+ continue;
4845
+ }
4846
+
4847
+ if (isPotentialTableRow(index) && index + 1 < lines.length && isTableDividerLine(index + 1)) {
4848
+ let endTable = index + 1;
4849
+ for (let i = index + 2; i < lines.length; i += 1) {
4850
+ if (isBlankLine(i) || !isPotentialTableRow(i)) break;
4851
+ endTable = i;
4852
+ }
4853
+ blocks.push(makeBlock("table", index, endTable));
4854
+ index = endTable + 1;
4855
+ continue;
4856
+ }
4857
+
4858
+ if (isBlockquoteLine(index)) {
4859
+ let endQuote = index;
4860
+ for (let i = index + 1; i < lines.length; i += 1) {
4861
+ if (isBlockquoteLine(i)) {
4862
+ endQuote = i;
4863
+ continue;
4864
+ }
4865
+ if (isBlankLine(i) && i + 1 < lines.length && isBlockquoteLine(i + 1)) {
4866
+ endQuote = i;
4867
+ continue;
4868
+ }
4869
+ break;
4870
+ }
4871
+ blocks.push(makeBlock("blockquote", index, endQuote));
4872
+ index = endQuote + 1;
4873
+ continue;
4874
+ }
4875
+
4876
+ if (isListLine(index)) {
4877
+ let endList = index;
4878
+ for (let i = index + 1; i < lines.length; i += 1) {
4879
+ if (isBlankLine(i)) {
4880
+ if (i + 1 < lines.length && (isListLine(i + 1) || isContinuationIndentedLine(i + 1))) {
4881
+ endList = i;
4882
+ continue;
4883
+ }
4884
+ break;
4885
+ }
4886
+ if (isListLine(i) || isContinuationIndentedLine(i)) {
4887
+ endList = i;
4888
+ continue;
4889
+ }
4890
+ if (isAtxHeadingLine(i) || isBlockquoteLine(i) || lineStartsFence(i) || (isPotentialTableRow(i) && i + 1 < lines.length && isTableDividerLine(i + 1))) {
4891
+ break;
4892
+ }
4893
+ endList = i;
4894
+ }
4895
+ blocks.push(makeBlock("list", index, endList));
4896
+ index = endList + 1;
4897
+ continue;
4898
+ }
4899
+
4900
+ let endParagraph = index;
4901
+ for (let i = index + 1; i < lines.length; i += 1) {
4902
+ if (isBlankLine(i) || isHtmlCommentStart(i) || lineStartsFence(i) || isAtxHeadingLine(i) || isBlockquoteLine(i) || isListLine(i)) {
4903
+ break;
4904
+ }
4905
+ if (i + 1 < lines.length && (isSetextUnderlineLine(i + 1) || (isPotentialTableRow(i) && isTableDividerLine(i + 1)))) {
4906
+ break;
4907
+ }
4908
+ endParagraph = i;
4909
+ }
4910
+ blocks.push(makeBlock("paragraph", index, endParagraph));
4911
+ index = endParagraph + 1;
4912
+ }
4913
+
4914
+ return blocks;
4915
+ }
4916
+
4917
+ function getPreviewCommentTargetKind(element) {
4918
+ if (!element || !(element instanceof Element)) return "";
4919
+ const tag = element.tagName ? element.tagName.toUpperCase() : "";
4920
+ if (/^H[1-6]$/.test(tag)) return "heading";
4921
+ if (tag === "P") return "paragraph";
4922
+ if (tag === "BLOCKQUOTE") return "blockquote";
4923
+ if (tag === "UL" || tag === "OL") return "list";
4924
+ if (tag === "TABLE") return "table";
4925
+ if (tag === "PRE") return "code";
4926
+ if (element.classList) {
4927
+ if (
4928
+ element.classList.contains("sourceCode")
4929
+ || element.classList.contains("mermaid-container")
4930
+ ) {
4931
+ return "code";
4932
+ }
4933
+ if (
4934
+ element.classList.contains("callout-note")
4935
+ || element.classList.contains("callout-tip")
4936
+ || element.classList.contains("callout-warning")
4937
+ || element.classList.contains("callout-important")
4938
+ || element.classList.contains("callout-caution")
4939
+ ) {
4940
+ return "blockquote";
4941
+ }
4942
+ }
4943
+ return "";
4944
+ }
4945
+
4946
+ function isPreviewCommentTargetElement(element) {
4947
+ return Boolean(getPreviewCommentTargetKind(element));
4948
+ }
4949
+
4950
+ function collectPreviewCommentTargetElements(targetEl) {
4951
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return [];
4952
+ const selector = "h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, table, div.sourceCode, pre, .callout-note, .callout-tip, .callout-warning, .callout-important, .callout-caution, .mermaid-container";
4953
+ return Array.from(targetEl.querySelectorAll(selector)).filter((element) => {
4954
+ if (!isPreviewCommentTargetElement(element)) return false;
4955
+ let ancestor = element.parentElement;
4956
+ while (ancestor && ancestor !== targetEl) {
4957
+ if (ancestor.classList && ancestor.classList.contains("preview-comment-block")) return false;
4958
+ if (isPreviewCommentTargetElement(ancestor)) return false;
4959
+ ancestor = ancestor.parentElement;
4960
+ }
4961
+ return true;
4962
+ }).map((element) => ({
4963
+ element,
4964
+ kind: getPreviewCommentTargetKind(element),
4965
+ }));
4966
+ }
4967
+
4968
+ function getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock) {
4969
+ if (!sourceBlock) return "";
4970
+ const blockText = String(sourceText || "").slice(sourceBlock.start, sourceBlock.end);
4971
+ if (supportsPreviewSelectionCommentsForBlockKind(sourceBlock.kind)) {
4972
+ return normalizeVisiblePreviewText(buildPreviewSelectionDisplayMap(blockText, sourceBlock.kind).text);
4973
+ }
4974
+ if (sourceBlock.kind === "code") {
4975
+ return normalizeVisiblePreviewText(
4976
+ blockText
4977
+ .replace(/^ {0,3}(`{3,}|~{3,}).*$/gm, "")
4978
+ .replace(/^ {0,3}$/gm, ""),
4979
+ );
4980
+ }
4981
+ if (sourceBlock.kind === "table") {
4982
+ return normalizeVisiblePreviewText(
4983
+ blockText
4984
+ .replace(/^\s*\|?(?:\s*:?-{3,}:?\s*\|)+(?:\s*:?-{3,}:?\s*)?\|?\s*$/gm, "")
4985
+ .replace(/\|/g, " "),
4986
+ );
4987
+ }
4988
+ return normalizeVisiblePreviewText(blockText);
4989
+ }
4990
+
4991
+ function getNormalizedPreviewCommentTargetText(targetEntry) {
4992
+ if (!targetEntry) return "";
4993
+ if (typeof targetEntry.normalizedText === "string") return targetEntry.normalizedText;
4994
+ targetEntry.normalizedText = normalizeVisiblePreviewText(
4995
+ targetEntry.element && typeof targetEntry.element.textContent === "string"
4996
+ ? targetEntry.element.textContent
4997
+ : "",
4998
+ );
4999
+ return targetEntry.normalizedText;
5000
+ }
5001
+
5002
+ function findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, startIndex) {
5003
+ const desiredKind = sourceBlock ? sourceBlock.kind : "";
5004
+ const desiredText = getNormalizedPreviewCommentSourceBlockText(sourceText, sourceBlock);
5005
+ let fallbackIndex = -1;
5006
+ let containsIndex = -1;
5007
+
5008
+ for (let i = Math.max(0, startIndex || 0); i < targetBlocks.length; i += 1) {
5009
+ const targetEntry = targetBlocks[i];
5010
+ if (!targetEntry || targetEntry.kind !== desiredKind) continue;
5011
+ if (fallbackIndex < 0) fallbackIndex = i;
5012
+ const targetText = getNormalizedPreviewCommentTargetText(targetEntry);
5013
+ if (desiredText && targetText) {
5014
+ if (targetText === desiredText) {
5015
+ return i;
5016
+ }
5017
+ if (containsIndex < 0 && (targetText.includes(desiredText) || desiredText.includes(targetText))) {
5018
+ containsIndex = i;
5019
+ }
5020
+ }
5021
+ }
5022
+
5023
+ if (containsIndex >= 0) return containsIndex;
5024
+ return fallbackIndex;
5025
+ }
5026
+
5027
+ function getPreviewCommentNotesForRange(start, end, sourceText, displayNotes) {
5028
+ const source = String(sourceText || "");
5029
+ const notes = Array.isArray(displayNotes) ? displayNotes : getDisplayReviewNotes();
5030
+ return notes.filter((note) => {
5031
+ const range = resolveReviewNoteRange(note, source);
5032
+ return range && rangesOverlap(range.start, range.end, start, end);
5033
+ });
5034
+ }
5035
+
5036
+ function updatePreviewCommentBlockState(blockEl, sourceText, displayNotes) {
5037
+ if (!blockEl || !blockEl.dataset) return;
5038
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5039
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5040
+ const summaryBtn = blockEl.querySelector(".preview-comment-summary");
5041
+ const addBtn = blockEl.querySelector(".preview-comment-add");
5042
+ const lineLabel = summarizeReviewNoteAnchor({ lineStart: lineStart, lineEnd: lineEnd }).toLowerCase();
5043
+ const blockKindLabel = getPreviewCommentBlockKindLabel(blockEl.dataset.previewCommentKind || "paragraph");
5044
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5045
+ const hasSelection = Boolean(activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey);
5046
+
5047
+ blockEl.classList.remove("has-comments");
5048
+ blockEl.classList.toggle("has-selection", hasSelection);
5049
+
5050
+ if (summaryBtn) {
5051
+ summaryBtn.hidden = true;
5052
+ summaryBtn.textContent = "";
5053
+ summaryBtn.dataset.reviewNoteId = "";
5054
+ }
5055
+
5056
+ if (addBtn) {
5057
+ addBtn.hidden = !hasSelection;
5058
+ addBtn.textContent = "Comment";
5059
+ addBtn.dataset.previewCommentMode = hasSelection ? "selection" : "";
5060
+ addBtn.title = hasSelection
5061
+ ? ("Add a local comment from the current preview selection on this " + blockKindLabel + " (" + lineLabel + ").")
5062
+ : "";
5063
+ addBtn.setAttribute("aria-label", addBtn.title || "Comment");
5064
+ }
5065
+ }
5066
+
5067
+ function updatePreviewCommentBlocksForElement(targetEl) {
5068
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5069
+ const sourceText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5070
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5071
+ updatePreviewCommentBlockState(blockEl, sourceText);
5072
+ });
5073
+ }
5074
+
5075
+ function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
5076
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
5077
+ const sourceBlocks = scanMarkdownPreviewCommentBlocks(sourceText);
5078
+ const targetBlocks = collectPreviewCommentTargetElements(targetEl);
5079
+ if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
5080
+
5081
+ let targetIndex = 0;
5082
+ for (const sourceBlock of sourceBlocks) {
5083
+ const matchedTargetIndex = findMatchingPreviewCommentTargetIndex(sourceText, sourceBlock, targetBlocks, targetIndex);
5084
+ if (matchedTargetIndex < 0) continue;
5085
+
5086
+ const targetEntry = targetBlocks[matchedTargetIndex];
5087
+ targetIndex = matchedTargetIndex + 1;
5088
+ const originalElement = targetEntry && targetEntry.element ? targetEntry.element : null;
5089
+ if (!originalElement || !originalElement.parentNode) continue;
5090
+
5091
+ const wrapper = document.createElement("div");
5092
+ wrapper.className = "preview-comment-block";
5093
+ wrapper.dataset.reviewNoteStart = String(sourceBlock.start);
5094
+ wrapper.dataset.reviewNoteEnd = String(sourceBlock.end);
5095
+ wrapper.dataset.reviewNoteLineStart = String(sourceBlock.lineStart);
5096
+ wrapper.dataset.reviewNoteLineEnd = String(sourceBlock.lineEnd);
5097
+ wrapper.dataset.previewCommentKind = sourceBlock.kind;
5098
+
5099
+ const controls = document.createElement("div");
5100
+ controls.className = "preview-comment-controls";
5101
+
5102
+ const summaryBtn = document.createElement("button");
5103
+ summaryBtn.type = "button";
5104
+ summaryBtn.className = "preview-comment-summary";
5105
+ summaryBtn.hidden = true;
5106
+ controls.appendChild(summaryBtn);
5107
+
5108
+ const addBtn = document.createElement("button");
5109
+ addBtn.type = "button";
5110
+ addBtn.className = "preview-comment-add";
5111
+ addBtn.textContent = "Comment";
5112
+ controls.appendChild(addBtn);
5113
+
5114
+ originalElement.replaceWith(wrapper);
5115
+ wrapper.appendChild(controls);
5116
+ originalElement.classList.add("preview-comment-block-content");
5117
+ wrapper.appendChild(originalElement);
5118
+ }
5119
+
5120
+ updatePreviewCommentBlocksForElement(targetEl);
5121
+ }
5122
+
5123
+ function refreshRenderedEditorPreviewComments() {
5124
+ if (sourcePreviewEl && !sourcePreviewEl.hidden) {
5125
+ updatePreviewCommentBlocksForElement(sourcePreviewEl);
5126
+ }
5127
+ if (critiqueViewEl && rightView === "editor-preview") {
5128
+ updatePreviewCommentBlocksForElement(critiqueViewEl);
5129
+ }
5130
+ }
5131
+
5132
+ function buildReviewNoteAnchorFromPreviewBlock(blockEl) {
5133
+ if (!blockEl || !blockEl.dataset) return null;
5134
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5135
+ const selectionStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5136
+ const selectionEnd = Math.max(selectionStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || selectionStart, source.length));
5137
+ const lineStart = Math.max(1, Number(blockEl.dataset.reviewNoteLineStart) || 1);
5138
+ const lineEnd = Math.max(lineStart, Number(blockEl.dataset.reviewNoteLineEnd) || lineStart);
5139
+ return {
5140
+ selectionStart,
5141
+ selectionEnd,
5142
+ lineStart,
5143
+ lineEnd,
5144
+ selectedText: source.slice(selectionStart, selectionEnd),
5145
+ selectedDisplayText: source.slice(selectionStart, selectionEnd),
5146
+ };
5147
+ }
5148
+
5149
+ function buildReviewNoteAnchorFromPreviewSelection(blockEl, contentEl, range) {
5150
+ if (!blockEl || !blockEl.dataset || !contentEl || !range) return null;
5151
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5152
+ if (!supportsPreviewSelectionCommentsForBlockKind(kind)) return null;
5153
+ if (!contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) return null;
5154
+
5155
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5156
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5157
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5158
+ if (blockEnd <= blockStart) return null;
5159
+
5160
+ const sourceBlockText = source.slice(blockStart, blockEnd);
5161
+ const displayMap = buildPreviewSelectionDisplayMap(sourceBlockText, kind);
5162
+ if (!displayMap.text || !displayMap.charStarts.length || !displayMap.charEnds.length) return null;
5163
+
5164
+ const prefixRange = document.createRange();
5165
+ prefixRange.selectNodeContents(contentEl);
5166
+ prefixRange.setEnd(range.startContainer, range.startOffset);
5167
+ const prefixText = normalizeVisiblePreviewText(prefixRange.toString());
5168
+ const selectedDisplayText = normalizeVisiblePreviewText(range.toString());
5169
+ if (!selectedDisplayText) return null;
5170
+
5171
+ const desiredStart = Math.max(0, Math.min(prefixText.length, displayMap.text.length));
5172
+ const bestIndex = findPreferredNormalizedTextMatch(displayMap.text, selectedDisplayText, desiredStart);
5173
+ if (bestIndex < 0) return null;
5174
+
5175
+ const endIndex = bestIndex + selectedDisplayText.length - 1;
5176
+ const rawStartRel = displayMap.charStarts[bestIndex];
5177
+ const rawEndRel = displayMap.charEnds[endIndex];
5178
+ if (!Number.isFinite(rawStartRel) || !Number.isFinite(rawEndRel) || rawEndRel <= rawStartRel) {
5179
+ return null;
5180
+ }
5181
+
5182
+ const selectionStart = blockStart + rawStartRel;
5183
+ const selectionEnd = blockStart + rawEndRel;
5184
+ return {
5185
+ selectionStart,
5186
+ selectionEnd,
5187
+ lineStart: getLineNumberAtOffset(source, selectionStart),
5188
+ lineEnd: getLineNumberAtOffset(source, Math.max(selectionStart, selectionEnd - 1)),
5189
+ selectedText: source.slice(selectionStart, selectionEnd),
5190
+ selectedDisplayText,
5191
+ };
5192
+ }
5193
+
5194
+ function getPreviewJumpNormalizedSelectionStart(note, blockEl, range) {
5195
+ if (!note || !blockEl || !blockEl.dataset || !range) return 0;
5196
+ const kind = String(blockEl.dataset.previewCommentKind || "");
5197
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5198
+ const blockStart = Math.max(0, Math.min(Number(blockEl.dataset.reviewNoteStart) || 0, source.length));
5199
+ const blockEnd = Math.max(blockStart, Math.min(Number(blockEl.dataset.reviewNoteEnd) || blockStart, source.length));
5200
+ const displayMap = buildPreviewSelectionDisplayMap(source.slice(blockStart, blockEnd), kind);
5201
+ if (!displayMap || !displayMap.charStarts || displayMap.charStarts.length === 0) return 0;
5202
+ const relativeStart = Math.max(0, range.start - blockStart);
5203
+ for (let i = 0; i < displayMap.charStarts.length; i += 1) {
5204
+ const charStart = Number(displayMap.charStarts[i]);
5205
+ const charEnd = Number(displayMap.charEnds[i]);
5206
+ if (charEnd > relativeStart && charStart <= relativeStart) {
5207
+ return i;
5208
+ }
5209
+ if (charStart >= relativeStart) {
5210
+ return i;
5211
+ }
5212
+ }
5213
+ return Math.max(0, displayMap.text.length - 1);
5214
+ }
5215
+
5216
+ function createPreviewJumpInlineHighlight(contentEl, blockEl, note, range) {
5217
+ if (!contentEl || !note || !range) return null;
5218
+ const selectedDisplayText = normalizeVisiblePreviewText(note.selectedDisplayText || note.selectedText || "");
5219
+ if (!selectedDisplayText) return null;
5220
+ const domMap = buildNormalizedDomTextMap(contentEl);
5221
+ if (!domMap.text || !domMap.charStarts.length || !domMap.charEnds.length) return null;
5222
+ const preferredStart = getPreviewJumpNormalizedSelectionStart(note, blockEl, range);
5223
+ const matchIndex = findPreferredNormalizedTextMatch(domMap.text, selectedDisplayText, preferredStart);
5224
+ if (matchIndex < 0) return null;
5225
+ const endIndex = matchIndex + selectedDisplayText.length - 1;
5226
+ const startRef = domMap.charStarts[matchIndex];
5227
+ const endRef = domMap.charEnds[endIndex];
5228
+ if (!startRef || !endRef || !startRef.node || !endRef.node) return null;
5229
+
5230
+ const domRange = document.createRange();
5231
+ domRange.setStart(startRef.node, startRef.offset);
5232
+ domRange.setEnd(endRef.node, endRef.offset);
5233
+
5234
+ const highlightEl = document.createElement("span");
5235
+ highlightEl.className = "preview-comment-inline-highlight";
5236
+ try {
5237
+ domRange.surroundContents(highlightEl);
5238
+ } catch {
5239
+ const fragment = domRange.extractContents();
5240
+ highlightEl.appendChild(fragment);
5241
+ domRange.insertNode(highlightEl);
5242
+ }
5243
+ return highlightEl;
5244
+ }
5245
+
5246
+ function findPreviewCommentBlockForRange(targetEl, range) {
5247
+ if (!targetEl || !range || typeof targetEl.querySelectorAll !== "function") return null;
5248
+ let bestBlock = null;
5249
+ let bestScore = Number.NEGATIVE_INFINITY;
5250
+ Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
5251
+ const blockStart = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5252
+ const blockEnd = Math.max(blockStart, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || blockStart);
5253
+ const overlapStart = Math.max(blockStart, range.start);
5254
+ const overlapEnd = Math.min(blockEnd, range.end);
5255
+ const overlap = Math.max(0, overlapEnd - overlapStart);
5256
+ const contains = range.start >= blockStart && range.end <= blockEnd;
5257
+ const distance = contains
5258
+ ? 0
5259
+ : Math.min(Math.abs(range.start - blockEnd), Math.abs(range.end - blockStart));
5260
+ const score = contains
5261
+ ? (1000000 - (blockEnd - blockStart))
5262
+ : (overlap > 0 ? overlap : -distance);
5263
+ if (score > bestScore) {
5264
+ bestScore = score;
5265
+ bestBlock = blockEl;
5266
+ }
5267
+ });
5268
+ return bestBlock;
5269
+ }
5270
+
5271
+ function revealReviewNoteInPreviewElement(targetEl, note) {
5272
+ if (!targetEl || !note) return false;
5273
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5274
+ const range = resolveReviewNoteRange(note, source);
5275
+ if (!range) return false;
5276
+ const blockEl = findPreviewCommentBlockForRange(targetEl, range);
5277
+ if (!blockEl) return false;
5278
+ const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
5279
+ const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
5280
+ if (typeof blockEl.scrollIntoView === "function") {
5281
+ blockEl.scrollIntoView({ block: "center", inline: "nearest" });
5282
+ }
5283
+ setPreviewJumpHighlight(targetEl, contentEl, inlineHighlightEl);
5284
+ return true;
5285
+ }
5286
+
5287
+ function revealReviewNoteInPreview(note) {
5288
+ if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
5289
+ revealReviewNoteInPreviewElement(critiqueViewEl, note);
5290
+ }
5291
+ }
5292
+
5293
+ function updateActivePreviewCommentSelectionFromDom() {
5294
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5295
+ if (!selection || selection.rangeCount <= 0 || selection.isCollapsed) {
5296
+ clearPreviewCommentSelection();
5297
+ return;
5298
+ }
5299
+
5300
+ const range = selection.getRangeAt(0);
5301
+ const startBlock = findPreviewCommentBlockFromNode(range.startContainer);
5302
+ const endBlock = findPreviewCommentBlockFromNode(range.endContainer);
5303
+ if (!startBlock || !endBlock || startBlock !== endBlock) {
5304
+ clearPreviewCommentSelection();
5305
+ return;
5306
+ }
5307
+
5308
+ const contentEl = startBlock.querySelector(".preview-comment-block-content");
5309
+ if (!contentEl || !contentEl.contains(range.startContainer) || !contentEl.contains(range.endContainer)) {
5310
+ clearPreviewCommentSelection();
5311
+ return;
5312
+ }
5313
+
5314
+ const anchor = buildReviewNoteAnchorFromPreviewSelection(startBlock, contentEl, range);
5315
+ if (!anchor) {
5316
+ clearPreviewCommentSelection();
5317
+ return;
5318
+ }
5319
+
5320
+ setActivePreviewCommentSelection({
5321
+ ...anchor,
5322
+ blockKey: getPreviewCommentBlockKey(startBlock),
5323
+ });
5324
+ }
5325
+
4317
5326
  function getDisplayReviewNotes() {
4318
5327
  const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
4319
5328
  return reviewNotes.slice().sort((left, right) => {
@@ -4386,6 +5395,7 @@
4386
5395
  reviewNotes = cloneReviewNotes(nextNotes);
4387
5396
  updateReviewNotesUi();
4388
5397
  renderReviewNotesList();
5398
+ refreshRenderedEditorPreviewComments();
4389
5399
  if (editorView === "markdown") {
4390
5400
  scheduleEditorLineNumberRender();
4391
5401
  }
@@ -4394,6 +5404,22 @@
4394
5404
  }
4395
5405
  }
4396
5406
 
5407
+ function updateEditorSelectionCommentUi() {
5408
+ if (!editorSelectionCommentBtn) return;
5409
+ const hasSelection = Boolean(
5410
+ editorView === "markdown"
5411
+ && document.activeElement === sourceTextEl
5412
+ && typeof sourceTextEl.selectionStart === "number"
5413
+ && typeof sourceTextEl.selectionEnd === "number"
5414
+ && sourceTextEl.selectionEnd > sourceTextEl.selectionStart
5415
+ );
5416
+ editorSelectionCommentBtn.hidden = !hasSelection;
5417
+ if (hasSelection) {
5418
+ editorSelectionCommentBtn.title = "Create a new local comment from the current editor selection.";
5419
+ editorSelectionCommentBtn.setAttribute("aria-label", editorSelectionCommentBtn.title);
5420
+ }
5421
+ }
5422
+
4397
5423
  function updateReviewNotesUi() {
4398
5424
  const descriptor = getCurrentStudioDocumentDescriptor();
4399
5425
  const count = reviewNotes.length;
@@ -4421,8 +5447,10 @@
4421
5447
  if (reviewNotesAddBtn) {
4422
5448
  reviewNotesAddBtn.disabled = editorView !== "markdown";
4423
5449
  reviewNotesAddBtn.title = editorView === "markdown"
4424
- ? "Create a new local comment from the current editor selection, or from the current line if nothing is selected."
4425
- : "Switch to Editor (Raw) to anchor a comment to the current selection or line.";
5450
+ ? "Create a new local comment on the current editor line."
5451
+ : (supportsPreviewCommentsForCurrentEditor()
5452
+ ? "Select preview text and use Comment for a local preview-anchored comment."
5453
+ : "Switch to Editor (Raw) to comment on the current line.");
4426
5454
  }
4427
5455
  if (reviewNotesInlineAllBtn) {
4428
5456
  const currentText = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
@@ -4580,12 +5608,49 @@
4580
5608
  pendingReviewNoteInlineFocusId = null;
4581
5609
  }
4582
5610
 
4583
- function addReviewNoteFromEditorSelection() {
4584
- if (editorView !== "markdown") {
4585
- setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
4586
- return;
5611
+ function focusReviewNotesForPreviewBlock(blockEl) {
5612
+ if (!blockEl) return;
5613
+ const start = Math.max(0, Number(blockEl.dataset && blockEl.dataset.reviewNoteStart) || 0);
5614
+ const end = Math.max(start, Number(blockEl.dataset && blockEl.dataset.reviewNoteEnd) || start);
5615
+ const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
5616
+ const notes = getPreviewCommentNotesForRange(start, end, source);
5617
+ if (!notes.length) return;
5618
+ focusReviewNoteInPanel(notes[0].id);
5619
+ }
5620
+
5621
+ function addReviewNoteFromPreviewBlock(blockEl) {
5622
+ const anchor = buildReviewNoteAnchorFromPreviewBlock(blockEl);
5623
+ if (!anchor) return null;
5624
+ return addReviewNoteFromAnchor(anchor, {
5625
+ statusMessage: "Added local comment from editor preview.",
5626
+ });
5627
+ }
5628
+
5629
+ function addReviewNoteFromPreviewSelection(blockEl) {
5630
+ if (!blockEl) return null;
5631
+ const blockKey = getPreviewCommentBlockKey(blockEl);
5632
+ const anchor = activePreviewCommentSelection && activePreviewCommentSelection.blockKey === blockKey
5633
+ ? activePreviewCommentSelection
5634
+ : null;
5635
+ if (!anchor) {
5636
+ setStatus("Select some preview text within a single block first.", "warning");
5637
+ return null;
4587
5638
  }
4588
- const anchor = getEditorAnchorForReviewNote();
5639
+ const note = addReviewNoteFromAnchor(anchor, {
5640
+ statusMessage: "Added local comment from preview selection.",
5641
+ });
5642
+ if (note) {
5643
+ const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
5644
+ if (selection && typeof selection.removeAllRanges === "function") {
5645
+ selection.removeAllRanges();
5646
+ }
5647
+ clearPreviewCommentSelection();
5648
+ }
5649
+ return note;
5650
+ }
5651
+
5652
+ function addReviewNoteFromAnchor(anchor, options) {
5653
+ if (!anchor || typeof anchor !== "object") return null;
4589
5654
  const note = normalizeReviewNote({
4590
5655
  id: makeRequestId(),
4591
5656
  text: "",
@@ -4596,14 +5661,47 @@
4596
5661
  lineStart: anchor.lineStart,
4597
5662
  lineEnd: anchor.lineEnd,
4598
5663
  selectedText: anchor.selectedText,
5664
+ selectedDisplayText: typeof anchor.selectedDisplayText === "string" ? anchor.selectedDisplayText : (typeof anchor.selectedText === "string" ? anchor.selectedText : ""),
4599
5665
  });
4600
- if (!note) return;
5666
+ if (!note) return null;
5667
+ if (editorSelectionCommentBtn) {
5668
+ editorSelectionCommentBtn.hidden = true;
5669
+ }
4601
5670
  pendingReviewNoteFocusId = note.id;
4602
5671
  setReviewNotes(reviewNotes.concat([note]));
4603
5672
  if (!isReviewNotesOpen()) {
4604
5673
  openReviewNotes();
4605
5674
  }
4606
- setStatus("Added local comment.", "success");
5675
+ const schedule = typeof window.requestAnimationFrame === "function"
5676
+ ? window.requestAnimationFrame.bind(window)
5677
+ : (cb) => window.setTimeout(cb, 16);
5678
+ schedule(() => {
5679
+ updateEditorSelectionCommentUi();
5680
+ });
5681
+ if (!options || options.status !== false) {
5682
+ setStatus((options && options.statusMessage) || "Added local comment.", "success");
5683
+ }
5684
+ return note;
5685
+ }
5686
+
5687
+ function addReviewNoteFromEditorSelection() {
5688
+ if (editorView !== "markdown") {
5689
+ setStatus("Switch to Editor (Raw) before adding an anchored comment.", "warning");
5690
+ return;
5691
+ }
5692
+ addReviewNoteFromAnchor(getEditorAnchorForReviewNote(), {
5693
+ statusMessage: "Added local comment.",
5694
+ });
5695
+ }
5696
+
5697
+ function addReviewNoteFromEditorLine() {
5698
+ if (editorView !== "markdown") {
5699
+ setStatus("Switch to Editor (Raw) before adding a line comment.", "warning");
5700
+ return;
5701
+ }
5702
+ addReviewNoteFromAnchor(getEditorLineAnchorForReviewNote(), {
5703
+ statusMessage: "Added local line comment.",
5704
+ });
4607
5705
  }
4608
5706
 
4609
5707
  function jumpToReviewNote(noteId) {
@@ -4624,6 +5722,7 @@
4624
5722
  : (cb) => window.setTimeout(cb, 16);
4625
5723
  schedule(() => {
4626
5724
  scrollEditorRangeIntoView(range);
5725
+ revealReviewNoteInPreview(note);
4627
5726
  });
4628
5727
  }
4629
5728
 
@@ -6116,14 +7215,43 @@
6116
7215
  });
6117
7216
 
6118
7217
  sourceTextEl.addEventListener("input", () => {
7218
+ if (activePreviewCommentSelection) {
7219
+ clearPreviewCommentSelection();
7220
+ }
6119
7221
  renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
6120
7222
  scheduleEditorMetaUpdate();
7223
+ updateEditorSelectionCommentUi();
6121
7224
  if (isReviewNotesOpen() && reviewNotes.length > 0) {
6122
7225
  renderReviewNotesList();
6123
7226
  updateReviewNotesUi();
6124
7227
  }
6125
7228
  });
6126
7229
 
7230
+ sourceTextEl.addEventListener("select", () => {
7231
+ updateEditorSelectionCommentUi();
7232
+ });
7233
+
7234
+ sourceTextEl.addEventListener("keyup", () => {
7235
+ updateEditorSelectionCommentUi();
7236
+ });
7237
+
7238
+ sourceTextEl.addEventListener("mouseup", () => {
7239
+ updateEditorSelectionCommentUi();
7240
+ });
7241
+
7242
+ sourceTextEl.addEventListener("focus", () => {
7243
+ updateEditorSelectionCommentUi();
7244
+ });
7245
+
7246
+ sourceTextEl.addEventListener("blur", () => {
7247
+ const schedule = typeof window.requestAnimationFrame === "function"
7248
+ ? window.requestAnimationFrame.bind(window)
7249
+ : (cb) => window.setTimeout(cb, 16);
7250
+ schedule(() => {
7251
+ updateEditorSelectionCommentUi();
7252
+ });
7253
+ });
7254
+
6127
7255
  sourceTextEl.addEventListener("scroll", () => {
6128
7256
  if (editorView !== "markdown") return;
6129
7257
  syncEditorHighlightScroll();
@@ -6487,6 +7615,15 @@
6487
7615
 
6488
7616
  if (reviewNotesAddBtn) {
6489
7617
  reviewNotesAddBtn.addEventListener("click", () => {
7618
+ addReviewNoteFromEditorLine();
7619
+ });
7620
+ }
7621
+
7622
+ if (editorSelectionCommentBtn) {
7623
+ editorSelectionCommentBtn.addEventListener("mousedown", (event) => {
7624
+ event.preventDefault();
7625
+ });
7626
+ editorSelectionCommentBtn.addEventListener("click", () => {
6490
7627
  addReviewNoteFromEditorSelection();
6491
7628
  });
6492
7629
  }
@@ -6508,6 +7645,42 @@
6508
7645
  });
6509
7646
  }
6510
7647
 
7648
+ function handlePreviewCommentActionMouseDown(event) {
7649
+ const target = event.target;
7650
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7651
+ if (!actionBtn) return;
7652
+ event.preventDefault();
7653
+ }
7654
+
7655
+ function handlePreviewCommentActionClick(event) {
7656
+ const target = event.target;
7657
+ const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-summary") : null;
7658
+ if (!actionBtn) return;
7659
+ const blockEl = actionBtn.closest(".preview-comment-block");
7660
+ if (!blockEl) return;
7661
+ event.preventDefault();
7662
+ event.stopPropagation();
7663
+ const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
7664
+ if (mode !== "selection") return;
7665
+ addReviewNoteFromPreviewSelection(blockEl);
7666
+ }
7667
+
7668
+ if (leftPaneEl) {
7669
+ leftPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7670
+ leftPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7671
+ }
7672
+
7673
+ if (rightPaneEl) {
7674
+ rightPaneEl.addEventListener("mousedown", handlePreviewCommentActionMouseDown);
7675
+ rightPaneEl.addEventListener("click", handlePreviewCommentActionClick);
7676
+ }
7677
+
7678
+ if (typeof document.addEventListener === "function") {
7679
+ document.addEventListener("selectionchange", () => {
7680
+ updateActivePreviewCommentSelectionFromDom();
7681
+ });
7682
+ }
7683
+
6511
7684
  if (scratchpadBtn) {
6512
7685
  scratchpadBtn.addEventListener("click", () => {
6513
7686
  openScratchpad();