pi-studio 0.9.9 → 0.9.11

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.
@@ -119,6 +119,10 @@
119
119
  const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
120
120
  const annotationModeSelect = document.getElementById("annotationModeSelect");
121
121
  const compactBtn = document.getElementById("compactBtn");
122
+ const shortcutsBtn = document.getElementById("shortcutsBtn");
123
+ const shortcutsOverlayEl = document.getElementById("shortcutsOverlay");
124
+ const shortcutsDialogEl = document.getElementById("shortcutsDialog");
125
+ const shortcutsCloseBtn = document.getElementById("shortcutsCloseBtn");
122
126
  const leftFocusBtn = document.getElementById("leftFocusBtn");
123
127
  const rightFocusBtn = document.getElementById("rightFocusBtn");
124
128
  const reviewNotesBtn = document.getElementById("reviewNotesBtn");
@@ -241,6 +245,7 @@
241
245
  const REPL_JOURNAL_MAX_ENTRIES = 80;
242
246
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
243
247
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
248
+ const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
244
249
  const EDITOR_TAB_TEXT = " ";
245
250
  const QUIZ_DEFAULT_COUNT = 5;
246
251
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
@@ -1905,7 +1910,7 @@
1905
1910
  if (document.body) document.body.classList.toggle("studio-zen-mode", studioZenModeEnabled);
1906
1911
  if (!zenModeBtn) return;
1907
1912
  zenModeBtn.textContent = studioZenModeEnabled ? "Exit Zen" : "Zen";
1908
- zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls." : "Hide secondary Studio controls.";
1913
+ zenModeBtn.title = studioZenModeEnabled ? "Show full Studio controls. Shortcut: F9." : "Hide secondary Studio controls. Shortcut: F9.";
1909
1914
  zenModeBtn.setAttribute("aria-pressed", studioZenModeEnabled ? "true" : "false");
1910
1915
  }
1911
1916
 
@@ -3084,6 +3089,110 @@
3084
3089
  }
3085
3090
  }
3086
3091
 
