pi-studio 0.9.10 → 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.
- package/CHANGELOG.md +9 -0
- package/client/studio-client.js +434 -7
- package/client/studio.css +33 -0
- package/index.ts +183 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.11] — 2026-05-19
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Render TeX math inside sandboxed interactive HTML previews through Studio's existing Pandoc/MathML pipeline without enabling network scripts.
|
|
11
|
+
- Added a **Copy source** button to rendered Mermaid diagrams.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Made same-document section links scroll correctly inside sandboxed interactive HTML previews.
|
|
15
|
+
|
|
7
16
|
## [0.9.10] — 2026-05-19
|
|
8
17
|
|
|
9
18
|
### Added
|
package/client/studio-client.js
CHANGED
|
@@ -245,6 +245,7 @@
|
|
|
245
245
|
const REPL_JOURNAL_MAX_ENTRIES = 80;
|
|
246
246
|
const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
247
247
|
const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
|
|
248
|
+
const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
|
|
248
249
|
const EDITOR_TAB_TEXT = " ";
|
|
249
250
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
250
251
|
const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
|
|
@@ -3968,6 +3969,11 @@
|
|
|
3968
3969
|
+ " root ? root.offsetHeight : 0\n"
|
|
3969
3970
|
+ " ));\n"
|
|
3970
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"
|
|
3971
3977
|
+ " function sendHeight() {\n"
|
|
3972
3978
|
+ " scheduled = false;\n"
|
|
3973
3979
|
+ " const height = measureHeight();\n"
|
|
@@ -3980,13 +3986,243 @@
|
|
|
3980
3986
|
+ " scheduled = true;\n"
|
|
3981
3987
|
+ " requestAnimationFrame(sendHeight);\n"
|
|
3982
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"
|
|
3983
4208
|
+ " window.addEventListener('message', (event) => {\n"
|
|
3984
4209
|
+ " const data = event && event.data;\n"
|
|
3985
|
-
+ " if (!data || typeof data !== 'object') return;\n"
|
|
3986
|
-
+ " if (data.type
|
|
3987
|
-
+ "
|
|
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"
|
|
3988
4218
|
+ " });\n"
|
|
3989
|
-
+ "
|
|
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"
|
|
3990
4226
|
+ " window.addEventListener('resize', scheduleHeight);\n"
|
|
3991
4227
|
+ " if (typeof ResizeObserver === 'function') {\n"
|
|
3992
4228
|
+ " const observer = new ResizeObserver(scheduleHeight);\n"
|
|
@@ -3994,20 +4230,32 @@
|
|
|
3994
4230
|
+ " if (document.body) observer.observe(document.body);\n"
|
|
3995
4231
|
+ " }\n"
|
|
3996
4232
|
+ " if (typeof MutationObserver === 'function') {\n"
|
|
3997
|
-
+ " const observer = new MutationObserver(scheduleHeight);\n"
|
|
4233
|
+
+ " const observer = new MutationObserver(() => { scheduleHeight(); scheduleHtmlMathRenderScan(); });\n"
|
|
3998
4234
|
+ " observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });\n"
|
|
3999
4235
|
+ " }\n"
|
|
4000
4236
|
+ " scheduleHeight();\n"
|
|
4001
4237
|
+ " setTimeout(scheduleHeight, 80);\n"
|
|
4002
4238
|
+ " setTimeout(scheduleHeight, 350);\n"
|
|
4239
|
+
+ " setTimeout(scheduleHtmlMathRenderScan, 0);\n"
|
|
4240
|
+
+ " setTimeout(scheduleHtmlMathRenderScan, 120);\n"
|
|
4241
|
+
+ " setTimeout(scheduleHtmlMathRenderScan, 500);\n"
|
|
4003
4242
|
+ "})();\n"
|
|
4004
4243
|
+ "<\/script>";
|
|
4005
4244
|
}
|
|
4006
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
|
+
|
|
4007
4254
|
function buildHtmlArtifactPreviewHeadMarkup(previewId) {
|
|
4008
4255
|
return "<meta charset=\"utf-8\">\n"
|
|
4009
4256
|
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n"
|
|
4010
4257
|
+ "<meta http-equiv=\"Content-Security-Policy\" content=\"" + escapeHtml(HTML_ARTIFACT_PREVIEW_CSP) + "\">\n"
|
|
4258
|
+
+ buildHtmlArtifactPreviewMathStyle()
|
|
4011
4259
|
+ buildHtmlArtifactPreviewResizeScript(previewId);
|
|
4012
4260
|
}
|
|
4013
4261
|
|
|
@@ -4067,7 +4315,161 @@
|
|
|
4067
4315
|
}
|
|
4068
4316
|
}
|
|
4069
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
|
+
|
|
4070
4470
|
window.addEventListener("message", handleHtmlArtifactFrameSizeMessage);
|
|
4471
|
+
window.addEventListener("message", handleHtmlArtifactFrameFragmentMessage);
|
|
4472
|
+
window.addEventListener("message", handleHtmlArtifactFrameMathRenderMessage);
|
|
4071
4473
|
|
|
4072
4474
|
function isStudioHtmlFocusOpen() {
|
|
4073
4475
|
return Boolean(studioHtmlFocusOverlayEl && studioHtmlFocusOverlayEl.hidden === false && studioHtmlFocusShellEl);
|
|
@@ -4375,7 +4777,7 @@
|
|
|
4375
4777
|
iframe.addEventListener("load", () => { postArtifactZoom(); });
|
|
4376
4778
|
iframe.srcdoc = buildHtmlArtifactSrcdoc(html, previewId);
|
|
4377
4779
|
shell.appendChild(iframe);
|
|
4378
|
-
htmlArtifactFramesById.set(previewId, { iframe, shell, detail, zoomControls });
|
|
4780
|
+
htmlArtifactFramesById.set(previewId, { id: previewId, iframe, shell, detail, zoomControls, mathRenderBatchCount: 0, mathRenderItemCount: 0 });
|
|
4379
4781
|
|
|
4380
4782
|
targetEl.appendChild(shell);
|
|
4381
4783
|
|
|
@@ -5719,12 +6121,34 @@
|
|
|
5719
6121
|
const source = codeEl ? codeEl.textContent : preEl.textContent;
|
|
5720
6122
|
|
|
5721
6123
|
const wrapper = document.createElement("div");
|
|
5722
|
-
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
|
+
}
|
|
5723
6129
|
|
|
5724
6130
|
const diagramEl = document.createElement("div");
|
|
5725
6131
|
diagramEl.className = "mermaid";
|
|
5726
6132
|
diagramEl.textContent = source || "";
|
|
5727
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);
|
|
5728
6152
|
wrapper.appendChild(diagramEl);
|
|
5729
6153
|
preEl.replaceWith(wrapper);
|
|
5730
6154
|
});
|
|
@@ -6300,6 +6724,9 @@
|
|
|
6300
6724
|
|
|
6301
6725
|
function getCopyablePreviewBlockText(blockEl) {
|
|
6302
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
|
+
}
|
|
6303
6730
|
if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
|
|
6304
6731
|
return normalizeCopyableBlockText(
|
|
6305
6732
|
Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
|
package/client/studio.css
CHANGED
|
@@ -2456,6 +2456,39 @@
|
|
|
2456
2456
|
overflow-x: auto;
|
|
2457
2457
|
}
|
|
2458
2458
|
|
|
2459
|
+
.rendered-markdown .mermaid-source-toolbar {
|
|
2460
|
+
display: flex;
|
|
2461
|
+
justify-content: flex-end;
|
|
2462
|
+
margin: 0 0 4px;
|
|
2463
|
+
pointer-events: none;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn {
|
|
2467
|
+
position: static;
|
|
2468
|
+
top: auto;
|
|
2469
|
+
right: auto;
|
|
2470
|
+
z-index: auto;
|
|
2471
|
+
padding: 2px 7px;
|
|
2472
|
+
border-color: transparent;
|
|
2473
|
+
background: transparent;
|
|
2474
|
+
box-shadow: none;
|
|
2475
|
+
font-size: 11px;
|
|
2476
|
+
opacity: 0.38;
|
|
2477
|
+
pointer-events: auto;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
.rendered-markdown .mermaid-container:hover > .studio-copy-block-btn {
|
|
2481
|
+
opacity: 0.38;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:hover,
|
|
2485
|
+
.rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:focus-visible {
|
|
2486
|
+
opacity: 1;
|
|
2487
|
+
color: var(--text);
|
|
2488
|
+
border-color: var(--control-border);
|
|
2489
|
+
background: var(--panel);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2459
2492
|
.rendered-markdown .mermaid-container svg {
|
|
2460
2493
|
max-width: 100%;
|
|
2461
2494
|
height: auto;
|
package/index.ts
CHANGED
|
@@ -64,6 +64,19 @@ interface StudioPromptDescriptor {
|
|
|
64
64
|
promptTriggerText: string | null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
interface StudioHtmlPreviewMathRenderItem {
|
|
68
|
+
mathId: string;
|
|
69
|
+
tex: string;
|
|
70
|
+
display: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface StudioHtmlPreviewMathRenderResult {
|
|
74
|
+
mathId: string;
|
|
75
|
+
ok: boolean;
|
|
76
|
+
html?: string;
|
|
77
|
+
error?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
interface ActiveStudioRequest extends StudioPromptDescriptor {
|
|
68
81
|
id: string;
|
|
69
82
|
kind: StudioRequestKind;
|
|
@@ -460,6 +473,9 @@ const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
460
473
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
461
474
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
462
475
|
const HTML_EXPORT_MAX_CHARS = 400_000;
|
|
476
|
+
const HTML_PREVIEW_MATH_RENDER_MAX_ITEMS = 250;
|
|
477
|
+
const HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS = 8_000;
|
|
478
|
+
const HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS = 120_000;
|
|
463
479
|
const STUDIO_QUIZ_SOURCE_MAX_CHARS = 80_000;
|
|
464
480
|
const STUDIO_QUIZ_CONTEXT_FILE_MAX_CHARS = 14_000;
|
|
465
481
|
const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
|
|
@@ -4900,6 +4916,72 @@ function stripMathMlAnnotationTags(html: string): string {
|
|
|
4900
4916
|
});
|
|
4901
4917
|
}
|
|
4902
4918
|
|
|
4919
|
+
function normalizeStudioHtmlPreviewMathForPandoc(tex: string): string {
|
|
4920
|
+
return String(tex ?? "")
|
|
4921
|
+
.replace(/\r\n/g, "\n")
|
|
4922
|
+
.replace(/\\rm\s*\{([^{}]+)\}/g, "\\mathrm{$1}")
|
|
4923
|
+
.replace(/\\rm\s+([A-Za-z]+)(?=[^A-Za-z]|$)/g, "\\mathrm{$1}");
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
function getStudioHtmlPreviewMathWrapperId(index: number): string {
|
|
4927
|
+
return `studio-html-preview-math-${Math.max(0, Math.floor(index))}`;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
function buildStudioHtmlPreviewMathPandocSource(items: StudioHtmlPreviewMathRenderItem[]): string {
|
|
4931
|
+
return items.map((item, index) => {
|
|
4932
|
+
const wrapperId = getStudioHtmlPreviewMathWrapperId(index);
|
|
4933
|
+
const tex = normalizeStudioHtmlPreviewMathForPandoc(item.tex);
|
|
4934
|
+
const mathSource = item.display ? `\\[\n${tex}\n\\]` : `\\(${tex}\\)`;
|
|
4935
|
+
return `:::: {#${wrapperId} .studio-html-preview-math-render-item}\n${mathSource}\n::::`;
|
|
4936
|
+
}).join("\n\n");
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4939
|
+
function extractStudioHtmlPreviewMathHtml(renderedHtml: string, wrapperId: string): string {
|
|
4940
|
+
const idPattern = escapeStudioRegExpLiteral(wrapperId);
|
|
4941
|
+
const wrapperPattern = new RegExp(`<div\\b(?=[^>]*\\bid="${idPattern}")[^>]*>([\\s\\S]*?)<\\/div>`, "i");
|
|
4942
|
+
const wrapperMatch = String(renderedHtml ?? "").match(wrapperPattern);
|
|
4943
|
+
const wrapperHtml = wrapperMatch ? String(wrapperMatch[1] ?? "") : "";
|
|
4944
|
+
const mathMatch = wrapperHtml.match(/<math\b[\s\S]*?<\/math>/i);
|
|
4945
|
+
return mathMatch ? stripMathMlAnnotationTags(mathMatch[0]) : "";
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4948
|
+
async function renderStudioHtmlPreviewMathWithPandoc(items: StudioHtmlPreviewMathRenderItem[]): Promise<StudioHtmlPreviewMathRenderResult[]> {
|
|
4949
|
+
if (items.length === 0) return [];
|
|
4950
|
+
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
4951
|
+
const inputFormat = "markdown+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash";
|
|
4952
|
+
const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
|
|
4953
|
+
const source = buildStudioHtmlPreviewMathPandocSource(items);
|
|
4954
|
+
const pandocResult = await runStudioSubprocess(pandocCommand, args, {
|
|
4955
|
+
input: source,
|
|
4956
|
+
timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
|
|
4957
|
+
stdoutMaxBytes: Math.min(STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES, 10_000_000),
|
|
4958
|
+
label: "pandoc HTML preview math render",
|
|
4959
|
+
notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
|
|
4960
|
+
});
|
|
4961
|
+
if (pandocResult.code !== 0) {
|
|
4962
|
+
throw new Error(`pandoc math render failed with exit code ${pandocResult.code}${pandocResult.stderr ? `: ${pandocResult.stderr}` : ""}`);
|
|
4963
|
+
}
|
|
4964
|
+
if (pandocResult.stdoutTruncated) {
|
|
4965
|
+
throw new Error("pandoc math render output exceeded Studio's size limit.");
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
return items.map((item, index) => {
|
|
4969
|
+
const html = extractStudioHtmlPreviewMathHtml(pandocResult.stdout, getStudioHtmlPreviewMathWrapperId(index));
|
|
4970
|
+
if (!html) {
|
|
4971
|
+
return {
|
|
4972
|
+
mathId: item.mathId,
|
|
4973
|
+
ok: false,
|
|
4974
|
+
error: "Pandoc did not render this expression as MathML.",
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
return {
|
|
4978
|
+
mathId: item.mathId,
|
|
4979
|
+
ok: true,
|
|
4980
|
+
html,
|
|
4981
|
+
};
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4903
4985
|
function normalizeObsidianImages(markdown: string): string {
|
|
4904
4986
|
// Use angle-bracket destinations so paths with spaces/special chars are safe for Pandoc
|
|
4905
4987
|
return markdown
|
|
@@ -11479,6 +11561,84 @@ export default function (pi: ExtensionAPI) {
|
|
|
11479
11561
|
}
|
|
11480
11562
|
};
|
|
11481
11563
|
|
|
11564
|
+
const handleRenderMathRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
11565
|
+
let rawBody = "";
|
|
11566
|
+
try {
|
|
11567
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
11568
|
+
} catch (error) {
|
|
11569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11570
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
11571
|
+
respondJson(res, status, { ok: false, error: message });
|
|
11572
|
+
return;
|
|
11573
|
+
}
|
|
11574
|
+
|
|
11575
|
+
let parsedBody: unknown;
|
|
11576
|
+
try {
|
|
11577
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
11578
|
+
} catch {
|
|
11579
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
11580
|
+
return;
|
|
11581
|
+
}
|
|
11582
|
+
|
|
11583
|
+
const rawItems =
|
|
11584
|
+
parsedBody && typeof parsedBody === "object" && Array.isArray((parsedBody as { items?: unknown }).items)
|
|
11585
|
+
? (parsedBody as { items: unknown[] }).items
|
|
11586
|
+
: null;
|
|
11587
|
+
if (!rawItems) {
|
|
11588
|
+
respondJson(res, 400, { ok: false, error: "Missing math items array in request body." });
|
|
11589
|
+
return;
|
|
11590
|
+
}
|
|
11591
|
+
if (rawItems.length > HTML_PREVIEW_MATH_RENDER_MAX_ITEMS) {
|
|
11592
|
+
respondJson(res, 413, {
|
|
11593
|
+
ok: false,
|
|
11594
|
+
error: `HTML preview math render accepts at most ${HTML_PREVIEW_MATH_RENDER_MAX_ITEMS} items per request.`,
|
|
11595
|
+
});
|
|
11596
|
+
return;
|
|
11597
|
+
}
|
|
11598
|
+
|
|
11599
|
+
const items: StudioHtmlPreviewMathRenderItem[] = [];
|
|
11600
|
+
let totalChars = 0;
|
|
11601
|
+
for (const rawItem of rawItems) {
|
|
11602
|
+
const item = rawItem && typeof rawItem === "object" ? rawItem as { mathId?: unknown; tex?: unknown; display?: unknown } : null;
|
|
11603
|
+
const mathId = typeof item?.mathId === "string" ? item.mathId.trim() : "";
|
|
11604
|
+
const tex = typeof item?.tex === "string" ? item.tex : "";
|
|
11605
|
+
if (!mathId || !tex.trim()) continue;
|
|
11606
|
+
if (mathId.length > 160) {
|
|
11607
|
+
respondJson(res, 400, { ok: false, error: "Math item id is too long." });
|
|
11608
|
+
return;
|
|
11609
|
+
}
|
|
11610
|
+
if (tex.length > HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS) {
|
|
11611
|
+
respondJson(res, 413, {
|
|
11612
|
+
ok: false,
|
|
11613
|
+
error: `A math expression exceeds ${HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS} characters.`,
|
|
11614
|
+
});
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
totalChars += tex.length;
|
|
11618
|
+
if (totalChars > HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS) {
|
|
11619
|
+
respondJson(res, 413, {
|
|
11620
|
+
ok: false,
|
|
11621
|
+
error: `Math render text exceeds ${HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS} characters.`,
|
|
11622
|
+
});
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11625
|
+
items.push({ mathId, tex, display: Boolean(item?.display) });
|
|
11626
|
+
}
|
|
11627
|
+
|
|
11628
|
+
if (items.length === 0) {
|
|
11629
|
+
respondJson(res, 400, { ok: false, error: "No valid math items to render." });
|
|
11630
|
+
return;
|
|
11631
|
+
}
|
|
11632
|
+
|
|
11633
|
+
try {
|
|
11634
|
+
const results = await renderStudioHtmlPreviewMathWithPandoc(items);
|
|
11635
|
+
respondJson(res, 200, { ok: true, renderer: "pandoc", results });
|
|
11636
|
+
} catch (error) {
|
|
11637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11638
|
+
respondJson(res, 500, { ok: false, error: `Math render failed: ${message}` });
|
|
11639
|
+
}
|
|
11640
|
+
};
|
|
11641
|
+
|
|
11482
11642
|
const handleExportPdfRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
11483
11643
|
let rawBody = "";
|
|
11484
11644
|
try {
|
|
@@ -11844,6 +12004,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
11844
12004
|
return;
|
|
11845
12005
|
}
|
|
11846
12006
|
|
|
12007
|
+
if (requestUrl.pathname === "/render-math") {
|
|
12008
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
12009
|
+
if (token !== serverState.token) {
|
|
12010
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
12011
|
+
return;
|
|
12012
|
+
}
|
|
12013
|
+
|
|
12014
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
12015
|
+
if (method !== "POST") {
|
|
12016
|
+
res.setHeader("Allow", "POST");
|
|
12017
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
|
|
12018
|
+
return;
|
|
12019
|
+
}
|
|
12020
|
+
|
|
12021
|
+
void handleRenderMathRequest(req, res).catch((error) => {
|
|
12022
|
+
respondJson(res, 500, {
|
|
12023
|
+
ok: false,
|
|
12024
|
+
error: `Math render failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
12025
|
+
});
|
|
12026
|
+
});
|
|
12027
|
+
return;
|
|
12028
|
+
}
|
|
12029
|
+
|
|
11847
12030
|
if (requestUrl.pathname === "/export-pdf") {
|
|
11848
12031
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
11849
12032
|
if (token !== serverState.token) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|