@lobehub/ui 5.10.3 → 5.10.4
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/es/Highlighter/style.mjs +1 -1
- package/es/HtmlPreview/HtmlPreview.d.mts +8 -0
- package/es/HtmlPreview/HtmlPreview.mjs +329 -0
- package/es/HtmlPreview/HtmlPreview.mjs.map +1 -0
- package/es/HtmlPreview/Iframe.d.mts +8 -0
- package/es/HtmlPreview/Iframe.mjs +159 -0
- package/es/HtmlPreview/Iframe.mjs.map +1 -0
- package/es/HtmlPreview/buildShellSrcDoc.mjs +235 -0
- package/es/HtmlPreview/buildShellSrcDoc.mjs.map +1 -0
- package/es/HtmlPreview/buildStaticSrcDoc.mjs +44 -0
- package/es/HtmlPreview/buildStaticSrcDoc.mjs.map +1 -0
- package/es/HtmlPreview/const.d.mts +42 -0
- package/es/HtmlPreview/const.mjs +63 -0
- package/es/HtmlPreview/const.mjs.map +1 -0
- package/es/HtmlPreview/index.d.mts +6 -0
- package/es/HtmlPreview/index.d.ts +1 -0
- package/es/HtmlPreview/index.js +1 -0
- package/es/HtmlPreview/index.mjs +5 -0
- package/es/HtmlPreview/injectAutoHeightScript.d.mts +5 -0
- package/es/HtmlPreview/injectAutoHeightScript.mjs +37 -0
- package/es/HtmlPreview/injectAutoHeightScript.mjs.map +1 -0
- package/es/HtmlPreview/injectStorageShim.mjs +62 -0
- package/es/HtmlPreview/injectStorageShim.mjs.map +1 -0
- package/es/HtmlPreview/type.d.mts +114 -0
- package/es/Markdown/Markdown.mjs +7 -3
- package/es/Markdown/Markdown.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +2 -17
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/fenceState.mjs +40 -0
- package/es/Markdown/SyntaxMarkdown/fenceState.mjs.map +1 -0
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +13 -0
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -1
- package/es/Markdown/components/CodeBlock.mjs +10 -2
- package/es/Markdown/components/CodeBlock.mjs.map +1 -1
- package/es/Markdown/type.d.mts +3 -0
- package/es/NeuralNetworkLoading/NeuralNetworkLoading.d.mts +8 -0
- package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs +142 -0
- package/es/NeuralNetworkLoading/NeuralNetworkLoading.mjs.map +1 -0
- package/es/NeuralNetworkLoading/index.d.mts +3 -0
- package/es/NeuralNetworkLoading/index.d.ts +1 -0
- package/es/NeuralNetworkLoading/index.js +1 -0
- package/es/NeuralNetworkLoading/index.mjs +2 -0
- package/es/NeuralNetworkLoading/type.d.mts +12 -0
- package/es/hooks/useMarkdown/useMarkdownComponents.mjs +9 -2
- package/es/hooks/useMarkdown/useMarkdownComponents.mjs.map +1 -1
- package/es/hooks/useStableValue.mjs +30 -0
- package/es/hooks/useStableValue.mjs.map +1 -0
- package/es/index.d.mts +8 -1
- package/es/index.mjs +7 -2
- package/es/mdx/mdxComponents/Pre.mjs +14 -1
- package/es/mdx/mdxComponents/Pre.mjs.map +1 -1
- package/es/utils/isDeepEqual.mjs +21 -0
- package/es/utils/isDeepEqual.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { buildAutoHeightScript } from "./injectAutoHeightScript.mjs";
|
|
2
|
+
import { STORAGE_SHIM_SCRIPT } from "./injectStorageShim.mjs";
|
|
3
|
+
//#region src/HtmlPreview/buildShellSrcDoc.ts
|
|
4
|
+
const SHELL_UPDATE_MESSAGE_TYPE = "lobe-html-shell-update";
|
|
5
|
+
/**
|
|
6
|
+
* Build the iframe's one-and-only document.
|
|
7
|
+
*
|
|
8
|
+
* Why a "shell" doc:
|
|
9
|
+
* The iframe is loaded *once* and never reloads during a streaming session.
|
|
10
|
+
* All subsequent body updates arrive via `postMessage` from the parent
|
|
11
|
+
* (see `SHELL_UPDATE_MESSAGE_TYPE`). The script in this document morphs the
|
|
12
|
+
* live DOM in place, so already-painted nodes stay untouched — only nodes
|
|
13
|
+
* that are *new* to this commit get a `.lobe-html-new` class and a CSS
|
|
14
|
+
* fade-in. No iframe reload means no white flash, no script reboots, and
|
|
15
|
+
* no jitter from height resets.
|
|
16
|
+
*/
|
|
17
|
+
const buildShellSrcDoc = ({ background, frameId }) => {
|
|
18
|
+
const baseRules = `html,body{margin:0;padding:0;${background ? `background:${background};` : ""}color-scheme:light dark;}`;
|
|
19
|
+
const fadeRules = `@keyframes lobe-html-fade{from{opacity:0}to{opacity:1}}.lobe-html-new{animation:lobe-html-fade 240ms ease-out both;}`;
|
|
20
|
+
const morphScript = `
|
|
21
|
+
(function () {
|
|
22
|
+
var FRAME_ID = ${JSON.stringify(frameId)};
|
|
23
|
+
var UPDATE_TYPE = ${JSON.stringify(SHELL_UPDATE_MESSAGE_TYPE)};
|
|
24
|
+
|
|
25
|
+
function cloneScript(src) {
|
|
26
|
+
// <script> elements parsed via DOMParser are inert. Rebuild them as
|
|
27
|
+
// proper DOM scripts so the browser executes them.
|
|
28
|
+
//
|
|
29
|
+
// Important: only set .text for inline scripts. Setting it on a
|
|
30
|
+
// src-bearing script (even to an empty string) causes some browser /
|
|
31
|
+
// extension combinations to treat the element as an inline script
|
|
32
|
+
// with empty body and skip the external fetch — so the CDN never
|
|
33
|
+
// loads. We just copy attributes; the browser will fetch the src on
|
|
34
|
+
// append.
|
|
35
|
+
var s = document.createElement('script');
|
|
36
|
+
for (var i = 0; i < src.attributes.length; i++) {
|
|
37
|
+
var a = src.attributes[i];
|
|
38
|
+
s.setAttribute(a.name, a.value);
|
|
39
|
+
}
|
|
40
|
+
if (!src.hasAttribute('src')) {
|
|
41
|
+
var text = src.textContent;
|
|
42
|
+
if (text) s.text = text;
|
|
43
|
+
}
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function importNode(node) {
|
|
48
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {
|
|
49
|
+
return cloneScript(node);
|
|
50
|
+
}
|
|
51
|
+
return document.importNode(node, true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function markFadeIn(node) {
|
|
55
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.classList) {
|
|
56
|
+
node.classList.add('lobe-html-new');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Recursive prefix-match morph. For each parent we match leading children
|
|
61
|
+
// that already exist (by outerHTML or recursive morph), then we remove
|
|
62
|
+
// trailing children that no longer exist, and finally append the new
|
|
63
|
+
// tail with a fade-in class.
|
|
64
|
+
function morph(oldEl, newEl) {
|
|
65
|
+
if (oldEl.nodeType !== newEl.nodeType) return false;
|
|
66
|
+
if (oldEl.nodeType !== Node.ELEMENT_NODE) return false;
|
|
67
|
+
if (oldEl.tagName !== newEl.tagName) return false;
|
|
68
|
+
|
|
69
|
+
// Sync attributes
|
|
70
|
+
var oldAttrs = oldEl.attributes;
|
|
71
|
+
for (var i = oldAttrs.length - 1; i >= 0; i--) {
|
|
72
|
+
var an = oldAttrs[i].name;
|
|
73
|
+
if (!newEl.hasAttribute(an)) oldEl.removeAttribute(an);
|
|
74
|
+
}
|
|
75
|
+
var newAttrs = newEl.attributes;
|
|
76
|
+
for (var j = 0; j < newAttrs.length; j++) {
|
|
77
|
+
var na = newAttrs[j];
|
|
78
|
+
if (oldEl.getAttribute(na.name) !== na.value) {
|
|
79
|
+
oldEl.setAttribute(na.name, na.value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var oldKids = oldEl.childNodes;
|
|
84
|
+
var newKids = newEl.childNodes;
|
|
85
|
+
var commonLen = 0;
|
|
86
|
+
|
|
87
|
+
while (commonLen < oldKids.length && commonLen < newKids.length) {
|
|
88
|
+
var o = oldKids[commonLen];
|
|
89
|
+
var n = newKids[commonLen];
|
|
90
|
+
if (o.nodeType !== n.nodeType) break;
|
|
91
|
+
if (o.nodeType === Node.TEXT_NODE) {
|
|
92
|
+
if (o.textContent !== n.textContent) {
|
|
93
|
+
// Update text content in place — no fade for text.
|
|
94
|
+
o.textContent = n.textContent;
|
|
95
|
+
}
|
|
96
|
+
commonLen++;
|
|
97
|
+
} else if (o.nodeType === Node.ELEMENT_NODE) {
|
|
98
|
+
// Cheap identity check before recursing.
|
|
99
|
+
if (o.outerHTML === n.outerHTML) {
|
|
100
|
+
commonLen++;
|
|
101
|
+
} else if (morph(o, n)) {
|
|
102
|
+
commonLen++;
|
|
103
|
+
} else {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
commonLen++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Trim old trailing children that no longer exist.
|
|
112
|
+
while (oldEl.childNodes.length > commonLen) {
|
|
113
|
+
oldEl.removeChild(oldEl.lastChild);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Append the new tail with fade-in markers, batched through a
|
|
117
|
+
// DocumentFragment. A flat sequence of appendChild calls fires one
|
|
118
|
+
// mutation per element — MutationObserver libraries (Tailwind Play
|
|
119
|
+
// CDN, Stimulus, etc.) that batch their work can drop intermediate
|
|
120
|
+
// notifications if they arrive too quickly. Going through a fragment
|
|
121
|
+
// delivers exactly one childList mutation that lists all new nodes
|
|
122
|
+
// at once, which observers handle reliably.
|
|
123
|
+
if (commonLen < newKids.length) {
|
|
124
|
+
var frag = document.createDocumentFragment();
|
|
125
|
+
for (var k = commonLen; k < newKids.length; k++) {
|
|
126
|
+
var imported = importNode(newKids[k]);
|
|
127
|
+
markFadeIn(imported);
|
|
128
|
+
frag.appendChild(imported);
|
|
129
|
+
}
|
|
130
|
+
oldEl.appendChild(frag);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Track which head extras (scripts/links/meta/title/base) we've already
|
|
137
|
+
// mounted so re-arriving chunks don't re-execute scripts or duplicate
|
|
138
|
+
// resources. Keyed by outerHTML — for streaming partial URLs each
|
|
139
|
+
// partial-and-then-complete tag is a distinct key, which means a partial
|
|
140
|
+
// CDN URL may briefly 404 before the complete one succeeds. That's
|
|
141
|
+
// acceptable; the alternative (waiting for the closing tag heuristic) is
|
|
142
|
+
// fragile and would defeat the live-CDN use case entirely.
|
|
143
|
+
var headSeen = Object.create(null);
|
|
144
|
+
|
|
145
|
+
function syncHeadExtras(headExtrasHtml) {
|
|
146
|
+
if (typeof headExtrasHtml !== 'string') return;
|
|
147
|
+
var parser = new DOMParser();
|
|
148
|
+
var doc = parser.parseFromString(
|
|
149
|
+
'<!doctype html><html><head>' + headExtrasHtml + '</head></html>',
|
|
150
|
+
'text/html',
|
|
151
|
+
);
|
|
152
|
+
var children = doc.head ? doc.head.children : [];
|
|
153
|
+
for (var i = 0; i < children.length; i++) {
|
|
154
|
+
var src = children[i];
|
|
155
|
+
var key = src.outerHTML;
|
|
156
|
+
if (headSeen[key]) continue;
|
|
157
|
+
headSeen[key] = true;
|
|
158
|
+
var clone = importNode(src);
|
|
159
|
+
// Tag for debugging — also keeps these distinguishable from the
|
|
160
|
+
// shell's own head children if anything ever needs to inspect them.
|
|
161
|
+
if (clone.setAttribute) clone.setAttribute('data-lobe-user', '');
|
|
162
|
+
document.head.appendChild(clone);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function applyUpdate(payload) {
|
|
167
|
+
if (!payload) return;
|
|
168
|
+
|
|
169
|
+
// 1) Inline user styles: merged into a single growing <style> element.
|
|
170
|
+
// Streaming partial CSS just keeps overwriting this text until the
|
|
171
|
+
// rules become complete, so we don't stack half-parsed <style>
|
|
172
|
+
// blocks in the head.
|
|
173
|
+
var styleEl = document.getElementById('lobe-user-style');
|
|
174
|
+
if (styleEl && styleEl.textContent !== payload.styleContent) {
|
|
175
|
+
styleEl.textContent = payload.styleContent || '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2) Everything else in the user's <head> (scripts, links, meta, …):
|
|
179
|
+
// append-with-dedupe so head-loaded resources actually run.
|
|
180
|
+
syncHeadExtras(payload.headExtrasHtml);
|
|
181
|
+
|
|
182
|
+
// 3) Body: in-place morph with fade-in on new nodes.
|
|
183
|
+
var bodyParser = new DOMParser();
|
|
184
|
+
var newDoc = bodyParser.parseFromString(
|
|
185
|
+
'<!doctype html><html><body>' + (payload.bodyHtml || '') + '</body></html>',
|
|
186
|
+
'text/html',
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// morph() returns false only for type mismatch on the root — body to
|
|
190
|
+
// body always matches, so this is safe.
|
|
191
|
+
morph(document.body, newDoc.body);
|
|
192
|
+
|
|
193
|
+
// Nudge class-engine CDNs (Tailwind Play CDN, Stimulus, etc.) into
|
|
194
|
+
// re-scanning the document. They watch via MutationObserver but some
|
|
195
|
+
// implementations only consider the directly-mutated nodes from each
|
|
196
|
+
// record and skip recursing into nested descendants, so deeply-styled
|
|
197
|
+
// subtrees can end up with un-generated utility classes. Toggling a
|
|
198
|
+
// throwaway class on body produces an attribute mutation that prompts
|
|
199
|
+
// a fresh full-document scan.
|
|
200
|
+
try {
|
|
201
|
+
document.body.classList.add('_lobe-rescan');
|
|
202
|
+
document.body.classList.remove('_lobe-rescan');
|
|
203
|
+
} catch (_) {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
window.addEventListener('message', function (event) {
|
|
207
|
+
var data = event.data;
|
|
208
|
+
if (!data || data.type !== UPDATE_TYPE || data.frameId !== FRAME_ID) return;
|
|
209
|
+
applyUpdate(data.payload);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Signal the parent that the listener is wired up so it can flush any
|
|
213
|
+
// pending content that was queued before this script ran.
|
|
214
|
+
try {
|
|
215
|
+
parent.postMessage({ type: UPDATE_TYPE + ':ready', frameId: FRAME_ID }, '*');
|
|
216
|
+
} catch (_) {}
|
|
217
|
+
})();
|
|
218
|
+
`;
|
|
219
|
+
return `<!DOCTYPE html>
|
|
220
|
+
<html>
|
|
221
|
+
<head>
|
|
222
|
+
<meta charset="utf-8">
|
|
223
|
+
<style>${baseRules}${fadeRules}</style>
|
|
224
|
+
<style id="lobe-user-style"></style>
|
|
225
|
+
<script>${STORAGE_SHIM_SCRIPT}<\/script>
|
|
226
|
+
<script>${buildAutoHeightScript(frameId)}<\/script>
|
|
227
|
+
<script>${morphScript}<\/script>
|
|
228
|
+
</head>
|
|
229
|
+
<body></body>
|
|
230
|
+
</html>`;
|
|
231
|
+
};
|
|
232
|
+
//#endregion
|
|
233
|
+
export { SHELL_UPDATE_MESSAGE_TYPE, buildShellSrcDoc };
|
|
234
|
+
|
|
235
|
+
//# sourceMappingURL=buildShellSrcDoc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildShellSrcDoc.mjs","names":[],"sources":["../../src/HtmlPreview/buildShellSrcDoc.ts"],"sourcesContent":["import { buildAutoHeightScript } from './injectAutoHeightScript';\nimport { STORAGE_SHIM_SCRIPT } from './injectStorageShim';\n\nexport const SHELL_UPDATE_MESSAGE_TYPE = 'lobe-html-shell-update';\n\ninterface BuildShellSrcDocOptions {\n background?: string;\n frameId: string;\n}\n\n/**\n * Build the iframe's one-and-only document.\n *\n * Why a \"shell\" doc:\n * The iframe is loaded *once* and never reloads during a streaming session.\n * All subsequent body updates arrive via `postMessage` from the parent\n * (see `SHELL_UPDATE_MESSAGE_TYPE`). The script in this document morphs the\n * live DOM in place, so already-painted nodes stay untouched — only nodes\n * that are *new* to this commit get a `.lobe-html-new` class and a CSS\n * fade-in. No iframe reload means no white flash, no script reboots, and\n * no jitter from height resets.\n */\nexport const buildShellSrcDoc = ({ background, frameId }: BuildShellSrcDocOptions): string => {\n const baseRules = `html,body{margin:0;padding:0;${background ? `background:${background};` : ''}color-scheme:light dark;}`;\n const fadeRules = `@keyframes lobe-html-fade{from{opacity:0}to{opacity:1}}.lobe-html-new{animation:lobe-html-fade 240ms ease-out both;}`;\n\n const morphScript = `\n(function () {\n var FRAME_ID = ${JSON.stringify(frameId)};\n var UPDATE_TYPE = ${JSON.stringify(SHELL_UPDATE_MESSAGE_TYPE)};\n\n function cloneScript(src) {\n // <script> elements parsed via DOMParser are inert. Rebuild them as\n // proper DOM scripts so the browser executes them.\n //\n // Important: only set .text for inline scripts. Setting it on a\n // src-bearing script (even to an empty string) causes some browser /\n // extension combinations to treat the element as an inline script\n // with empty body and skip the external fetch — so the CDN never\n // loads. We just copy attributes; the browser will fetch the src on\n // append.\n var s = document.createElement('script');\n for (var i = 0; i < src.attributes.length; i++) {\n var a = src.attributes[i];\n s.setAttribute(a.name, a.value);\n }\n if (!src.hasAttribute('src')) {\n var text = src.textContent;\n if (text) s.text = text;\n }\n return s;\n }\n\n function importNode(node) {\n if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SCRIPT') {\n return cloneScript(node);\n }\n return document.importNode(node, true);\n }\n\n function markFadeIn(node) {\n if (node.nodeType === Node.ELEMENT_NODE && node.classList) {\n node.classList.add('lobe-html-new');\n }\n }\n\n // Recursive prefix-match morph. For each parent we match leading children\n // that already exist (by outerHTML or recursive morph), then we remove\n // trailing children that no longer exist, and finally append the new\n // tail with a fade-in class.\n function morph(oldEl, newEl) {\n if (oldEl.nodeType !== newEl.nodeType) return false;\n if (oldEl.nodeType !== Node.ELEMENT_NODE) return false;\n if (oldEl.tagName !== newEl.tagName) return false;\n\n // Sync attributes\n var oldAttrs = oldEl.attributes;\n for (var i = oldAttrs.length - 1; i >= 0; i--) {\n var an = oldAttrs[i].name;\n if (!newEl.hasAttribute(an)) oldEl.removeAttribute(an);\n }\n var newAttrs = newEl.attributes;\n for (var j = 0; j < newAttrs.length; j++) {\n var na = newAttrs[j];\n if (oldEl.getAttribute(na.name) !== na.value) {\n oldEl.setAttribute(na.name, na.value);\n }\n }\n\n var oldKids = oldEl.childNodes;\n var newKids = newEl.childNodes;\n var commonLen = 0;\n\n while (commonLen < oldKids.length && commonLen < newKids.length) {\n var o = oldKids[commonLen];\n var n = newKids[commonLen];\n if (o.nodeType !== n.nodeType) break;\n if (o.nodeType === Node.TEXT_NODE) {\n if (o.textContent !== n.textContent) {\n // Update text content in place — no fade for text.\n o.textContent = n.textContent;\n }\n commonLen++;\n } else if (o.nodeType === Node.ELEMENT_NODE) {\n // Cheap identity check before recursing.\n if (o.outerHTML === n.outerHTML) {\n commonLen++;\n } else if (morph(o, n)) {\n commonLen++;\n } else {\n break;\n }\n } else {\n commonLen++;\n }\n }\n\n // Trim old trailing children that no longer exist.\n while (oldEl.childNodes.length > commonLen) {\n oldEl.removeChild(oldEl.lastChild);\n }\n\n // Append the new tail with fade-in markers, batched through a\n // DocumentFragment. A flat sequence of appendChild calls fires one\n // mutation per element — MutationObserver libraries (Tailwind Play\n // CDN, Stimulus, etc.) that batch their work can drop intermediate\n // notifications if they arrive too quickly. Going through a fragment\n // delivers exactly one childList mutation that lists all new nodes\n // at once, which observers handle reliably.\n if (commonLen < newKids.length) {\n var frag = document.createDocumentFragment();\n for (var k = commonLen; k < newKids.length; k++) {\n var imported = importNode(newKids[k]);\n markFadeIn(imported);\n frag.appendChild(imported);\n }\n oldEl.appendChild(frag);\n }\n\n return true;\n }\n\n // Track which head extras (scripts/links/meta/title/base) we've already\n // mounted so re-arriving chunks don't re-execute scripts or duplicate\n // resources. Keyed by outerHTML — for streaming partial URLs each\n // partial-and-then-complete tag is a distinct key, which means a partial\n // CDN URL may briefly 404 before the complete one succeeds. That's\n // acceptable; the alternative (waiting for the closing tag heuristic) is\n // fragile and would defeat the live-CDN use case entirely.\n var headSeen = Object.create(null);\n\n function syncHeadExtras(headExtrasHtml) {\n if (typeof headExtrasHtml !== 'string') return;\n var parser = new DOMParser();\n var doc = parser.parseFromString(\n '<!doctype html><html><head>' + headExtrasHtml + '</head></html>',\n 'text/html',\n );\n var children = doc.head ? doc.head.children : [];\n for (var i = 0; i < children.length; i++) {\n var src = children[i];\n var key = src.outerHTML;\n if (headSeen[key]) continue;\n headSeen[key] = true;\n var clone = importNode(src);\n // Tag for debugging — also keeps these distinguishable from the\n // shell's own head children if anything ever needs to inspect them.\n if (clone.setAttribute) clone.setAttribute('data-lobe-user', '');\n document.head.appendChild(clone);\n }\n }\n\n function applyUpdate(payload) {\n if (!payload) return;\n\n // 1) Inline user styles: merged into a single growing <style> element.\n // Streaming partial CSS just keeps overwriting this text until the\n // rules become complete, so we don't stack half-parsed <style>\n // blocks in the head.\n var styleEl = document.getElementById('lobe-user-style');\n if (styleEl && styleEl.textContent !== payload.styleContent) {\n styleEl.textContent = payload.styleContent || '';\n }\n\n // 2) Everything else in the user's <head> (scripts, links, meta, …):\n // append-with-dedupe so head-loaded resources actually run.\n syncHeadExtras(payload.headExtrasHtml);\n\n // 3) Body: in-place morph with fade-in on new nodes.\n var bodyParser = new DOMParser();\n var newDoc = bodyParser.parseFromString(\n '<!doctype html><html><body>' + (payload.bodyHtml || '') + '</body></html>',\n 'text/html',\n );\n\n // morph() returns false only for type mismatch on the root — body to\n // body always matches, so this is safe.\n morph(document.body, newDoc.body);\n\n // Nudge class-engine CDNs (Tailwind Play CDN, Stimulus, etc.) into\n // re-scanning the document. They watch via MutationObserver but some\n // implementations only consider the directly-mutated nodes from each\n // record and skip recursing into nested descendants, so deeply-styled\n // subtrees can end up with un-generated utility classes. Toggling a\n // throwaway class on body produces an attribute mutation that prompts\n // a fresh full-document scan.\n try {\n document.body.classList.add('_lobe-rescan');\n document.body.classList.remove('_lobe-rescan');\n } catch (_) {}\n }\n\n window.addEventListener('message', function (event) {\n var data = event.data;\n if (!data || data.type !== UPDATE_TYPE || data.frameId !== FRAME_ID) return;\n applyUpdate(data.payload);\n });\n\n // Signal the parent that the listener is wired up so it can flush any\n // pending content that was queued before this script ran.\n try {\n parent.postMessage({ type: UPDATE_TYPE + ':ready', frameId: FRAME_ID }, '*');\n } catch (_) {}\n})();\n`;\n\n return `<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>${baseRules}${fadeRules}</style>\n<style id=\"lobe-user-style\"></style>\n<script>${STORAGE_SHIM_SCRIPT}</script>\n<script>${buildAutoHeightScript(frameId)}</script>\n<script>${morphScript}</script>\n</head>\n<body></body>\n</html>`;\n};\n"],"mappings":";;;AAGA,MAAa,4BAA4B;;;;;;;;;;;;;AAmBzC,MAAa,oBAAoB,EAAE,YAAY,cAA+C;CAC5F,MAAM,YAAY,gCAAgC,aAAa,cAAc,WAAW,KAAK,GAAG;CAChG,MAAM,YAAY;CAElB,MAAM,cAAc;;mBAEH,KAAK,UAAU,QAAQ,CAAC;sBACrB,KAAK,UAAU,0BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqM9D,QAAO;;;;SAIA,YAAY,UAAU;;UAErB,oBAAoB;UACpB,sBAAsB,QAAQ,CAAC;UAC/B,YAAY"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { buildAutoHeightScript } from "./injectAutoHeightScript.mjs";
|
|
2
|
+
import { STORAGE_SHIM_SCRIPT } from "./injectStorageShim.mjs";
|
|
3
|
+
//#region src/HtmlPreview/buildStaticSrcDoc.ts
|
|
4
|
+
const shimsBlock = (frameId) => `<script>${STORAGE_SHIM_SCRIPT}<\/script><script>${buildAutoHeightScript(frameId)}<\/script>`;
|
|
5
|
+
const baseStyle = (background) => `<style>html,body{margin:0;padding:0;${background ? `background:${background};` : ""}color-scheme:light dark;}</style>`;
|
|
6
|
+
/**
|
|
7
|
+
* Wrap a user's HTML document with our shims so the iframe can load it as
|
|
8
|
+
* a single self-contained srcDoc. This path is used when the content is
|
|
9
|
+
* *static* (not streaming) — the browser parses it via the normal HTML
|
|
10
|
+
* pipeline, so external `<script src=…>` tags load and execute exactly
|
|
11
|
+
* like they would on a regular page. Tailwind CDN, Chart.js, p5.js — all
|
|
12
|
+
* the things a Play CDN expects to see at parse time — work naturally.
|
|
13
|
+
*
|
|
14
|
+
* For *streaming* content we instead use `buildShellSrcDoc` + postMessage
|
|
15
|
+
* morph, which trades reliable script execution for the ability to fade
|
|
16
|
+
* in new nodes without reloading the iframe.
|
|
17
|
+
*
|
|
18
|
+
* Strategy: inject our shims (storage shim, auto-height) as early in the
|
|
19
|
+
* resulting `<head>` as possible so they run before any user script. If
|
|
20
|
+
* the user supplied a `<head>` open tag we slot the shims in right after
|
|
21
|
+
* it; otherwise we wrap a minimal document around fragments.
|
|
22
|
+
*/
|
|
23
|
+
const buildStaticSrcDoc = ({ background, content, frameId }) => {
|
|
24
|
+
const head = `${baseStyle(background)}${shimsBlock(frameId)}`;
|
|
25
|
+
const lower = content.toLowerCase();
|
|
26
|
+
if (!lower.includes("<html")) return `<!DOCTYPE html><html><head><meta charset="utf-8">${head}</head><body>${content}</body></html>`;
|
|
27
|
+
const headOpenMatch = content.match(/<head\b[^>]*>/i);
|
|
28
|
+
if (headOpenMatch) {
|
|
29
|
+
const idx = headOpenMatch.index + headOpenMatch[0].length;
|
|
30
|
+
return content.slice(0, idx) + head + content.slice(idx);
|
|
31
|
+
}
|
|
32
|
+
const headCloseIdx = lower.indexOf("</head>");
|
|
33
|
+
if (headCloseIdx !== -1) return content.slice(0, headCloseIdx) + head + content.slice(headCloseIdx);
|
|
34
|
+
const htmlOpenMatch = content.match(/<html\b[^>]*>/i);
|
|
35
|
+
if (htmlOpenMatch) {
|
|
36
|
+
const idx = htmlOpenMatch.index + htmlOpenMatch[0].length;
|
|
37
|
+
return content.slice(0, idx) + `<head>${head}</head>` + content.slice(idx);
|
|
38
|
+
}
|
|
39
|
+
return `<!DOCTYPE html><html><head>${head}</head><body>${content}</body></html>`;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { buildStaticSrcDoc };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=buildStaticSrcDoc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildStaticSrcDoc.mjs","names":[],"sources":["../../src/HtmlPreview/buildStaticSrcDoc.ts"],"sourcesContent":["import { buildAutoHeightScript } from './injectAutoHeightScript';\nimport { STORAGE_SHIM_SCRIPT } from './injectStorageShim';\n\ninterface BuildStaticSrcDocOptions {\n background?: string;\n content: string;\n frameId: string;\n}\n\nconst shimsBlock = (frameId: string) =>\n `<script>${STORAGE_SHIM_SCRIPT}</script><script>${buildAutoHeightScript(frameId)}</script>`;\n\nconst baseStyle = (background?: string) =>\n `<style>html,body{margin:0;padding:0;${background ? `background:${background};` : ''}color-scheme:light dark;}</style>`;\n\n/**\n * Wrap a user's HTML document with our shims so the iframe can load it as\n * a single self-contained srcDoc. This path is used when the content is\n * *static* (not streaming) — the browser parses it via the normal HTML\n * pipeline, so external `<script src=…>` tags load and execute exactly\n * like they would on a regular page. Tailwind CDN, Chart.js, p5.js — all\n * the things a Play CDN expects to see at parse time — work naturally.\n *\n * For *streaming* content we instead use `buildShellSrcDoc` + postMessage\n * morph, which trades reliable script execution for the ability to fade\n * in new nodes without reloading the iframe.\n *\n * Strategy: inject our shims (storage shim, auto-height) as early in the\n * resulting `<head>` as possible so they run before any user script. If\n * the user supplied a `<head>` open tag we slot the shims in right after\n * it; otherwise we wrap a minimal document around fragments.\n */\nexport const buildStaticSrcDoc = ({\n background,\n content,\n frameId,\n}: BuildStaticSrcDocOptions): string => {\n const head = `${baseStyle(background)}${shimsBlock(frameId)}`;\n const lower = content.toLowerCase();\n const hasHtmlTag = lower.includes('<html');\n\n if (!hasHtmlTag) {\n return `<!DOCTYPE html><html><head><meta charset=\"utf-8\">${head}</head><body>${content}</body></html>`;\n }\n\n const headOpenMatch = content.match(/<head\\b[^>]*>/i);\n if (headOpenMatch) {\n const idx = headOpenMatch.index! + headOpenMatch[0].length;\n return content.slice(0, idx) + head + content.slice(idx);\n }\n\n const headCloseIdx = lower.indexOf('</head>');\n if (headCloseIdx !== -1) {\n return content.slice(0, headCloseIdx) + head + content.slice(headCloseIdx);\n }\n\n const htmlOpenMatch = content.match(/<html\\b[^>]*>/i);\n if (htmlOpenMatch) {\n const idx = htmlOpenMatch.index! + htmlOpenMatch[0].length;\n return content.slice(0, idx) + `<head>${head}</head>` + content.slice(idx);\n }\n\n return `<!DOCTYPE html><html><head>${head}</head><body>${content}</body></html>`;\n};\n"],"mappings":";;;AASA,MAAM,cAAc,YAClB,WAAW,oBAAoB,oBAAmB,sBAAsB,QAAQ,CAAC;AAEnF,MAAM,aAAa,eACjB,uCAAuC,aAAa,cAAc,WAAW,KAAK,GAAG;;;;;;;;;;;;;;;;;;AAmBvF,MAAa,qBAAqB,EAChC,YACA,SACA,cACsC;CACtC,MAAM,OAAO,GAAG,UAAU,WAAW,GAAG,WAAW,QAAQ;CAC3D,MAAM,QAAQ,QAAQ,aAAa;AAGnC,KAAI,CAFe,MAAM,SAAS,QAEnB,CACb,QAAO,oDAAoD,KAAK,eAAe,QAAQ;CAGzF,MAAM,gBAAgB,QAAQ,MAAM,iBAAiB;AACrD,KAAI,eAAe;EACjB,MAAM,MAAM,cAAc,QAAS,cAAc,GAAG;AACpD,SAAO,QAAQ,MAAM,GAAG,IAAI,GAAG,OAAO,QAAQ,MAAM,IAAI;;CAG1D,MAAM,eAAe,MAAM,QAAQ,UAAU;AAC7C,KAAI,iBAAiB,GACnB,QAAO,QAAQ,MAAM,GAAG,aAAa,GAAG,OAAO,QAAQ,MAAM,aAAa;CAG5E,MAAM,gBAAgB,QAAQ,MAAM,iBAAiB;AACrD,KAAI,eAAe;EACjB,MAAM,MAAM,cAAc,QAAS,cAAc,GAAG;AACpD,SAAO,QAAQ,MAAM,GAAG,IAAI,GAAG,SAAS,KAAK,WAAW,QAAQ,MAAM,IAAI;;AAG5E,QAAO,8BAA8B,KAAK,eAAe,QAAQ"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/HtmlPreview/const.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default sandbox attribute for HTML preview iframes.
|
|
4
|
+
*
|
|
5
|
+
* Why this exact set:
|
|
6
|
+
* - `allow-scripts` — required by the use case (three.js / p5.js / Tailwind
|
|
7
|
+
* CDN style demos). Without it inline preview degrades to source view.
|
|
8
|
+
* - `allow-forms` — lets demos handle `<form>` submissions in-frame.
|
|
9
|
+
* - `allow-modals` — `alert`/`confirm`/`prompt` are common in toy demos.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately omitted:
|
|
12
|
+
* - `allow-same-origin` — would let scripts read parent cookies / localStorage
|
|
13
|
+
* under cloud deployments, and bridge the IPC boundary on desktop builds.
|
|
14
|
+
* - `allow-popups`, `allow-top-navigation` — phishing surface.
|
|
15
|
+
*
|
|
16
|
+
* Override at your own risk via `sandbox` prop.
|
|
17
|
+
*/
|
|
18
|
+
declare const DEFAULT_SANDBOX = "allow-scripts allow-forms allow-modals";
|
|
19
|
+
declare const DEFAULT_HEIGHT = 400;
|
|
20
|
+
/**
|
|
21
|
+
* Is the content a "full" HTML document (has `<html>` or `<!DOCTYPE html>`)?
|
|
22
|
+
* Fragments without these markers render poorly inline and should not auto-mount.
|
|
23
|
+
*/
|
|
24
|
+
declare const isFullHtmlDocument: (content: string) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Heuristic for whether streaming HTML is "stable enough to mount the iframe".
|
|
27
|
+
* Re-mounting srcDoc on every token reboots scripts (p5.js setup runs each
|
|
28
|
+
* time), so we wait for a clear closing signal.
|
|
29
|
+
*/
|
|
30
|
+
declare const isHtmlContentClosed: (content: string) => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Does the content contain a `<script>` tag?
|
|
33
|
+
*
|
|
34
|
+
* Used by `streamingMode: 'auto'` to decide whether live-streaming the
|
|
35
|
+
* iframe is safe. Script-bearing content gets deferred until stable so we
|
|
36
|
+
* don't re-run `setup()` on every token; script-less content (pure
|
|
37
|
+
* markup + styles) streams live for a more responsive feel.
|
|
38
|
+
*/
|
|
39
|
+
declare const containsScript: (content: string) => boolean;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { DEFAULT_HEIGHT, DEFAULT_SANDBOX, containsScript, isFullHtmlDocument, isHtmlContentClosed };
|
|
42
|
+
//# sourceMappingURL=const.d.mts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//#region src/HtmlPreview/const.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default sandbox attribute for HTML preview iframes.
|
|
4
|
+
*
|
|
5
|
+
* Why this exact set:
|
|
6
|
+
* - `allow-scripts` — required by the use case (three.js / p5.js / Tailwind
|
|
7
|
+
* CDN style demos). Without it inline preview degrades to source view.
|
|
8
|
+
* - `allow-forms` — lets demos handle `<form>` submissions in-frame.
|
|
9
|
+
* - `allow-modals` — `alert`/`confirm`/`prompt` are common in toy demos.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately omitted:
|
|
12
|
+
* - `allow-same-origin` — would let scripts read parent cookies / localStorage
|
|
13
|
+
* under cloud deployments, and bridge the IPC boundary on desktop builds.
|
|
14
|
+
* - `allow-popups`, `allow-top-navigation` — phishing surface.
|
|
15
|
+
*
|
|
16
|
+
* Override at your own risk via `sandbox` prop.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_SANDBOX = "allow-scripts allow-forms allow-modals";
|
|
19
|
+
const DEFAULT_HEIGHT = 400;
|
|
20
|
+
/**
|
|
21
|
+
* Cap for srcDoc length. Beyond a few MB browsers start misbehaving;
|
|
22
|
+
* we fall back to source-only above this threshold.
|
|
23
|
+
*/
|
|
24
|
+
const SRCDOC_MAX_LENGTH = 5 * 1024 * 1024;
|
|
25
|
+
const FULL_HTML_MARKERS = [
|
|
26
|
+
"<!doctype html",
|
|
27
|
+
"<html",
|
|
28
|
+
"<HTML"
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Is the content a "full" HTML document (has `<html>` or `<!DOCTYPE html>`)?
|
|
32
|
+
* Fragments without these markers render poorly inline and should not auto-mount.
|
|
33
|
+
*/
|
|
34
|
+
const isFullHtmlDocument = (content) => {
|
|
35
|
+
if (!content) return false;
|
|
36
|
+
const head = content.slice(0, 1024).toLowerCase();
|
|
37
|
+
return FULL_HTML_MARKERS.some((marker) => head.includes(marker.toLowerCase()));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Heuristic for whether streaming HTML is "stable enough to mount the iframe".
|
|
41
|
+
* Re-mounting srcDoc on every token reboots scripts (p5.js setup runs each
|
|
42
|
+
* time), so we wait for a clear closing signal.
|
|
43
|
+
*/
|
|
44
|
+
const isHtmlContentClosed = (content) => {
|
|
45
|
+
if (!content) return false;
|
|
46
|
+
return content.slice(-1024).toLowerCase().includes("</html>");
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Does the content contain a `<script>` tag?
|
|
50
|
+
*
|
|
51
|
+
* Used by `streamingMode: 'auto'` to decide whether live-streaming the
|
|
52
|
+
* iframe is safe. Script-bearing content gets deferred until stable so we
|
|
53
|
+
* don't re-run `setup()` on every token; script-less content (pure
|
|
54
|
+
* markup + styles) streams live for a more responsive feel.
|
|
55
|
+
*/
|
|
56
|
+
const containsScript = (content) => {
|
|
57
|
+
if (!content) return false;
|
|
58
|
+
return /<script\b/i.test(content);
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { DEFAULT_HEIGHT, DEFAULT_SANDBOX, SRCDOC_MAX_LENGTH, containsScript, isFullHtmlDocument, isHtmlContentClosed };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=const.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"const.mjs","names":[],"sources":["../../src/HtmlPreview/const.ts"],"sourcesContent":["/**\n * Default sandbox attribute for HTML preview iframes.\n *\n * Why this exact set:\n * - `allow-scripts` — required by the use case (three.js / p5.js / Tailwind\n * CDN style demos). Without it inline preview degrades to source view.\n * - `allow-forms` — lets demos handle `<form>` submissions in-frame.\n * - `allow-modals` — `alert`/`confirm`/`prompt` are common in toy demos.\n *\n * Deliberately omitted:\n * - `allow-same-origin` — would let scripts read parent cookies / localStorage\n * under cloud deployments, and bridge the IPC boundary on desktop builds.\n * - `allow-popups`, `allow-top-navigation` — phishing surface.\n *\n * Override at your own risk via `sandbox` prop.\n */\nexport const DEFAULT_SANDBOX = 'allow-scripts allow-forms allow-modals';\n\nexport const DEFAULT_HEIGHT = 400;\n\n/**\n * Cap for srcDoc length. Beyond a few MB browsers start misbehaving;\n * we fall back to source-only above this threshold.\n */\nexport const SRCDOC_MAX_LENGTH = 5 * 1024 * 1024;\n\nconst FULL_HTML_MARKERS = ['<!doctype html', '<html', '<HTML'];\n\n/**\n * Is the content a \"full\" HTML document (has `<html>` or `<!DOCTYPE html>`)?\n * Fragments without these markers render poorly inline and should not auto-mount.\n */\nexport const isFullHtmlDocument = (content: string): boolean => {\n if (!content) return false;\n const head = content.slice(0, 1024).toLowerCase();\n return FULL_HTML_MARKERS.some((marker) => head.includes(marker.toLowerCase()));\n};\n\n/**\n * Heuristic for whether streaming HTML is \"stable enough to mount the iframe\".\n * Re-mounting srcDoc on every token reboots scripts (p5.js setup runs each\n * time), so we wait for a clear closing signal.\n */\nexport const isHtmlContentClosed = (content: string): boolean => {\n if (!content) return false;\n const tail = content.slice(-1024).toLowerCase();\n return tail.includes('</html>');\n};\n\n/**\n * Does the content contain a `<script>` tag?\n *\n * Used by `streamingMode: 'auto'` to decide whether live-streaming the\n * iframe is safe. Script-bearing content gets deferred until stable so we\n * don't re-run `setup()` on every token; script-less content (pure\n * markup + styles) streams live for a more responsive feel.\n */\nexport const containsScript = (content: string): boolean => {\n if (!content) return false;\n return /<script\\b/i.test(content);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,kBAAkB;AAE/B,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB,IAAI,OAAO;AAE5C,MAAM,oBAAoB;CAAC;CAAkB;CAAS;CAAQ;;;;;AAM9D,MAAa,sBAAsB,YAA6B;AAC9D,KAAI,CAAC,QAAS,QAAO;CACrB,MAAM,OAAO,QAAQ,MAAM,GAAG,KAAK,CAAC,aAAa;AACjD,QAAO,kBAAkB,MAAM,WAAW,KAAK,SAAS,OAAO,aAAa,CAAC,CAAC;;;;;;;AAQhF,MAAa,uBAAuB,YAA6B;AAC/D,KAAI,CAAC,QAAS,QAAO;AAErB,QADa,QAAQ,MAAM,MAAM,CAAC,aACvB,CAAC,SAAS,UAAU;;;;;;;;;;AAWjC,MAAa,kBAAkB,YAA6B;AAC1D,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO,aAAa,KAAK,QAAQ"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_HEIGHT, DEFAULT_SANDBOX, containsScript, isFullHtmlDocument, isHtmlContentClosed } from "./const.mjs";
|
|
2
|
+
import { HtmlPreviewIframeProps, HtmlPreviewMode, HtmlPreviewProps, HtmlPreviewStreamingMode } from "./type.mjs";
|
|
3
|
+
import { HtmlPreview } from "./HtmlPreview.mjs";
|
|
4
|
+
import { HtmlPreviewIframe } from "./Iframe.mjs";
|
|
5
|
+
import { AUTO_HEIGHT_MESSAGE_TYPE } from "./injectAutoHeightScript.mjs";
|
|
6
|
+
export { DEFAULT_HEIGHT as HTML_PREVIEW_DEFAULT_HEIGHT, DEFAULT_SANDBOX as HTML_PREVIEW_DEFAULT_SANDBOX, AUTO_HEIGHT_MESSAGE_TYPE as HTML_PREVIEW_RESIZE_MESSAGE, HtmlPreviewIframe, HtmlPreviewIframeProps, HtmlPreviewMode, HtmlPreviewProps, HtmlPreviewStreamingMode, HtmlPreview as default, containsScript as htmlPreviewContainsScript, isFullHtmlDocument, isHtmlContentClosed };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.d.mts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index.mjs';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_HEIGHT, DEFAULT_SANDBOX, containsScript, isFullHtmlDocument, isHtmlContentClosed } from "./const.mjs";
|
|
2
|
+
import { AUTO_HEIGHT_MESSAGE_TYPE } from "./injectAutoHeightScript.mjs";
|
|
3
|
+
import HtmlPreviewIframe from "./Iframe.mjs";
|
|
4
|
+
import HtmlPreview from "./HtmlPreview.mjs";
|
|
5
|
+
export { DEFAULT_HEIGHT as HTML_PREVIEW_DEFAULT_HEIGHT, DEFAULT_SANDBOX as HTML_PREVIEW_DEFAULT_SANDBOX, AUTO_HEIGHT_MESSAGE_TYPE as HTML_PREVIEW_RESIZE_MESSAGE, HtmlPreviewIframe, HtmlPreview as default, containsScript as htmlPreviewContainsScript, isFullHtmlDocument, isHtmlContentClosed };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/HtmlPreview/injectAutoHeightScript.ts
|
|
2
|
+
const AUTO_HEIGHT_MESSAGE_TYPE = "lobe-html-resize";
|
|
3
|
+
const buildAutoHeightScript = (frameId) => `
|
|
4
|
+
(function () {
|
|
5
|
+
var frameId = ${JSON.stringify(frameId)};
|
|
6
|
+
function post() {
|
|
7
|
+
try {
|
|
8
|
+
var h = Math.max(
|
|
9
|
+
document.documentElement.scrollHeight,
|
|
10
|
+
document.body ? document.body.scrollHeight : 0,
|
|
11
|
+
);
|
|
12
|
+
parent.postMessage({ type: ${JSON.stringify(AUTO_HEIGHT_MESSAGE_TYPE)}, frameId: frameId, height: h }, '*');
|
|
13
|
+
} catch (_) {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function attach() {
|
|
17
|
+
post();
|
|
18
|
+
try {
|
|
19
|
+
var ro = new ResizeObserver(post);
|
|
20
|
+
if (document.body) ro.observe(document.body);
|
|
21
|
+
if (document.documentElement) ro.observe(document.documentElement);
|
|
22
|
+
} catch (_) {}
|
|
23
|
+
window.addEventListener('load', post);
|
|
24
|
+
window.addEventListener('resize', post);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (document.readyState === 'loading') {
|
|
28
|
+
document.addEventListener('DOMContentLoaded', attach);
|
|
29
|
+
} else {
|
|
30
|
+
attach();
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
`;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { AUTO_HEIGHT_MESSAGE_TYPE, buildAutoHeightScript };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=injectAutoHeightScript.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectAutoHeightScript.mjs","names":[],"sources":["../../src/HtmlPreview/injectAutoHeightScript.ts"],"sourcesContent":["export const AUTO_HEIGHT_MESSAGE_TYPE = 'lobe-html-resize';\n\nexport const buildAutoHeightScript = (frameId: string) => `\n(function () {\n var frameId = ${JSON.stringify(frameId)};\n function post() {\n try {\n var h = Math.max(\n document.documentElement.scrollHeight,\n document.body ? document.body.scrollHeight : 0,\n );\n parent.postMessage({ type: ${JSON.stringify(AUTO_HEIGHT_MESSAGE_TYPE)}, frameId: frameId, height: h }, '*');\n } catch (_) {}\n }\n\n function attach() {\n post();\n try {\n var ro = new ResizeObserver(post);\n if (document.body) ro.observe(document.body);\n if (document.documentElement) ro.observe(document.documentElement);\n } catch (_) {}\n window.addEventListener('load', post);\n window.addEventListener('resize', post);\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', attach);\n } else {\n attach();\n }\n})();\n`;\n"],"mappings":";AAAA,MAAa,2BAA2B;AAExC,MAAa,yBAAyB,YAAoB;;kBAExC,KAAK,UAAU,QAAQ,CAAC;;;;;;;mCAOP,KAAK,UAAU,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/HtmlPreview/injectStorageShim.ts
|
|
2
|
+
/**
|
|
3
|
+
* Why: When iframe sandbox does not include `allow-same-origin`, accessing
|
|
4
|
+
* `window.localStorage` / `window.sessionStorage` throws a SecurityError.
|
|
5
|
+
* Many LLM-generated demos use these APIs as a convenience even when they
|
|
6
|
+
* don't need persistence — letting them throw kills the whole demo.
|
|
7
|
+
*
|
|
8
|
+
* The shim defines per-frame in-memory Storage objects that match the
|
|
9
|
+
* Storage interface, so naive `localStorage.setItem(...)` calls succeed.
|
|
10
|
+
* State does not survive a reload (acceptable — sandbox is throwaway).
|
|
11
|
+
*/
|
|
12
|
+
const STORAGE_SHIM_SCRIPT = `
|
|
13
|
+
(function () {
|
|
14
|
+
function createStorage() {
|
|
15
|
+
var store = Object.create(null);
|
|
16
|
+
return {
|
|
17
|
+
get length() {
|
|
18
|
+
return Object.keys(store).length;
|
|
19
|
+
},
|
|
20
|
+
key: function (i) {
|
|
21
|
+
var keys = Object.keys(store);
|
|
22
|
+
return i >= 0 && i < keys.length ? keys[i] : null;
|
|
23
|
+
},
|
|
24
|
+
getItem: function (k) {
|
|
25
|
+
return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null;
|
|
26
|
+
},
|
|
27
|
+
setItem: function (k, v) {
|
|
28
|
+
store[String(k)] = String(v);
|
|
29
|
+
},
|
|
30
|
+
removeItem: function (k) {
|
|
31
|
+
delete store[k];
|
|
32
|
+
},
|
|
33
|
+
clear: function () {
|
|
34
|
+
store = Object.create(null);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tryShim(name) {
|
|
40
|
+
try {
|
|
41
|
+
// Accessing the property in a sandboxed (no allow-same-origin) frame
|
|
42
|
+
// throws synchronously — that's the signal to install the shim.
|
|
43
|
+
// eslint-disable-next-line no-unused-expressions
|
|
44
|
+
window[name];
|
|
45
|
+
return;
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
try {
|
|
48
|
+
Object.defineProperty(window, name, {
|
|
49
|
+
configurable: true,
|
|
50
|
+
value: createStorage(),
|
|
51
|
+
});
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
tryShim('localStorage');
|
|
56
|
+
tryShim('sessionStorage');
|
|
57
|
+
})();
|
|
58
|
+
`;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { STORAGE_SHIM_SCRIPT };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=injectStorageShim.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"injectStorageShim.mjs","names":[],"sources":["../../src/HtmlPreview/injectStorageShim.ts"],"sourcesContent":["/**\n * Why: When iframe sandbox does not include `allow-same-origin`, accessing\n * `window.localStorage` / `window.sessionStorage` throws a SecurityError.\n * Many LLM-generated demos use these APIs as a convenience even when they\n * don't need persistence — letting them throw kills the whole demo.\n *\n * The shim defines per-frame in-memory Storage objects that match the\n * Storage interface, so naive `localStorage.setItem(...)` calls succeed.\n * State does not survive a reload (acceptable — sandbox is throwaway).\n */\nexport const STORAGE_SHIM_SCRIPT = `\n(function () {\n function createStorage() {\n var store = Object.create(null);\n return {\n get length() {\n return Object.keys(store).length;\n },\n key: function (i) {\n var keys = Object.keys(store);\n return i >= 0 && i < keys.length ? keys[i] : null;\n },\n getItem: function (k) {\n return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null;\n },\n setItem: function (k, v) {\n store[String(k)] = String(v);\n },\n removeItem: function (k) {\n delete store[k];\n },\n clear: function () {\n store = Object.create(null);\n },\n };\n }\n\n function tryShim(name) {\n try {\n // Accessing the property in a sandboxed (no allow-same-origin) frame\n // throws synchronously — that's the signal to install the shim.\n // eslint-disable-next-line no-unused-expressions\n window[name];\n return;\n } catch (_) {}\n try {\n Object.defineProperty(window, name, {\n configurable: true,\n value: createStorage(),\n });\n } catch (_) {}\n }\n\n tryShim('localStorage');\n tryShim('sessionStorage');\n})();\n`;\n"],"mappings":";;;;;;;;;;;AAUA,MAAa,sBAAsB"}
|