3092
+ function focusPaneViewControl(pane) {
3093
+ const control = pane === "right" ? rightViewSelect : editorViewSelect;
3094
+ if (!control || control.disabled || control.hidden) return false;
3095
+ try {
3096
+ control.focus({ preventScroll: true });
3097
+ } catch {
3098
+ try { control.focus(); } catch { return false; }
3099
+ }
3100
+ return true;
3101
+ }
3102
+
3103
+ function activatePaneFromShortcut(nextPane) {
3104
+ const pane = nextPane === "right" ? "right" : "left";
3105
+ if (isEditorOnlyMode && pane === "right") {
3106
+ setStatus("Only the editor pane is available in editor-only Studio.", "warning");
3107
+ return;
3108
+ }
3109
+ const snapshot = snapshotStudioScrollablePositions();
3110
+ setActivePane(pane);
3111
+ scheduleStudioScrollablePositionRestore(snapshot);
3112
+ focusPaneViewControl(pane);
3113
+ setStatus("Active pane: " + paneLabel(pane) + ". F7 cycles this pane's view.");
3114
+ }
3115
+
3116
+ function getSelectEnabledValues(selectEl) {
3117
+ if (!selectEl || !selectEl.options) return [];
3118
+ return Array.from(selectEl.options)
3119
+ .filter((option) => option && !option.disabled)
3120
+ .map((option) => option.value)
3121
+ .filter((value) => typeof value === "string" && value);
3122
+ }
3123
+
3124
+ function getCycledSelectValue(selectEl, currentValue, direction) {
3125
+ const values = getSelectEnabledValues(selectEl);
3126
+ if (!values.length) return null;
3127
+ const currentIndex = values.indexOf(currentValue);
3128
+ const startIndex = currentIndex >= 0 ? currentIndex : 0;
3129
+ const step = direction < 0 ? -1 : 1;
3130
+ return values[(startIndex + step + values.length) % values.length];
3131
+ }
3132
+
3133
+ function focusEditorTextFromShortcut() {
3134
+ const snapshot = snapshotStudioScrollablePositions();
3135
+ setActivePane("left");
3136
+ if (editorView !== "markdown") setEditorView("markdown");
3137
+ scheduleStudioScrollablePositionRestore(snapshot);
3138
+ window.setTimeout(() => {
3139
+ if (sourceTextEl && typeof sourceTextEl.focus === "function") {
3140
+ try {
3141
+ sourceTextEl.focus({ preventScroll: true });
3142
+ } catch {
3143
+ try { sourceTextEl.focus(); } catch {}
3144
+ }
3145
+ }
3146
+ }, 0);
3147
+ setStatus("Editor text focused.");
3148
+ }
3149
+
3150
+ function focusRightContentFromShortcut() {
3151
+ if (isEditorOnlyMode) {
3152
+ setStatus("Only the editor pane is available in editor-only Studio.", "warning");
3153
+ return;
3154
+ }
3155
+ const snapshot = snapshotStudioScrollablePositions();
3156
+ setActivePane("right");
3157
+ scheduleStudioScrollablePositionRestore(snapshot);
3158
+ window.setTimeout(() => {
3159
+ if (critiqueViewEl && typeof critiqueViewEl.focus === "function") {
3160
+ if (!critiqueViewEl.hasAttribute("tabindex")) critiqueViewEl.setAttribute("tabindex", "-1");
3161
+ try {
3162
+ critiqueViewEl.focus({ preventScroll: true });
3163
+ } catch {
3164
+ try { critiqueViewEl.focus(); } catch {}
3165
+ }
3166
+ }
3167
+ }, 0);
3168
+ setStatus("Right pane content focused.");
3169
+ }
3170
+
3171
+ function cycleActivePaneView(direction) {
3172
+ if (activePane === "right") {
3173
+ if (isEditorOnlyMode || !rightViewSelect || rightViewSelect.disabled) {
3174
+ setStatus("The right-pane view selector is unavailable.", "warning");
3175
+ return;
3176
+ }
3177
+ const nextView = getCycledSelectValue(rightViewSelect, rightView, direction);
3178
+ if (!nextView) return;
3179
+ setRightView(nextView);
3180
+ focusPaneViewControl("right");
3181
+ setStatus("Right pane view: " + (rightViewSelect.selectedOptions && rightViewSelect.selectedOptions[0] ? rightViewSelect.selectedOptions[0].textContent : nextView) + ".");
3182
+ return;
3183
+ }
3184
+
3185
+ if (!editorViewSelect || editorViewSelect.disabled) {
3186
+ setStatus("The editor view selector is unavailable.", "warning");
3187
+ return;
3188
+ }
3189
+ const nextView = getCycledSelectValue(editorViewSelect, editorView, direction);
3190
+ if (!nextView) return;
3191
+ setEditorView(nextView);
3192
+ focusPaneViewControl("left");
3193
+ setStatus("Editor view: " + (editorViewSelect.selectedOptions && editorViewSelect.selectedOptions[0] ? editorViewSelect.selectedOptions[0].textContent : nextView) + ".");
3194
+ }
3195
+
3087
3196
  function paneLabel(pane) {
3088
3197
  if (pane === "right") {
3089
3198
  return "Response";
@@ -3131,6 +3240,17 @@
3131
3240
  return false;
3132
3241
  }
3133
3242
 
3243
+ function isTextEntryShortcutTarget(target) {
3244
+ if (!(target instanceof Element)) return false;
3245
+ const editable = target.closest("input, textarea, select, [contenteditable]");
3246
+ if (!editable) return false;
3247
+ if (editable.hasAttribute && editable.hasAttribute("contenteditable")) {
3248
+ const value = String(editable.getAttribute("contenteditable") || "").toLowerCase();
3249
+ return value !== "false";
3250
+ }
3251
+ return true;
3252
+ }
3253
+
3134
3254
  function handlePaneShortcut(event) {
3135
3255
  if (!event || event.defaultPrevented) return;
3136
3256
 
@@ -3158,6 +3278,12 @@
3158
3278
  && typeof outlineDialogEl.contains === "function"
3159
3279
  && outlineDialogEl.contains(event.target)
3160
3280
  );
3281
+ const shortcutsOwnsEvent = Boolean(
3282
+ shortcutsDialogEl
3283
+ && event.target
3284
+ && typeof shortcutsDialogEl.contains === "function"
3285
+ && shortcutsDialogEl.contains(event.target)
3286
+ );
3161
3287
  const pdfFocusOwnsEvent = Boolean(
3162
3288
  studioPdfFocusDialogEl
3163
3289
  && event.target
@@ -3201,6 +3327,12 @@
3201
3327
  return;
3202
3328
  }
3203
3329
 
3330
+ if (isShortcutsOpen() && plainEscape) {
3331
+ event.preventDefault();
3332
+ closeShortcuts();
3333
+ return;
3334
+ }
3335
+
3204
3336
  if (isReviewNotesOpen() && plainEscape) {
3205
3337
  event.preventDefault();
3206
3338
  closeReviewNotes();
@@ -3213,7 +3345,46 @@
3213
3345
  return;
3214
3346
  }
3215
3347
 
3216
- if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
3348
+ if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent || shortcutsOwnsEvent || pdfFocusOwnsEvent || htmlFocusOwnsEvent || quizOwnsEvent) {
3349
+ return;
3350
+ }
3351
+
3352
+ if ((key === "?" || (key === "/" && event.shiftKey)) && !event.metaKey && !event.ctrlKey && !event.altKey && !isTextEntryShortcutTarget(event.target)) {
3353
+ event.preventDefault();
3354
+ toggleShortcuts();
3355
+ return;
3356
+ }
3357
+
3358
+ const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
3359
+ if (isPaneSwitchShortcut) {
3360
+ event.preventDefault();
3361
+ activatePaneFromShortcut(activePane === "right" ? "left" : "right");
3362
+ return;
3363
+ }
3364
+
3365
+ const isViewCycleShortcut = key === "F7" && !event.metaKey && !event.ctrlKey && !event.altKey;
3366
+ if (isViewCycleShortcut) {
3367
+ event.preventDefault();
3368
+ cycleActivePaneView(event.shiftKey ? -1 : 1);
3369
+ return;
3370
+ }
3371
+
3372
+ const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
3373
+ if (isContentFocusShortcut) {
3374
+ event.preventDefault();
3375
+ if (event.shiftKey) {
3376
+ focusRightContentFromShortcut();
3377
+ } else {
3378
+ focusEditorTextFromShortcut();
3379
+ }
3380
+ return;
3381
+ }
3382
+
3383
+ const isZenModeShortcut = key === "F9" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
3384
+ if (isZenModeShortcut) {
3385
+ event.preventDefault();
3386
+ setStudioZenMode(!studioZenModeEnabled);
3387
+ setStatus(studioZenModeEnabled ? "Zen mode on." : "Zen mode off.");
3217
3388
  return;
3218
3389
  }
3219
3390
 
@@ -3798,6 +3969,11 @@
3798
3969
  + " root ? root.offsetHeight : 0\n"
3799
3970
  + " ));\n"
3800
3971
  + " }\n"
3972
+ + " function getScrollTop() {\n"
3973
+ + " const body = document.body;\n"
3974
+ + " const root = document.documentElement;\n"
3975
+ + " return window.scrollY || (root ? root.scrollTop : 0) || (body ? body.scrollTop : 0) || 0;\n"
3976
+ + " }\n"
3801
3977
  + " function sendHeight() {\n"
3802
3978
  + " scheduled = false;\n"
3803
3979
  + " const height = measureHeight();\n"
@@ -3810,13 +3986,243 @@
3810
3986
  + " scheduled = true;\n"
3811
3987
  + " requestAnimationFrame(sendHeight);\n"
3812
3988
  + " }\n"
3989
+ + " function decodeFragment(value) {\n"
3990
+ + " const text = String(value || '').replace(/^#/, '');\n"
3991
+ + " try { return decodeURIComponent(text); } catch { return text; }\n"
3992
+ + " }\n"
3993
+ + " function findNamedFragmentTarget(fragment) {\n"
3994
+ + " const decoded = decodeFragment(fragment);\n"
3995
+ + " if (!decoded) return document.documentElement || document.body;\n"
3996
+ + " return document.getElementById(decoded) || document.getElementsByName(decoded)[0] || null;\n"
3997
+ + " }\n"
3998
+ + " function postFragmentScroll(target) {\n"
3999
+ + " if (!target || typeof target.getBoundingClientRect !== 'function') return;\n"
4000
+ + " const rect = target.getBoundingClientRect();\n"
4001
+ + " const scrollTop = getScrollTop();\n"
4002
+ + " try {\n"
4003
+ + " parent.postMessage({ type: 'pi-studio-html-artifact-fragment', id: PREVIEW_ID, targetTop: Math.max(0, rect.top + scrollTop), scrollTop, viewportHeight: window.innerHeight || 0, documentHeight: measureHeight() }, '*');\n"
4004
+ + " } catch {}\n"
4005
+ + " }\n"
4006
+ + " function scrollFragmentIntoView(fragment, options) {\n"
4007
+ + " const target = findNamedFragmentTarget(fragment);\n"
4008
+ + " if (!target) return false;\n"
4009
+ + " const behavior = options && options.smooth === false ? 'auto' : 'smooth';\n"
4010
+ + " try { target.scrollIntoView({ block: 'start', inline: 'nearest', behavior }); } catch { try { target.scrollIntoView(true); } catch {} }\n"
4011
+ + " postFragmentScroll(target);\n"
4012
+ + " setTimeout(() => postFragmentScroll(target), 80);\n"
4013
+ + " setTimeout(() => postFragmentScroll(target), 300);\n"
4014
+ + " return true;\n"
4015
+ + " }\n"
4016
+ + " function getAnchorFromClickTarget(target) {\n"
4017
+ + " let node = target;\n"
4018
+ + " if (node && node.nodeType === 3) node = node.parentElement;\n"
4019
+ + " return node && typeof node.closest === 'function' ? node.closest('a[href]') : null;\n"
4020
+ + " }\n"
4021
+ + " function getSameDocumentFragment(anchor) {\n"
4022
+ + " if (!anchor || typeof anchor.getAttribute !== 'function') return null;\n"
4023
+ + " if (anchor.hasAttribute('download')) return null;\n"
4024
+ + " const target = String(anchor.getAttribute('target') || '').trim().toLowerCase();\n"
4025
+ + " if (target && target !== '_self') return null;\n"
4026
+ + " const rawHref = String(anchor.getAttribute('href') || '').trim();\n"
4027
+ + " if (!rawHref) return null;\n"
4028
+ + " if (rawHref.charAt(0) === '#') return rawHref.slice(1);\n"
4029
+ + " const hashIndex = rawHref.indexOf('#');\n"
4030
+ + " if (hashIndex < 0) return null;\n"
4031
+ + " const beforeHash = rawHref.slice(0, hashIndex);\n"
4032
+ + " const currentWithoutHash = String(window.location && window.location.href || '').split('#')[0];\n"
4033
+ + " if (!beforeHash || beforeHash === currentWithoutHash || beforeHash === 'about:srcdoc') return rawHref.slice(hashIndex + 1);\n"
4034
+ + " return null;\n"
4035
+ + " }\n"
4036
+ + " function writeFragmentHistory(fragment) {\n"
4037
+ + " try {\n"
4038
+ + " if (history && typeof history.pushState === 'function') {\n"
4039
+ + " history.pushState(null, '', fragment ? '#' + encodeURIComponent(decodeFragment(fragment)) : '#');\n"
4040
+ + " }\n"
4041
+ + " } catch {}\n"
4042
+ + " }\n"
4043
+ + " function handleFragmentAnchorClick(event) {\n"
4044
+ + " if (!event || event.defaultPrevented) return;\n"
4045
+ + " if (typeof event.button === 'number' && event.button !== 0) return;\n"
4046
+ + " if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n"
4047
+ + " const anchor = getAnchorFromClickTarget(event.target);\n"
4048
+ + " const fragment = getSameDocumentFragment(anchor);\n"
4049
+ + " if (fragment == null) return;\n"
4050
+ + " if (!scrollFragmentIntoView(fragment)) return;\n"
4051
+ + " event.preventDefault();\n"
4052
+ + " writeFragmentHistory(fragment);\n"
4053
+ + " }\n"
4054
+ + " const htmlMathPlaceholders = new Map();\n"
4055
+ + " let htmlMathSerial = 0;\n"
4056
+ + " let htmlMathScanScheduled = false;\n"
4057
+ + " function delimiterListIncludes(list, start, end) {\n"
4058
+ + " if (!Array.isArray(list)) return false;\n"
4059
+ + " return list.some((entry) => Array.isArray(entry) && entry[0] === start && entry[1] === end);\n"
4060
+ + " }\n"
4061
+ + " function getHtmlMathDelimiterConfig() {\n"
4062
+ + " const mathJax = window.MathJax && typeof window.MathJax === 'object' ? window.MathJax : null;\n"
4063
+ + " const tex = mathJax && mathJax.tex && typeof mathJax.tex === 'object' ? mathJax.tex : null;\n"
4064
+ + " return {\n"
4065
+ + " inlineDollar: Boolean(tex && delimiterListIncludes(tex.inlineMath, '$', '$')),\n"
4066
+ + " displayDollar: Boolean(tex && delimiterListIncludes(tex.displayMath, '$$', '$$')),\n"
4067
+ + " };\n"
4068
+ + " }\n"
4069
+ + " function isEscapedAt(text, index) {\n"
4070
+ + " let count = 0;\n"
4071
+ + " let pos = index - 1;\n"
4072
+ + " while (pos >= 0 && text.charAt(pos) === '\\\\') { count += 1; pos -= 1; }\n"
4073
+ + " return count % 2 === 1;\n"
4074
+ + " }\n"
4075
+ + " function findUnescapedDelimiter(text, delimiter, fromIndex) {\n"
4076
+ + " let index = Math.max(0, Number(fromIndex) || 0);\n"
4077
+ + " while (index < text.length) {\n"
4078
+ + " index = text.indexOf(delimiter, index);\n"
4079
+ + " if (index < 0) return -1;\n"
4080
+ + " if (!isEscapedAt(text, index)) return index;\n"
4081
+ + " index += Math.max(1, delimiter.length);\n"
4082
+ + " }\n"
4083
+ + " return -1;\n"
4084
+ + " }\n"
4085
+ + " function textMightContainMath(text, config) {\n"
4086
+ + " if (!text) return false;\n"
4087
+ + " if (text.indexOf('\\\\(') !== -1 || text.indexOf('\\\\[') !== -1) return true;\n"
4088
+ + " return Boolean((config.inlineDollar || config.displayDollar) && text.indexOf('$') !== -1);\n"
4089
+ + " }\n"
4090
+ + " function findNextMathSegment(text, startIndex, config) {\n"
4091
+ + " for (let index = startIndex; index < text.length; index += 1) {\n"
4092
+ + " if (text.startsWith('\\\\(', index)) {\n"
4093
+ + " const end = findUnescapedDelimiter(text, '\\\\)', index + 2);\n"
4094
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: false };\n"
4095
+ + " }\n"
4096
+ + " if (text.startsWith('\\\\[', index)) {\n"
4097
+ + " const end = findUnescapedDelimiter(text, '\\\\]', index + 2);\n"
4098
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
4099
+ + " }\n"
4100
+ + " if (config.displayDollar && text.startsWith('$$', index) && !isEscapedAt(text, index)) {\n"
4101
+ + " const end = findUnescapedDelimiter(text, '$$', index + 2);\n"
4102
+ + " if (end > index + 2) return { start: index, end: end + 2, tex: text.slice(index + 2, end).trim(), display: true };\n"
4103
+ + " }\n"
4104
+ + " if (config.inlineDollar && text.charAt(index) === '$' && text.charAt(index + 1) !== '$' && !isEscapedAt(text, index)) {\n"
4105
+ + " const end = findUnescapedDelimiter(text, '$', index + 1);\n"
4106
+ + " if (end > index + 1) return { start: index, end: end + 1, tex: text.slice(index + 1, end).trim(), display: false };\n"
4107
+ + " }\n"
4108
+ + " }\n"
4109
+ + " return null;\n"
4110
+ + " }\n"
4111
+ + " function parseHtmlMathSegments(text, config, maxCount) {\n"
4112
+ + " const segments = [];\n"
4113
+ + " let index = 0;\n"
4114
+ + " const maxSegments = Math.max(1, Number(maxCount) || 1);\n"
4115
+ + " while (index < text.length && segments.length < maxSegments) {\n"
4116
+ + " const segment = findNextMathSegment(text, index, config);\n"
4117
+ + " if (!segment) break;\n"
4118
+ + " if (segment.tex) segments.push(segment);\n"
4119
+ + " index = Math.max(segment.end, segment.start + 1);\n"
4120
+ + " }\n"
4121
+ + " return segments;\n"
4122
+ + " }\n"
4123
+ + " function shouldSkipHtmlMathTextNode(node) {\n"
4124
+ + " let el = node && node.parentElement;\n"
4125
+ + " while (el) {\n"
4126
+ + " const tag = el.tagName ? el.tagName.toLowerCase() : '';\n"
4127
+ + " if (['script', 'style', 'textarea', 'pre', 'code', 'math', 'svg', 'mjx-container'].indexOf(tag) !== -1) return true;\n"
4128
+ + " if (el.classList && (el.classList.contains('pi-studio-html-math') || el.classList.contains('MathJax'))) return true;\n"
4129
+ + " el = el.parentElement;\n"
4130
+ + " }\n"
4131
+ + " return false;\n"
4132
+ + " }\n"
4133
+ + " function replaceTextNodeWithHtmlMathPlaceholders(node, segments) {\n"
4134
+ + " if (!node || !node.parentNode || !segments || segments.length === 0) return [];\n"
4135
+ + " const text = String(node.nodeValue || '');\n"
4136
+ + " const fragment = document.createDocumentFragment();\n"
4137
+ + " const items = [];\n"
4138
+ + " let index = 0;\n"
4139
+ + " segments.forEach((segment) => {\n"
4140
+ + " if (segment.start > index) fragment.appendChild(document.createTextNode(text.slice(index, segment.start)));\n"
4141
+ + " const mathId = PREVIEW_ID + '_math_' + (++htmlMathSerial).toString(36);\n"
4142
+ + " const span = document.createElement('span');\n"
4143
+ + " span.className = 'pi-studio-html-math pi-studio-html-math-' + (segment.display ? 'display' : 'inline');\n"
4144
+ + " span.setAttribute('data-pi-studio-html-math-id', mathId);\n"
4145
+ + " span.setAttribute('aria-busy', 'true');\n"
4146
+ + " span.textContent = text.slice(segment.start, segment.end);\n"
4147
+ + " htmlMathPlaceholders.set(mathId, span);\n"
4148
+ + " items.push({ mathId, tex: segment.tex, display: Boolean(segment.display) });\n"
4149
+ + " fragment.appendChild(span);\n"
4150
+ + " index = segment.end;\n"
4151
+ + " });\n"
4152
+ + " if (index < text.length) fragment.appendChild(document.createTextNode(text.slice(index)));\n"
4153
+ + " node.parentNode.replaceChild(fragment, node);\n"
4154
+ + " return items;\n"
4155
+ + " }\n"
4156
+ + " function applyRenderedHtmlMath(results) {\n"
4157
+ + " if (!Array.isArray(results)) return;\n"
4158
+ + " results.forEach((result) => {\n"
4159
+ + " if (!result || typeof result !== 'object') return;\n"
4160
+ + " const mathId = typeof result.mathId === 'string' ? result.mathId : '';\n"
4161
+ + " const placeholder = mathId ? htmlMathPlaceholders.get(mathId) : null;\n"
4162
+ + " if (!placeholder || !placeholder.isConnected) return;\n"
4163
+ + " placeholder.removeAttribute('aria-busy');\n"
4164
+ + " if (result.ok === true && typeof result.html === 'string' && result.html.trim()) {\n"
4165
+ + " placeholder.innerHTML = result.html;\n"
4166
+ + " placeholder.classList.add('pi-studio-html-math-rendered');\n"
4167
+ + " } else {\n"
4168
+ + " placeholder.classList.add('pi-studio-html-math-failed');\n"
4169
+ + " if (typeof result.error === 'string' && result.error) placeholder.title = result.error;\n"
4170
+ + " }\n"
4171
+ + " htmlMathPlaceholders.delete(mathId);\n"
4172
+ + " });\n"
4173
+ + " scheduleHeight();\n"
4174
+ + " }\n"
4175
+ + " function runHtmlMathRenderScan() {\n"
4176
+ + " htmlMathScanScheduled = false;\n"
4177
+ + " if (!document.body || typeof document.createTreeWalker !== 'function') return;\n"
4178
+ + " const nodeFilterApi = typeof NodeFilter !== 'undefined' ? NodeFilter : { SHOW_TEXT: 4, FILTER_ACCEPT: 1, FILTER_REJECT: 2 };\n"
4179
+ + " const config = getHtmlMathDelimiterConfig();\n"
4180
+ + " const nodes = [];\n"
4181
+ + " const walker = document.createTreeWalker(document.body, nodeFilterApi.SHOW_TEXT, {\n"
4182
+ + " acceptNode(node) {\n"
4183
+ + " const text = String(node && node.nodeValue || '');\n"
4184
+ + " if (!textMightContainMath(text, config)) return nodeFilterApi.FILTER_REJECT;\n"
4185
+ + " if (shouldSkipHtmlMathTextNode(node)) return nodeFilterApi.FILTER_REJECT;\n"
4186
+ + " return nodeFilterApi.FILTER_ACCEPT;\n"
4187
+ + " }\n"
4188
+ + " });\n"
4189
+ + " while (walker.nextNode()) nodes.push(walker.currentNode);\n"
4190
+ + " const items = [];\n"
4191
+ + " for (const node of nodes) {\n"
4192
+ + " const remaining = 250 - items.length;\n"
4193
+ + " if (remaining <= 0) break;\n"
4194
+ + " const text = String(node && node.nodeValue || '');\n"
4195
+ + " const segments = parseHtmlMathSegments(text, config, remaining);\n"
4196
+ + " if (segments.length === 0) continue;\n"
4197
+ + " items.push(...replaceTextNodeWithHtmlMathPlaceholders(node, segments));\n"
4198
+ + " }\n"
4199
+ + " if (items.length > 0) {\n"
4200
+ + " try { parent.postMessage({ type: 'pi-studio-html-artifact-render-math', id: PREVIEW_ID, items }, '*'); } catch {}\n"
4201
+ + " }\n"
4202
+ + " }\n"
4203
+ + " function scheduleHtmlMathRenderScan() {\n"
4204
+ + " if (htmlMathScanScheduled) return;\n"
4205
+ + " htmlMathScanScheduled = true;\n"
4206
+ + " requestAnimationFrame(runHtmlMathRenderScan);\n"
4207
+ + " }\n"
3813
4208
  + " window.addEventListener('message', (event) => {\n"
3814
4209
  + " const data = event && event.data;\n"
3815
- + " if (!data || typeof data !== 'object') return;\n"
3816
- + " if (data.type !== 'pi-studio-html-artifact-zoom' || data.id !== PREVIEW_ID) return;\n"
3817
- + " applyZoom(data.zoom);\n"
4210
+ + " if (!data || typeof data !== 'object' || data.id !== PREVIEW_ID) return;\n"
4211
+ + " if (data.type === 'pi-studio-html-artifact-zoom') {\n"
4212
+ + " applyZoom(data.zoom);\n"
4213
+ + " return;\n"
4214
+ + " }\n"
4215
+ + " if (data.type === 'pi-studio-html-artifact-math-rendered') {\n"
4216
+ + " applyRenderedHtmlMath(data.results);\n"
4217
+ + " }\n"
3818
4218
  + " });\n"
3819
- + " window.addEventListener('load', scheduleHeight);\n"
4219
+ + " document.addEventListener('click', handleFragmentAnchorClick);\n"
4220
+ + " document.addEventListener('DOMContentLoaded', scheduleHtmlMathRenderScan);\n"
4221
+ + " window.addEventListener('hashchange', () => {\n"
4222
+ + " const hash = String(window.location && window.location.hash || '');\n"
4223
+ + " if (hash) scrollFragmentIntoView(hash.slice(1), { smooth: false });\n"
4224
+ + " });\n"
4225
+ + " window.addEventListener('load', () => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
3820
4226
  + " window.addEventListener('resize', scheduleHeight);\n"
3821
4227
  + " if (typeof ResizeObserver === 'function') {\n"
3822
4228
  + " const observer = new ResizeObserver(scheduleHeight);\n"
@@ -3824,20 +4230,32 @@
3824
4230
  + " if (document.body) observer.observe(document.body);\n"
3825
4231
  + " }\n"
3826
4232
  + " if (typeof MutationObserver === 'function') {\n"
3827
- + " const observer = new MutationObserver(scheduleHeight);\n"
4233
+ + " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
3828
4234
  + " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
3829
4235
  + " }\n"
3830
4236
  + " scheduleHeight();\n"
3831
4237
  + " setTimeout(scheduleHeight, 80);\n"
3832
4238
  + " setTimeout(scheduleHeight, 350);\n"
4239
+ + " setTimeout(scheduleHtmlMathRenderScan, 0);\n"
4240
+ + " setTimeout(scheduleHtmlMathRenderScan, 120);\n"
4241
+ + " setTimeout(scheduleHtmlMathRenderScan, 500);\n"
3833
4242
  + "})();\n"
3834
4243
  + "<\/script>";
3835
4244
  }
3836
4245
 
4246
+ function buildHtmlArtifactPreviewMathStyle() {
4247
+ return "<style data-pi-studio-html-preview-math>\n"
4248
+ + ".pi-studio-html-math-display{display:block;margin:0.75em 0;overflow-x:auto;text-align:center;}\n"
4249
+ + ".pi-studio-html-math-display>math{display:block;margin:0 auto;}\n"
4250
+ + ".pi-studio-html-math-inline>math{vertical-align:-0.15em;}\n"
4251
+ + "</style>\n";
4252
+ }
4253
+
3837
4254
  function buildHtmlArtifactPreviewHeadMarkup(previewId) {
3838
4255
  return "<meta charset=\"utf-8\">\n"
3839
4256
  + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
3840
4257
  + "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
4258
+ + buildHtmlArtifactPreviewMathStyle()
3841
4259
  + buildHtmlArtifactPreviewResizeScript(previewId);
3842
4260
  }
3843
4261
 
@@ -3897,7 +4315,161 @@
3897
4315
  }
3898
4316
  }
3899
4317
 
4318
+ function handleHtmlArtifactFrameFragmentMessage(event) {
4319
+ const data = event && event.data;
4320
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-fragment") return;
4321
+ const id = typeof data.id === "string" ? data.id : "";
4322
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4323
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4324
+ if (id) htmlArtifactFramesById.delete(id);
4325
+ return;
4326
+ }
4327
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4328
+ if (record.shell && record.shell.classList && record.shell.classList.contains("is-focused")) return;
4329
+
4330
+ const scrollContainer = record.shell && typeof record.shell.closest === "function"
4331
+ ? record.shell.closest(".panel-scroll")
4332
+ : null;
4333
+ const isCapped = Boolean(record.iframe.classList && record.iframe.classList.contains("is-height-capped"));
4334
+ const documentHeight = Number(data.documentHeight);
4335
+ const viewportHeight = Number(data.viewportHeight);
4336
+ const isInternallyScrollable = isCapped
4337
+ || (Number.isFinite(documentHeight) && Number.isFinite(viewportHeight) && documentHeight > viewportHeight + 2);
4338
+ if (!scrollContainer || isInternallyScrollable) {
4339
+ if (typeof record.iframe.scrollIntoView === "function") {
4340
+ try {
4341
+ record.iframe.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
4342
+ } catch {
4343
+ record.iframe.scrollIntoView(false);
4344
+ }
4345
+ }
4346
+ return;
4347
+ }
4348
+
4349
+ const rawTargetTop = Number(data.targetTop);
4350
+ const offsetInFrame = Number.isFinite(rawTargetTop) && rawTargetTop > 0 ? rawTargetTop : 0;
4351
+ const iframeRect = record.iframe.getBoundingClientRect();
4352
+ const containerRect = scrollContainer.getBoundingClientRect();
4353
+ const topPadding = 12;
4354
+ const nextTop = Math.max(
4355
+ 0,
4356
+ scrollContainer.scrollTop + iframeRect.top - containerRect.top + offsetInFrame - topPadding,
4357
+ );
4358
+ try {
4359
+ scrollContainer.scrollTo({ top: nextTop, behavior: "smooth" });
4360
+ } catch {
4361
+ scrollContainer.scrollTop = nextTop;
4362
+ }
4363
+ }
4364
+
4365
+ function normalizeHtmlArtifactMathRenderItems(rawItems) {
4366
+ if (!Array.isArray(rawItems)) return [];
4367
+ return rawItems.slice(0, 250).map((item) => {
4368
+ const raw = item && typeof item === "object" ? item : null;
4369
+ const mathId = raw && typeof raw.mathId === "string" ? raw.mathId : "";
4370
+ const tex = raw && typeof raw.tex === "string" ? raw.tex : "";
4371
+ if (!mathId || !tex.trim()) return null;
4372
+ return {
4373
+ mathId,
4374
+ tex,
4375
+ display: Boolean(raw.display),
4376
+ };
4377
+ }).filter(Boolean);
4378
+ }
4379
+
4380
+ async function fetchRenderedHtmlArtifactMath(items) {
4381
+ const token = getToken();
4382
+ if (!token) {
4383
+ throw new Error("Missing Studio token in URL.");
4384
+ }
4385
+ const response = await fetchWithTimeout("/render-math?token=" + encodeURIComponent(token), {
4386
+ method: "POST",
4387
+ headers: {
4388
+ "Content-Type": "application/json",
4389
+ },
4390
+ body: JSON.stringify({ items }),
4391
+ }, HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS, "HTML preview math render");
4392
+
4393
+ const rawBody = await response.text();
4394
+ let payload = null;
4395
+ try {
4396
+ payload = rawBody ? JSON.parse(rawBody) : null;
4397
+ } catch {
4398
+ payload = null;
4399
+ }
4400
+ if (!response.ok) {
4401
+ const message = payload && typeof payload.error === "string"
4402
+ ? payload.error
4403
+ : "HTML preview math render failed with HTTP " + response.status + ".";
4404
+ throw new Error(message);
4405
+ }
4406
+ if (!payload || payload.ok !== true || !Array.isArray(payload.results)) {
4407
+ throw new Error("HTML preview math renderer returned an invalid payload.");
4408
+ }
4409
+ return payload.results;
4410
+ }
4411
+
4412
+ function postHtmlArtifactMathResults(record, results) {
4413
+ if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
4414
+ try {
4415
+ record.iframe.contentWindow.postMessage({
4416
+ type: "pi-studio-html-artifact-math-rendered",
4417
+ id: record.id || "",
4418
+ results: Array.isArray(results) ? results : [],
4419
+ }, "*");
4420
+ } catch {
4421
+ // Ignore iframe postMessage failures.
4422
+ }
4423
+ }
4424
+
4425
+ async function renderHtmlArtifactMathItems(record, items) {
4426
+ if (!record || !Array.isArray(items) || items.length === 0) return;
4427
+ if (record.detail) record.detail.textContent = "HTML preview · rendering math";
4428
+ try {
4429
+ const results = await fetchRenderedHtmlArtifactMath(items);
4430
+ postHtmlArtifactMathResults(record, results);
4431
+ } catch (error) {
4432
+ console.error("HTML preview math render failed:", error);
4433
+ postHtmlArtifactMathResults(record, items.map((item) => ({
4434
+ mathId: item.mathId,
4435
+ ok: false,
4436
+ error: error && error.message ? error.message : String(error || "HTML preview math render failed."),
4437
+ })));
4438
+ } finally {
4439
+ if (record.detail) record.detail.textContent = "HTML preview";
4440
+ }
4441
+ }
4442
+
4443
+ function handleHtmlArtifactFrameMathRenderMessage(event) {
4444
+ const data = event && event.data;
4445
+ if (!data || typeof data !== "object" || data.type !== "pi-studio-html-artifact-render-math") return;
4446
+ const id = typeof data.id === "string" ? data.id : "";
4447
+ const record = id ? htmlArtifactFramesById.get(id) : null;
4448
+ if (!record || !record.iframe || !record.iframe.isConnected) {
4449
+ if (id) htmlArtifactFramesById.delete(id);
4450
+ return;
4451
+ }
4452
+ if (event.source && record.iframe.contentWindow && event.source !== record.iframe.contentWindow) return;
4453
+ const items = normalizeHtmlArtifactMathRenderItems(data.items);
4454
+ if (items.length === 0) return;
4455
+
4456
+ record.mathRenderBatchCount = Math.max(0, Number(record.mathRenderBatchCount) || 0) + 1;
4457
+ record.mathRenderItemCount = Math.max(0, Number(record.mathRenderItemCount) || 0) + items.length;
4458
+ if (record.mathRenderBatchCount > 24 || record.mathRenderItemCount > 1000) {
4459
+ postHtmlArtifactMathResults(record, items.map((item) => ({
4460
+ mathId: item.mathId,
4461
+ ok: false,
4462
+ error: "HTML preview math render limit reached.",
4463
+ })));
4464
+ return;
4465
+ }
4466
+
4467
+ void renderHtmlArtifactMathItems(record, items);
4468
+ }
4469
+
3900
4470
  window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
4471
+ window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
4472
+ window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
3901
4473
 
3902
4474
  function isStudioHtmlFocusOpen() {
3903
4475
  return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
@@ -4205,7 +4777,7 @@
4205
4777
  iframe.addEventListener("load", () => { postArtifactZoom(); });
4206
4778
  iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
4207
4779
  shell.appendChild(iframe);
4208
- htmlArtifactFramesById.set(previewId, { iframe, shell, detail, zoomControls });
4780
+ htmlArtifactFramesById.set(previewId, { id: previewId, iframe, shell, detail, zoomControls, mathRenderBatchCount: 0, mathRenderItemCount: 0 });
4209
4781
 
4210
4782
  targetEl.appendChild(shell);
4211
4783
 
@@ -5549,12 +6121,34 @@
5549
6121
  const source = codeEl ? codeEl.textContent : preEl.textContent;
5550
6122
 
5551
6123
  const wrapper = document.createElement("div");
5552
- wrapper.className = "mermaid-container";
6124
+ wrapper.className = "mermaid-container studio-copyable-block";
6125
+ if (wrapper.dataset) {
6126
+ wrapper.dataset.mermaidSource = source || "";
6127
+ wrapper.dataset.studioCopyDecorated = "1";
6128
+ }
5553
6129
 
5554
6130
  const diagramEl = document.createElement("div");
5555
6131
  diagramEl.className = "mermaid";
5556
6132
  diagramEl.textContent = source || "";
5557
6133
 
6134
+ const copyBtn = document.createElement("button");
6135
+ copyBtn.type = "button";
6136
+ copyBtn.className = "studio-copy-block-btn studio-copy-mermaid-source-btn";
6137
+ copyBtn.textContent = "Copy source";
6138
+ copyBtn.title = "Copy this Mermaid source to the clipboard.";
6139
+ copyBtn.setAttribute("aria-label", "Copy Mermaid source to the clipboard");
6140
+ copyBtn.addEventListener("pointerdown", (event) => {
6141
+ event.stopPropagation();
6142
+ });
6143
+ copyBtn.addEventListener("mousedown", (event) => {
6144
+ event.stopPropagation();
6145
+ });
6146
+
6147
+ const toolbarEl = document.createElement("div");
6148
+ toolbarEl.className = "mermaid-source-toolbar";
6149
+ toolbarEl.appendChild(copyBtn);
6150
+
6151
+ wrapper.appendChild(toolbarEl);
5558
6152
  wrapper.appendChild(diagramEl);
5559
6153
  preEl.replaceWith(wrapper);
5560
6154
  });
@@ -6130,6 +6724,9 @@
6130
6724
 
6131
6725
  function getCopyablePreviewBlockText(blockEl) {
6132
6726
  if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
6727
+ if (blockEl.classList && blockEl.classList.contains("mermaid-container") && blockEl.dataset && typeof blockEl.dataset.mermaidSource === "string") {
6728
+ return normalizeCopyableBlockText(blockEl.dataset.mermaidSource);
6729
+ }
6133
6730
  if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
6134
6731
  return normalizeCopyableBlockText(
6135
6732
  Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
@@ -9139,6 +9736,10 @@
9139
9736
  persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
9140
9737
  }
9141
9738
 
9739
+ function isShortcutsOpen() {
9740
+ return Boolean(shortcutsOverlayEl && !shortcutsOverlayEl.hidden);
9741
+ }
9742
+
9142
9743
  function isScratchpadOpen() {
9143
9744
  return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
9144
9745
  }
@@ -9152,7 +9753,7 @@
9152
9753
  }
9153
9754
 
9154
9755
  function syncModalOpenState() {
9155
- document.body.classList.toggle("scratchpad-open", isScratchpadOpen());
9756
+ document.body.classList.toggle("scratchpad-open", isScratchpadOpen() || isShortcutsOpen());
9156
9757
  }
9157
9758
 
9158
9759
  function describeStudioDocument(state) {
@@ -13348,6 +13949,41 @@
13348
13949
  updateScratchpadUi();
13349
13950
  }
13350
13951
 
13952
+ function closeShortcuts(options) {
13953
+ if (!shortcutsOverlayEl || shortcutsOverlayEl.hidden) return;
13954
+ shortcutsOverlayEl.hidden = true;
13955
+ syncModalOpenState();
13956
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
13957
+ ? options.focusTarget
13958
+ : (shortcutsBtn || sourceTextEl);
13959
+ if (focusTarget && typeof focusTarget.focus === "function") {
13960
+ const schedule = typeof window.requestAnimationFrame === "function"
13961
+ ? window.requestAnimationFrame.bind(window)
13962
+ : (cb) => window.setTimeout(cb, 16);
13963
+ schedule(() => focusTarget.focus());
13964
+ }
13965
+ }
13966
+
13967
+ function openShortcuts() {
13968
+ if (!shortcutsOverlayEl) return;
13969
+ if (isScratchpadOpen()) closeScratchpad({ focusTarget: null });
13970
+ if (isReviewNotesOpen()) closeReviewNotes({ focusTarget: null });
13971
+ if (isOutlineOpen()) closeOutline({ focusTarget: null });
13972
+ shortcutsOverlayEl.hidden = false;
13973
+ syncModalOpenState();
13974
+ const schedule = typeof window.requestAnimationFrame === "function"
13975
+ ? window.requestAnimationFrame.bind(window)
13976
+ : (cb) => window.setTimeout(cb, 16);
13977
+ schedule(() => {
13978
+ if (shortcutsCloseBtn && typeof shortcutsCloseBtn.focus === "function") shortcutsCloseBtn.focus();
13979
+ });
13980
+ }
13981
+
13982
+ function toggleShortcuts() {
13983
+ if (isShortcutsOpen()) closeShortcuts({ focusTarget: shortcutsBtn || sourceTextEl });
13984
+ else openShortcuts();
13985
+ }
13986
+
13351
13987
  function closeScratchpad(options) {
13352
13988
  if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
13353
13989
  scratchpadOverlayEl.hidden = true;
@@ -15699,6 +16335,26 @@
15699
16335
  });
15700
16336
  }
15701
16337
 
16338
+ if (shortcutsBtn) {
16339
+ shortcutsBtn.addEventListener("click", () => {
16340
+ toggleShortcuts();
16341
+ });
16342
+ }
16343
+
16344
+ if (shortcutsCloseBtn) {
16345
+ shortcutsCloseBtn.addEventListener("click", () => {
16346
+ closeShortcuts();
16347
+ });
16348
+ }
16349
+
16350
+ if (shortcutsOverlayEl) {
16351
+ shortcutsOverlayEl.addEventListener("click", (event) => {
16352
+ if (event.target === shortcutsOverlayEl) {
16353
+ closeShortcuts();
16354
+ }
16355
+ });
16356
+ }
16357
+
15702
16358
  if (scratchpadBtn) {
15703
16359
  scratchpadBtn.addEventListener("click", () => {
15704
16360
  openScratchpad();