@teleporthq/teleport-shared 0.43.61 → 0.43.63
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/__tests__/utils/font-preconnect.ts +62 -0
- package/__tests__/utils/rich-text-embeds-runtime-module.ts +124 -0
- package/__tests__/utils/rich-text-embeds.ts +254 -0
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-revalidate/api-route.d.ts +24 -0
- package/dist/cjs/page-revalidate/api-route.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/api-route.js +30 -0
- package/dist/cjs/page-revalidate/api-route.js.map +1 -0
- package/dist/cjs/page-revalidate/constants.d.ts +22 -0
- package/dist/cjs/page-revalidate/constants.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/constants.js +25 -0
- package/dist/cjs/page-revalidate/constants.js.map +1 -0
- package/dist/cjs/page-revalidate/index.d.ts +3 -0
- package/dist/cjs/page-revalidate/index.d.ts.map +1 -0
- package/dist/cjs/page-revalidate/index.js +19 -0
- package/dist/cjs/page-revalidate/index.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/font-preconnect.d.ts +40 -0
- package/dist/cjs/utils/font-preconnect.d.ts.map +1 -0
- package/dist/cjs/utils/font-preconnect.js +52 -0
- package/dist/cjs/utils/font-preconnect.js.map +1 -0
- package/dist/cjs/utils/product-ratings.d.ts +26 -0
- package/dist/cjs/utils/product-ratings.d.ts.map +1 -0
- package/dist/cjs/utils/product-ratings.js +30 -0
- package/dist/cjs/utils/product-ratings.js.map +1 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.d.ts +9 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.js +139 -0
- package/dist/cjs/utils/rich-text-embeds-codegen.js.map +1 -0
- package/dist/cjs/utils/rich-text-embeds.d.ts +251 -0
- package/dist/cjs/utils/rich-text-embeds.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-embeds.js +852 -0
- package/dist/cjs/utils/rich-text-embeds.js.map +1 -0
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-revalidate/api-route.d.ts +24 -0
- package/dist/esm/page-revalidate/api-route.d.ts.map +1 -0
- package/dist/esm/page-revalidate/api-route.js +26 -0
- package/dist/esm/page-revalidate/api-route.js.map +1 -0
- package/dist/esm/page-revalidate/constants.d.ts +22 -0
- package/dist/esm/page-revalidate/constants.d.ts.map +1 -0
- package/dist/esm/page-revalidate/constants.js +22 -0
- package/dist/esm/page-revalidate/constants.js.map +1 -0
- package/dist/esm/page-revalidate/index.d.ts +3 -0
- package/dist/esm/page-revalidate/index.d.ts.map +1 -0
- package/dist/esm/page-revalidate/index.js +3 -0
- package/dist/esm/page-revalidate/index.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/font-preconnect.d.ts +40 -0
- package/dist/esm/utils/font-preconnect.d.ts.map +1 -0
- package/dist/esm/utils/font-preconnect.js +48 -0
- package/dist/esm/utils/font-preconnect.js.map +1 -0
- package/dist/esm/utils/product-ratings.d.ts +26 -0
- package/dist/esm/utils/product-ratings.d.ts.map +1 -0
- package/dist/esm/utils/product-ratings.js +26 -0
- package/dist/esm/utils/product-ratings.js.map +1 -0
- package/dist/esm/utils/rich-text-embeds-codegen.d.ts +9 -0
- package/dist/esm/utils/rich-text-embeds-codegen.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-embeds-codegen.js +134 -0
- package/dist/esm/utils/rich-text-embeds-codegen.js.map +1 -0
- package/dist/esm/utils/rich-text-embeds.d.ts +251 -0
- package/dist/esm/utils/rich-text-embeds.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-embeds.js +822 -0
- package/dist/esm/utils/rich-text-embeds.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +10 -0
- package/src/page-revalidate/api-route.ts +77 -0
- package/src/page-revalidate/constants.ts +24 -0
- package/src/page-revalidate/index.ts +2 -0
- package/src/utils/font-preconnect.ts +61 -0
- package/src/utils/product-ratings.ts +98 -0
- package/src/utils/rich-text-embeds-codegen.ts +216 -0
- package/src/utils/rich-text-embeds.ts +966 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The "code embed" contract for rich-text CONTENT — the blog post body, and any
|
|
4
|
+
* other column authored through a rich-text editor.
|
|
5
|
+
*
|
|
6
|
+
* A code embed is a block a post author drops into the body to pull in content
|
|
7
|
+
* from another site (a video, a social post, a design frame, a form, or a raw
|
|
8
|
+
* snippet they pasted). It is stored INSIDE the content HTML, so the same shape
|
|
9
|
+
* has to be produced by the studio's Quill editor, produced by the generated
|
|
10
|
+
* admin panel's Quill editor, previewed by the studio canvas and rendered by
|
|
11
|
+
* the exported Next.js app. This module is that single shape.
|
|
12
|
+
*
|
|
13
|
+
* Two rendering modes, decided by one rule (`resolveEmbedMode`):
|
|
14
|
+
*
|
|
15
|
+
* - `inline` — the markup is provably passive (no `<script>`, no `on*`
|
|
16
|
+
* handler, no `javascript:` URL). It is stored as the element's
|
|
17
|
+
* own children, so it renders server-side, needs no JavaScript
|
|
18
|
+
* and is crawlable. Most provider embeds are a bare `<iframe>`
|
|
19
|
+
* and land here.
|
|
20
|
+
* - `sandbox` — anything else. The markup is base64-encoded into
|
|
21
|
+
* `data-tq-embed-code` and the runtime materializes it inside a
|
|
22
|
+
* sandboxed `srcdoc` iframe, so author-supplied JavaScript can
|
|
23
|
+
* never touch the hosting page. The children hold only a
|
|
24
|
+
* `<noscript>` link fallback.
|
|
25
|
+
*
|
|
26
|
+
* The provider registry is deliberately DECLARATIVE — patterns plus a
|
|
27
|
+
* replacement template, never functions — so `rich-text-embeds-codegen` can
|
|
28
|
+
* serialize it verbatim into the module the generated project gets, and the
|
|
29
|
+
* editor and the published site can never drift.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.normalizeEmbedsForStorage = exports.createEmbedToken = exports.parseEmbedHeightMessage = exports.buildEmbedSandboxDocument = exports.EMBED_SANDBOX_DOC_TAIL = exports.EMBED_SANDBOX_DOC_HEAD = exports.EMBED_INITIAL_HEIGHT = exports.EMBED_MAX_HEIGHT = exports.EMBED_MIN_HEIGHT = exports.EMBED_TOKEN_PLACEHOLDER = exports.EMBED_HEIGHT_MESSAGE_TYPE = exports.resolveEmbedSandboxFlags = exports.EMBED_SANDBOX_FLAGS_TRUSTED = exports.EMBED_SANDBOX_FLAGS_STRICT = exports.applyEmbedElementCaption = exports.applyEmbedElementLayout = exports.readEncodedEmbedCode = exports.readEmbedValueFromElement = exports.findEmbedFrame = exports.buildEmbedFrameChildren = exports.buildEmbedElementHtml = exports.EMBED_CAPTION_STYLE = exports.embedFrameStyle = exports.embedBlockStyle = exports.createEmbedValue = exports.decodeEmbedCode = exports.encodeEmbedCode = exports.escapeEmbedText = exports.escapeEmbedAttribute = exports.normalizeEmbedAlign = exports.clampEmbedWidth = exports.resolveEmbedMode = exports.buildEmbedHtmlFromUrl = exports.applyEmbedTemplate = exports.resolveEmbedProvider = exports.embedProviderLabel = exports.isSafeEmbedUrl = exports.EMBED_PROVIDERS = exports.WIDESCREEN_EMBED_RATIO = exports.DEFAULT_EMBED_SANDBOX_LEVEL = exports.DEFAULT_EMBED_ALIGN = exports.DEFAULT_EMBED_WIDTH = exports.GENERIC_EMBED_PROVIDER_ID = exports.CUSTOM_EMBED_PROVIDER_ID = exports.EMBED_ALIGNMENTS = exports.EMBED_ATTR = exports.EMBED_BLOT_NAME = exports.EMBED_CAPTION_CLASS_NAME = exports.EMBED_FRAME_CLASS_NAME = exports.EMBED_CLASS_NAME = void 0;
|
|
33
|
+
exports.EMBED_CLASS_NAME = 'tq-embed';
|
|
34
|
+
exports.EMBED_FRAME_CLASS_NAME = 'tq-embed-frame';
|
|
35
|
+
exports.EMBED_CAPTION_CLASS_NAME = 'tq-embed-caption';
|
|
36
|
+
/** The Quill blot name, shared by the studio editor and the generated one. */
|
|
37
|
+
exports.EMBED_BLOT_NAME = 'tq-embed';
|
|
38
|
+
exports.EMBED_ATTR = {
|
|
39
|
+
MARKER: 'data-tq-embed',
|
|
40
|
+
PROVIDER: 'data-tq-embed-provider',
|
|
41
|
+
URL: 'data-tq-embed-url',
|
|
42
|
+
MODE: 'data-tq-embed-mode',
|
|
43
|
+
CODE: 'data-tq-embed-code',
|
|
44
|
+
RATIO: 'data-tq-embed-ratio',
|
|
45
|
+
WIDTH: 'data-tq-embed-width',
|
|
46
|
+
ALIGN: 'data-tq-embed-align',
|
|
47
|
+
CAPTION: 'data-tq-embed-caption',
|
|
48
|
+
SANDBOX: 'data-tq-embed-sandbox',
|
|
49
|
+
/** Written by a runtime once a sandbox embed has been mounted. */
|
|
50
|
+
ACTIVATED: 'data-tq-embed-activated',
|
|
51
|
+
/**
|
|
52
|
+
* Marks an element an EDITOR added purely to preview the embed. Editors read
|
|
53
|
+
* their value back out of the live DOM, so a preview must be recognizable in
|
|
54
|
+
* order to be stripped again — see `normalizeEmbedsForStorage`.
|
|
55
|
+
*/
|
|
56
|
+
PREVIEW: 'data-tq-embed-preview',
|
|
57
|
+
};
|
|
58
|
+
exports.EMBED_ALIGNMENTS = ['center', 'float-left', 'float-right'];
|
|
59
|
+
exports.CUSTOM_EMBED_PROVIDER_ID = 'custom';
|
|
60
|
+
exports.GENERIC_EMBED_PROVIDER_ID = 'iframe-url';
|
|
61
|
+
exports.DEFAULT_EMBED_WIDTH = 100;
|
|
62
|
+
exports.DEFAULT_EMBED_ALIGN = 'center';
|
|
63
|
+
exports.DEFAULT_EMBED_SANDBOX_LEVEL = 'strict';
|
|
64
|
+
/** 16:9, as a padding-top percentage. */
|
|
65
|
+
exports.WIDESCREEN_EMBED_RATIO = 56.25;
|
|
66
|
+
var FILL_FRAME_STYLE = 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;border-radius:inherit';
|
|
67
|
+
var BLOCK_FRAME_STYLE = 'display:block;width:100%;border:0;border-radius:inherit';
|
|
68
|
+
var iframeTemplate = function (src, options) {
|
|
69
|
+
var _a;
|
|
70
|
+
var style = options.fill
|
|
71
|
+
? FILL_FRAME_STYLE
|
|
72
|
+
: "".concat(BLOCK_FRAME_STYLE, ";height:").concat((_a = options.height) !== null && _a !== void 0 ? _a : 400, "px");
|
|
73
|
+
return ("<iframe src=\"".concat(src, "\" title=\"").concat(options.title, "\" loading=\"lazy\" frameborder=\"0\" ") +
|
|
74
|
+
"referrerpolicy=\"strict-origin-when-cross-origin\" " +
|
|
75
|
+
"allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share\" " +
|
|
76
|
+
"allowfullscreen style=\"".concat(style, "\"></iframe>"));
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Ordered most-specific first: `resolveEmbedProvider` returns the first match,
|
|
80
|
+
* and the generic `iframe-url` fallback is deliberately last.
|
|
81
|
+
*/
|
|
82
|
+
exports.EMBED_PROVIDERS = [
|
|
83
|
+
{
|
|
84
|
+
id: 'youtube',
|
|
85
|
+
label: 'YouTube',
|
|
86
|
+
patterns: [
|
|
87
|
+
/^https?:\/\/(?:www\.|m\.)?youtube\.com\/watch\?(?:[^#]*&)?v=([A-Za-z0-9_-]{6,})/i,
|
|
88
|
+
/^https?:\/\/youtu\.be\/([A-Za-z0-9_-]{6,})/i,
|
|
89
|
+
/^https?:\/\/(?:www\.)?youtube\.com\/shorts\/([A-Za-z0-9_-]{6,})/i,
|
|
90
|
+
/^https?:\/\/(?:www\.)?youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{6,})/i,
|
|
91
|
+
],
|
|
92
|
+
template: iframeTemplate('https://www.youtube.com/embed/$1', {
|
|
93
|
+
title: 'YouTube video',
|
|
94
|
+
fill: true,
|
|
95
|
+
}),
|
|
96
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'vimeo',
|
|
100
|
+
label: 'Vimeo',
|
|
101
|
+
patterns: [
|
|
102
|
+
/^https?:\/\/(?:www\.)?vimeo\.com\/(\d+)/i,
|
|
103
|
+
/^https?:\/\/player\.vimeo\.com\/video\/(\d+)/i,
|
|
104
|
+
],
|
|
105
|
+
template: iframeTemplate('https://player.vimeo.com/video/$1', {
|
|
106
|
+
title: 'Vimeo video',
|
|
107
|
+
fill: true,
|
|
108
|
+
}),
|
|
109
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'loom',
|
|
113
|
+
label: 'Loom',
|
|
114
|
+
patterns: [/^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([A-Za-z0-9]+)/i],
|
|
115
|
+
template: iframeTemplate('https://www.loom.com/embed/$1', {
|
|
116
|
+
title: 'Loom video',
|
|
117
|
+
fill: true,
|
|
118
|
+
}),
|
|
119
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'dailymotion',
|
|
123
|
+
label: 'Dailymotion',
|
|
124
|
+
patterns: [
|
|
125
|
+
/^https?:\/\/(?:www\.)?dailymotion\.com\/video\/([A-Za-z0-9]+)/i,
|
|
126
|
+
/^https?:\/\/dai\.ly\/([A-Za-z0-9]+)/i,
|
|
127
|
+
],
|
|
128
|
+
template: iframeTemplate('https://www.dailymotion.com/embed/video/$1', {
|
|
129
|
+
title: 'Dailymotion video',
|
|
130
|
+
fill: true,
|
|
131
|
+
}),
|
|
132
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'spotify',
|
|
136
|
+
label: 'Spotify',
|
|
137
|
+
patterns: [
|
|
138
|
+
/^https?:\/\/open\.spotify\.com\/(?:embed\/)?(track|album|playlist|episode|show|artist)\/([A-Za-z0-9]+)/i,
|
|
139
|
+
],
|
|
140
|
+
template: iframeTemplate('https://open.spotify.com/embed/$1/$2', {
|
|
141
|
+
title: 'Spotify player',
|
|
142
|
+
fill: false,
|
|
143
|
+
height: 352,
|
|
144
|
+
}),
|
|
145
|
+
ratio: null,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'soundcloud',
|
|
149
|
+
label: 'SoundCloud',
|
|
150
|
+
patterns: [/^https?:\/\/soundcloud\.com\/[^?#]+/i],
|
|
151
|
+
template: iframeTemplate('https://w.soundcloud.com/player/?url=$E&&color=%23ff5500&auto_play=false&show_teaser=false', { title: 'SoundCloud player', fill: false, height: 166 }),
|
|
152
|
+
ratio: null,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'apple-podcasts',
|
|
156
|
+
label: 'Apple Podcasts',
|
|
157
|
+
patterns: [/^https?:\/\/podcasts\.apple\.com\/(.+)$/i],
|
|
158
|
+
template: iframeTemplate('https://embed.podcasts.apple.com/$1', {
|
|
159
|
+
title: 'Apple Podcasts player',
|
|
160
|
+
fill: false,
|
|
161
|
+
height: 175,
|
|
162
|
+
}),
|
|
163
|
+
ratio: null,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'twitter',
|
|
167
|
+
label: 'X (Twitter)',
|
|
168
|
+
patterns: [/^https?:\/\/(?:www\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/i],
|
|
169
|
+
template: '<blockquote class="twitter-tweet"><a href="$&"></a></blockquote>' +
|
|
170
|
+
'<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>',
|
|
171
|
+
ratio: null,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'instagram',
|
|
175
|
+
label: 'Instagram',
|
|
176
|
+
patterns: [/^https?:\/\/(?:www\.)?instagram\.com\/(?:p|reel|tv)\/([A-Za-z0-9_-]+)/i],
|
|
177
|
+
template: '<blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/p/$1/" data-instgrm-version="14"></blockquote>' +
|
|
178
|
+
'<script async src="https://www.instagram.com/embed.js"></script>',
|
|
179
|
+
ratio: null,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'tiktok',
|
|
183
|
+
label: 'TikTok',
|
|
184
|
+
patterns: [/^https?:\/\/(?:www\.)?tiktok\.com\/@[^/]+\/video\/(\d+)/i],
|
|
185
|
+
template: '<blockquote class="tiktok-embed" cite="$&" data-video-id="$1"><section></section></blockquote>' +
|
|
186
|
+
'<script async src="https://www.tiktok.com/embed.js"></script>',
|
|
187
|
+
ratio: null,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'reddit',
|
|
191
|
+
label: 'Reddit',
|
|
192
|
+
patterns: [/^https?:\/\/(?:www\.)?reddit\.com\/r\/[^/]+\/comments\/.+/i],
|
|
193
|
+
template: '<blockquote class="reddit-embed-bq"><a href="$&"></a></blockquote>' +
|
|
194
|
+
'<script async src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>',
|
|
195
|
+
ratio: null,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'pinterest',
|
|
199
|
+
label: 'Pinterest',
|
|
200
|
+
patterns: [/^https?:\/\/(?:[a-z]{2}\.)?pinterest\.[a-z.]+\/pin\/[0-9]+/i],
|
|
201
|
+
template: '<a data-pin-do="embedPin" href="$&"></a>' +
|
|
202
|
+
'<script async defer src="https://assets.pinterest.com/js/pinit.js"></script>',
|
|
203
|
+
ratio: null,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'facebook',
|
|
207
|
+
label: 'Facebook',
|
|
208
|
+
patterns: [/^https?:\/\/(?:www\.|web\.)?facebook\.com\/.+\/(?:posts|videos)\/.+/i],
|
|
209
|
+
template: iframeTemplate('https://www.facebook.com/plugins/post.php?href=$E&&show_text=true', {
|
|
210
|
+
title: 'Facebook post',
|
|
211
|
+
fill: false,
|
|
212
|
+
height: 500,
|
|
213
|
+
}),
|
|
214
|
+
ratio: null,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'github-gist',
|
|
218
|
+
label: 'GitHub Gist',
|
|
219
|
+
patterns: [/^https?:\/\/gist\.github\.com\/([^/]+\/[a-f0-9]+)/i],
|
|
220
|
+
template: '<script src="https://gist.github.com/$1.js"></script>',
|
|
221
|
+
ratio: null,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'codepen',
|
|
225
|
+
label: 'CodePen',
|
|
226
|
+
patterns: [/^https?:\/\/codepen\.io\/([^/]+)\/(?:pen|embed|details|full)\/([A-Za-z0-9]+)/i],
|
|
227
|
+
template: iframeTemplate('https://codepen.io/$1/embed/$2?default-tab=result', {
|
|
228
|
+
title: 'CodePen',
|
|
229
|
+
fill: false,
|
|
230
|
+
height: 400,
|
|
231
|
+
}),
|
|
232
|
+
ratio: null,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'codesandbox',
|
|
236
|
+
label: 'CodeSandbox',
|
|
237
|
+
patterns: [
|
|
238
|
+
/^https?:\/\/codesandbox\.io\/(?:s|embed)\/([A-Za-z0-9_-]+)/i,
|
|
239
|
+
/^https?:\/\/codesandbox\.io\/p\/(?:sandbox|devbox)\/([A-Za-z0-9_-]+)/i,
|
|
240
|
+
],
|
|
241
|
+
template: iframeTemplate('https://codesandbox.io/embed/$1', {
|
|
242
|
+
title: 'CodeSandbox',
|
|
243
|
+
fill: false,
|
|
244
|
+
height: 500,
|
|
245
|
+
}),
|
|
246
|
+
ratio: null,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'jsfiddle',
|
|
250
|
+
label: 'JSFiddle',
|
|
251
|
+
patterns: [/^https?:\/\/(?:www\.)?jsfiddle\.net\/((?:[^/]+\/)?[A-Za-z0-9]+)\/?/i],
|
|
252
|
+
template: iframeTemplate('https://jsfiddle.net/$1/embedded/', {
|
|
253
|
+
title: 'JSFiddle',
|
|
254
|
+
fill: false,
|
|
255
|
+
height: 400,
|
|
256
|
+
}),
|
|
257
|
+
ratio: null,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'figma',
|
|
261
|
+
label: 'Figma',
|
|
262
|
+
patterns: [/^https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto|board|slides)\/.+/i],
|
|
263
|
+
template: iframeTemplate('https://www.figma.com/embed?embed_host=share&url=$E&', {
|
|
264
|
+
title: 'Figma',
|
|
265
|
+
fill: true,
|
|
266
|
+
}),
|
|
267
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'canva',
|
|
271
|
+
label: 'Canva',
|
|
272
|
+
patterns: [
|
|
273
|
+
/^https?:\/\/(?:www\.)?canva\.com\/design\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)\/(?:view|watch)/i,
|
|
274
|
+
],
|
|
275
|
+
template: iframeTemplate('https://www.canva.com/design/$1/$2/view?embed', {
|
|
276
|
+
title: 'Canva design',
|
|
277
|
+
fill: true,
|
|
278
|
+
}),
|
|
279
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'miro',
|
|
283
|
+
label: 'Miro',
|
|
284
|
+
patterns: [/^https?:\/\/miro\.com\/app\/(?:board|live-embed)\/([A-Za-z0-9_=-]+)/i],
|
|
285
|
+
template: iframeTemplate('https://miro.com/app/live-embed/$1/', {
|
|
286
|
+
title: 'Miro board',
|
|
287
|
+
fill: true,
|
|
288
|
+
}),
|
|
289
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 'google-maps',
|
|
293
|
+
label: 'Google Maps',
|
|
294
|
+
patterns: [/^https?:\/\/(?:www\.)?google\.[a-z.]+\/maps\/embed\?(.+)$/i],
|
|
295
|
+
template: iframeTemplate('https://www.google.com/maps/embed?$1', {
|
|
296
|
+
title: 'Google Maps',
|
|
297
|
+
fill: true,
|
|
298
|
+
}),
|
|
299
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'google-maps-place',
|
|
303
|
+
label: 'Google Maps',
|
|
304
|
+
patterns: [/^https?:\/\/(?:(?:www\.)?google\.[a-z.]+\/maps|maps\.app\.goo\.gl)\/.+/i],
|
|
305
|
+
template: iframeTemplate('https://maps.google.com/maps?q=$E&&output=embed', {
|
|
306
|
+
title: 'Google Maps',
|
|
307
|
+
fill: true,
|
|
308
|
+
}),
|
|
309
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 'google-forms',
|
|
313
|
+
label: 'Google Forms',
|
|
314
|
+
patterns: [/^https?:\/\/docs\.google\.com\/forms\/d\/(?:e\/)?([A-Za-z0-9_-]+)/i],
|
|
315
|
+
template: iframeTemplate('https://docs.google.com/forms/d/e/$1/viewform?embedded=true', {
|
|
316
|
+
title: 'Google Form',
|
|
317
|
+
fill: false,
|
|
318
|
+
height: 700,
|
|
319
|
+
}),
|
|
320
|
+
ratio: null,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'google-docs',
|
|
324
|
+
label: 'Google Docs',
|
|
325
|
+
patterns: [
|
|
326
|
+
/^https?:\/\/docs\.google\.com\/(document|spreadsheets|presentation)\/d\/([A-Za-z0-9_-]+)/i,
|
|
327
|
+
],
|
|
328
|
+
template: iframeTemplate('https://docs.google.com/$1/d/$2/preview', {
|
|
329
|
+
title: 'Google Docs',
|
|
330
|
+
fill: false,
|
|
331
|
+
height: 600,
|
|
332
|
+
}),
|
|
333
|
+
ratio: null,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 'airtable',
|
|
337
|
+
label: 'Airtable',
|
|
338
|
+
patterns: [/^https?:\/\/airtable\.com\/(?:embed\/)?(app[A-Za-z0-9/?=&_-]+)/i],
|
|
339
|
+
template: iframeTemplate('https://airtable.com/embed/$1', {
|
|
340
|
+
title: 'Airtable',
|
|
341
|
+
fill: false,
|
|
342
|
+
height: 533,
|
|
343
|
+
}),
|
|
344
|
+
ratio: null,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: 'typeform',
|
|
348
|
+
label: 'Typeform',
|
|
349
|
+
patterns: [/^https?:\/\/(?:[A-Za-z0-9-]+\.)?typeform\.com\/to\/([A-Za-z0-9]+)/i],
|
|
350
|
+
template: iframeTemplate('https://form.typeform.com/to/$1', {
|
|
351
|
+
title: 'Typeform',
|
|
352
|
+
fill: false,
|
|
353
|
+
height: 500,
|
|
354
|
+
}),
|
|
355
|
+
ratio: null,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: 'calendly',
|
|
359
|
+
label: 'Calendly',
|
|
360
|
+
patterns: [/^https?:\/\/calendly\.com\/(.+)$/i],
|
|
361
|
+
template: iframeTemplate('https://calendly.com/$1', {
|
|
362
|
+
title: 'Calendly',
|
|
363
|
+
fill: false,
|
|
364
|
+
height: 700,
|
|
365
|
+
}),
|
|
366
|
+
ratio: null,
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: exports.GENERIC_EMBED_PROVIDER_ID,
|
|
370
|
+
label: 'Web page',
|
|
371
|
+
patterns: [/^https:\/\/[^\s"'<>]+$/i],
|
|
372
|
+
template: iframeTemplate('$&', { title: 'Embedded page', fill: true }),
|
|
373
|
+
ratio: exports.WIDESCREEN_EMBED_RATIO,
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
/** `javascript:`, `data:` and protocol-relative URLs never become an embed. */
|
|
377
|
+
function isSafeEmbedUrl(url) {
|
|
378
|
+
return /^https?:\/\/[^\s"'<>]+$/i.test((url || '').trim());
|
|
379
|
+
}
|
|
380
|
+
exports.isSafeEmbedUrl = isSafeEmbedUrl;
|
|
381
|
+
/** How an embed names itself in the editor: the provider, or "Custom code". */
|
|
382
|
+
function embedProviderLabel(providerId) {
|
|
383
|
+
var provider = exports.EMBED_PROVIDERS.filter(function (entry) { return entry.id === providerId; })[0];
|
|
384
|
+
return provider ? provider.label : 'Custom code';
|
|
385
|
+
}
|
|
386
|
+
exports.embedProviderLabel = embedProviderLabel;
|
|
387
|
+
function resolveEmbedProvider(url) {
|
|
388
|
+
var trimmed = (url || '').trim();
|
|
389
|
+
if (!isSafeEmbedUrl(trimmed)) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
for (var _i = 0, EMBED_PROVIDERS_1 = exports.EMBED_PROVIDERS; _i < EMBED_PROVIDERS_1.length; _i++) {
|
|
393
|
+
var provider = EMBED_PROVIDERS_1[_i];
|
|
394
|
+
for (var _a = 0, _b = provider.patterns; _a < _b.length; _a++) {
|
|
395
|
+
var pattern = _b[_a];
|
|
396
|
+
var match = trimmed.match(pattern);
|
|
397
|
+
if (match) {
|
|
398
|
+
return { provider: provider, match: match };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
exports.resolveEmbedProvider = resolveEmbedProvider;
|
|
405
|
+
/**
|
|
406
|
+
* Substitutes `$1`-`$9` / `$E1`-`$E9` / `$&` / `$E&` in a provider template.
|
|
407
|
+
* Written by hand rather than through `String.replace` so the replacement
|
|
408
|
+
* patterns `String.replace` reserves (`$'`, "$`", `$<name>`) stay literal.
|
|
409
|
+
*/
|
|
410
|
+
function applyEmbedTemplate(template, match, url) {
|
|
411
|
+
return template.replace(/\$(\$|E?(?:[1-9]|&))/g, function (_token, spec) {
|
|
412
|
+
var _a;
|
|
413
|
+
if (spec === '$') {
|
|
414
|
+
return '$';
|
|
415
|
+
}
|
|
416
|
+
var encoded = spec.charAt(0) === 'E';
|
|
417
|
+
var key = encoded ? spec.slice(1) : spec;
|
|
418
|
+
var raw = key === '&' ? url : (_a = match[Number(key)]) !== null && _a !== void 0 ? _a : '';
|
|
419
|
+
return encoded ? encodeURIComponent(raw) : raw;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
exports.applyEmbedTemplate = applyEmbedTemplate;
|
|
423
|
+
/** The embed markup for a pasted URL, or `null` when nothing recognizes it. */
|
|
424
|
+
function buildEmbedHtmlFromUrl(url) {
|
|
425
|
+
var resolved = resolveEmbedProvider(url);
|
|
426
|
+
if (!resolved) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
provider: resolved.provider,
|
|
431
|
+
html: applyEmbedTemplate(resolved.provider.template, resolved.match, url.trim()),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
exports.buildEmbedHtmlFromUrl = buildEmbedHtmlFromUrl;
|
|
435
|
+
/**
|
|
436
|
+
* Markup that can execute anything — a `<script>`, an inline `on*` handler or a
|
|
437
|
+
* `javascript:` URL — is quarantined in a sandboxed iframe. Everything else is
|
|
438
|
+
* passive and can be rendered straight into the page.
|
|
439
|
+
*/
|
|
440
|
+
function resolveEmbedMode(html) {
|
|
441
|
+
var markup = html || '';
|
|
442
|
+
var executes = /<script[\s/>]/i.test(markup) ||
|
|
443
|
+
/\son[a-z]+\s*=/i.test(markup) ||
|
|
444
|
+
/(?:href|src|action|formaction)\s*=\s*["']?\s*javascript:/i.test(markup);
|
|
445
|
+
return executes ? 'sandbox' : 'inline';
|
|
446
|
+
}
|
|
447
|
+
exports.resolveEmbedMode = resolveEmbedMode;
|
|
448
|
+
function clampEmbedWidth(width) {
|
|
449
|
+
if (!isFinite(width)) {
|
|
450
|
+
return exports.DEFAULT_EMBED_WIDTH;
|
|
451
|
+
}
|
|
452
|
+
return Math.max(10, Math.min(100, Math.round(width)));
|
|
453
|
+
}
|
|
454
|
+
exports.clampEmbedWidth = clampEmbedWidth;
|
|
455
|
+
function normalizeEmbedAlign(align) {
|
|
456
|
+
return exports.EMBED_ALIGNMENTS.indexOf(align) !== -1
|
|
457
|
+
? align
|
|
458
|
+
: exports.DEFAULT_EMBED_ALIGN;
|
|
459
|
+
}
|
|
460
|
+
exports.normalizeEmbedAlign = normalizeEmbedAlign;
|
|
461
|
+
function escapeEmbedAttribute(value) {
|
|
462
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
463
|
+
.replace(/&/g, '&')
|
|
464
|
+
.replace(/"/g, '"')
|
|
465
|
+
.replace(/</g, '<')
|
|
466
|
+
.replace(/>/g, '>');
|
|
467
|
+
}
|
|
468
|
+
exports.escapeEmbedAttribute = escapeEmbedAttribute;
|
|
469
|
+
function escapeEmbedText(value) {
|
|
470
|
+
return String(value !== null && value !== void 0 ? value : '')
|
|
471
|
+
.replace(/&/g, '&')
|
|
472
|
+
.replace(/</g, '<')
|
|
473
|
+
.replace(/>/g, '>');
|
|
474
|
+
}
|
|
475
|
+
exports.escapeEmbedText = escapeEmbedText;
|
|
476
|
+
/**
|
|
477
|
+
* UTF-8 safe base64, working in both the browser (`btoa`) and Node (`Buffer`).
|
|
478
|
+
* The intermediate `%xx` round-trip is what makes non-ASCII captions and
|
|
479
|
+
* markup survive — `btoa` alone throws on anything above U+00FF.
|
|
480
|
+
*/
|
|
481
|
+
function encodeEmbedCode(code) {
|
|
482
|
+
var binary = encodeURIComponent(code).replace(/%([0-9A-F]{2})/gi, function (_m, hex) {
|
|
483
|
+
return String.fromCharCode(parseInt(hex, 16));
|
|
484
|
+
});
|
|
485
|
+
if (typeof btoa === 'function') {
|
|
486
|
+
return btoa(binary);
|
|
487
|
+
}
|
|
488
|
+
if (typeof Buffer !== 'undefined') {
|
|
489
|
+
return Buffer.from(binary, 'binary').toString('base64');
|
|
490
|
+
}
|
|
491
|
+
throw new Error('No base64 encoder available');
|
|
492
|
+
}
|
|
493
|
+
exports.encodeEmbedCode = encodeEmbedCode;
|
|
494
|
+
function decodeEmbedCode(encoded) {
|
|
495
|
+
var binary;
|
|
496
|
+
if (typeof atob === 'function') {
|
|
497
|
+
binary = atob(encoded);
|
|
498
|
+
}
|
|
499
|
+
else if (typeof Buffer !== 'undefined') {
|
|
500
|
+
binary = Buffer.from(encoded, 'base64').toString('binary');
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
throw new Error('No base64 decoder available');
|
|
504
|
+
}
|
|
505
|
+
var percent = '';
|
|
506
|
+
for (var index = 0; index < binary.length; index += 1) {
|
|
507
|
+
percent += "%".concat("00".concat(binary.charCodeAt(index).toString(16)).slice(-2));
|
|
508
|
+
}
|
|
509
|
+
return decodeURIComponent(percent);
|
|
510
|
+
}
|
|
511
|
+
exports.decodeEmbedCode = decodeEmbedCode;
|
|
512
|
+
function createEmbedValue(params) {
|
|
513
|
+
var _a, _b, _c;
|
|
514
|
+
var code = params.code || '';
|
|
515
|
+
return {
|
|
516
|
+
provider: params.provider || exports.CUSTOM_EMBED_PROVIDER_ID,
|
|
517
|
+
url: params.url || '',
|
|
518
|
+
code: code,
|
|
519
|
+
mode: resolveEmbedMode(code),
|
|
520
|
+
ratio: (_a = params.ratio) !== null && _a !== void 0 ? _a : null,
|
|
521
|
+
width: clampEmbedWidth((_b = params.width) !== null && _b !== void 0 ? _b : exports.DEFAULT_EMBED_WIDTH),
|
|
522
|
+
align: normalizeEmbedAlign((_c = params.align) !== null && _c !== void 0 ? _c : exports.DEFAULT_EMBED_ALIGN),
|
|
523
|
+
caption: params.caption || '',
|
|
524
|
+
sandboxLevel: params.sandboxLevel === 'trusted' ? 'trusted' : exports.DEFAULT_EMBED_SANDBOX_LEVEL,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
exports.createEmbedValue = createEmbedValue;
|
|
528
|
+
/**
|
|
529
|
+
* The outer block's own layout — width, float and the margins each align needs.
|
|
530
|
+
*
|
|
531
|
+
* `position:relative` is deliberate: an editor overlays a click catcher on the
|
|
532
|
+
* block (an embed's own iframe swallows the click that should select it), and
|
|
533
|
+
* that overlay needs the block as its positioning context.
|
|
534
|
+
*/
|
|
535
|
+
function embedBlockStyle(value) {
|
|
536
|
+
var declarations = [
|
|
537
|
+
'position:relative',
|
|
538
|
+
"width:".concat(clampEmbedWidth(value.width), "%"),
|
|
539
|
+
'max-width:100%',
|
|
540
|
+
'margin:16px 0',
|
|
541
|
+
];
|
|
542
|
+
if (value.align === 'center') {
|
|
543
|
+
declarations.push('margin-left:auto', 'margin-right:auto');
|
|
544
|
+
}
|
|
545
|
+
else if (value.align === 'float-left') {
|
|
546
|
+
declarations.push('float:left', 'margin-right:16px', 'margin-bottom:8px');
|
|
547
|
+
}
|
|
548
|
+
else if (value.align === 'float-right') {
|
|
549
|
+
declarations.push('float:right', 'margin-left:16px', 'margin-bottom:8px');
|
|
550
|
+
}
|
|
551
|
+
return declarations.join(';');
|
|
552
|
+
}
|
|
553
|
+
exports.embedBlockStyle = embedBlockStyle;
|
|
554
|
+
/**
|
|
555
|
+
* The frame is what holds the embed itself. With a `ratio` it becomes the
|
|
556
|
+
* responsive padding-top box the provider templates fill absolutely; without
|
|
557
|
+
* one the template carries its own height.
|
|
558
|
+
*/
|
|
559
|
+
function embedFrameStyle(ratio) {
|
|
560
|
+
if (ratio === null || !isFinite(ratio) || ratio <= 0) {
|
|
561
|
+
return 'position:relative;width:100%';
|
|
562
|
+
}
|
|
563
|
+
return "position:relative;width:100%;padding-top:".concat(ratio, "%");
|
|
564
|
+
}
|
|
565
|
+
exports.embedFrameStyle = embedFrameStyle;
|
|
566
|
+
exports.EMBED_CAPTION_STYLE = 'margin-top:8px;font-size:0.875em;line-height:1.5;opacity:0.75;text-align:center';
|
|
567
|
+
/**
|
|
568
|
+
* The stored markup for one embed. This is the exact string both editors write
|
|
569
|
+
* into the content column and both runtimes read back.
|
|
570
|
+
*/
|
|
571
|
+
function buildEmbedElementHtml(value) {
|
|
572
|
+
var attrs = [
|
|
573
|
+
"class=\"".concat(exports.EMBED_CLASS_NAME, "\""),
|
|
574
|
+
"".concat(exports.EMBED_ATTR.MARKER, "=\"1\""),
|
|
575
|
+
"".concat(exports.EMBED_ATTR.PROVIDER, "=\"").concat(escapeEmbedAttribute(value.provider), "\""),
|
|
576
|
+
"".concat(exports.EMBED_ATTR.MODE, "=\"").concat(value.mode, "\""),
|
|
577
|
+
"".concat(exports.EMBED_ATTR.WIDTH, "=\"").concat(clampEmbedWidth(value.width), "\""),
|
|
578
|
+
"".concat(exports.EMBED_ATTR.ALIGN, "=\"").concat(value.align, "\""),
|
|
579
|
+
"".concat(exports.EMBED_ATTR.SANDBOX, "=\"").concat(value.sandboxLevel, "\""),
|
|
580
|
+
];
|
|
581
|
+
if (value.url) {
|
|
582
|
+
attrs.push("".concat(exports.EMBED_ATTR.URL, "=\"").concat(escapeEmbedAttribute(value.url), "\""));
|
|
583
|
+
}
|
|
584
|
+
if (value.ratio !== null) {
|
|
585
|
+
attrs.push("".concat(exports.EMBED_ATTR.RATIO, "=\"").concat(value.ratio, "\""));
|
|
586
|
+
}
|
|
587
|
+
if (value.caption) {
|
|
588
|
+
attrs.push("".concat(exports.EMBED_ATTR.CAPTION, "=\"").concat(escapeEmbedAttribute(value.caption), "\""));
|
|
589
|
+
}
|
|
590
|
+
if (value.mode === 'sandbox') {
|
|
591
|
+
attrs.push("".concat(exports.EMBED_ATTR.CODE, "=\"").concat(escapeEmbedAttribute(encodeEmbedCode(value.code)), "\""));
|
|
592
|
+
}
|
|
593
|
+
attrs.push('contenteditable="false"');
|
|
594
|
+
attrs.push("style=\"".concat(embedBlockStyle(value), "\""));
|
|
595
|
+
var caption = value.caption
|
|
596
|
+
? "<figcaption class=\"".concat(exports.EMBED_CAPTION_CLASS_NAME, "\" style=\"").concat(exports.EMBED_CAPTION_STYLE, "\">").concat(escapeEmbedText(value.caption), "</figcaption>")
|
|
597
|
+
: '';
|
|
598
|
+
return ("<figure ".concat(attrs.join(' '), ">") +
|
|
599
|
+
"<div class=\"".concat(exports.EMBED_FRAME_CLASS_NAME, "\" style=\"").concat(embedFrameStyle(value.ratio), "\">") +
|
|
600
|
+
"".concat(buildEmbedFrameChildren(value), "</div>").concat(caption, "</figure>"));
|
|
601
|
+
}
|
|
602
|
+
exports.buildEmbedElementHtml = buildEmbedElementHtml;
|
|
603
|
+
/**
|
|
604
|
+
* `inline` keeps the markup as real children so it renders with no JavaScript.
|
|
605
|
+
* `sandbox` keeps only a `<noscript>` link — the markup itself lives, encoded,
|
|
606
|
+
* on `data-tq-embed-code` and the runtime mounts it in a sandboxed iframe.
|
|
607
|
+
*/
|
|
608
|
+
function buildEmbedFrameChildren(value) {
|
|
609
|
+
if (value.mode === 'inline') {
|
|
610
|
+
return value.code;
|
|
611
|
+
}
|
|
612
|
+
if (!value.url) {
|
|
613
|
+
return '';
|
|
614
|
+
}
|
|
615
|
+
return ("<noscript><a href=\"".concat(escapeEmbedAttribute(value.url), "\" rel=\"noopener noreferrer\" ") +
|
|
616
|
+
"target=\"_blank\">".concat(escapeEmbedText(value.url), "</a></noscript>"));
|
|
617
|
+
}
|
|
618
|
+
exports.buildEmbedFrameChildren = buildEmbedFrameChildren;
|
|
619
|
+
/* ── Reading a stored embed back out of the DOM ─────────────────────────────
|
|
620
|
+
*
|
|
621
|
+
* Used by the studio blot (`static value(node)`), by the selection overlay and
|
|
622
|
+
* by the runtime activator. The frame element is the single place the markup
|
|
623
|
+
* lives, so both directions agree on where to look.
|
|
624
|
+
*/
|
|
625
|
+
function findEmbedFrame(element) {
|
|
626
|
+
return element.querySelector(".".concat(exports.EMBED_FRAME_CLASS_NAME));
|
|
627
|
+
}
|
|
628
|
+
exports.findEmbedFrame = findEmbedFrame;
|
|
629
|
+
function readEmbedValueFromElement(element) {
|
|
630
|
+
var mode = element.getAttribute(exports.EMBED_ATTR.MODE) === 'sandbox' ? 'sandbox' : 'inline';
|
|
631
|
+
var frame = findEmbedFrame(element);
|
|
632
|
+
var rawRatio = element.getAttribute(exports.EMBED_ATTR.RATIO);
|
|
633
|
+
var parsedRatio = rawRatio === null ? NaN : parseFloat(rawRatio);
|
|
634
|
+
var code = '';
|
|
635
|
+
if (mode === 'sandbox') {
|
|
636
|
+
code = readEncodedEmbedCode(element);
|
|
637
|
+
}
|
|
638
|
+
else if (frame) {
|
|
639
|
+
code = frame.innerHTML;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
provider: element.getAttribute(exports.EMBED_ATTR.PROVIDER) || exports.CUSTOM_EMBED_PROVIDER_ID,
|
|
643
|
+
url: element.getAttribute(exports.EMBED_ATTR.URL) || '',
|
|
644
|
+
code: code,
|
|
645
|
+
mode: mode,
|
|
646
|
+
ratio: isFinite(parsedRatio) ? parsedRatio : null,
|
|
647
|
+
width: clampEmbedWidth(parseFloat(element.getAttribute(exports.EMBED_ATTR.WIDTH) || '')),
|
|
648
|
+
align: normalizeEmbedAlign(element.getAttribute(exports.EMBED_ATTR.ALIGN) || ''),
|
|
649
|
+
caption: element.getAttribute(exports.EMBED_ATTR.CAPTION) || '',
|
|
650
|
+
sandboxLevel: element.getAttribute(exports.EMBED_ATTR.SANDBOX) === 'trusted'
|
|
651
|
+
? 'trusted'
|
|
652
|
+
: exports.DEFAULT_EMBED_SANDBOX_LEVEL,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
exports.readEmbedValueFromElement = readEmbedValueFromElement;
|
|
656
|
+
/**
|
|
657
|
+
* A corrupted `data-tq-embed-code` (truncated by an export round-trip, mangled
|
|
658
|
+
* by a content rewrite) must degrade to "render nothing", never to a thrown
|
|
659
|
+
* exception inside a render pass.
|
|
660
|
+
*/
|
|
661
|
+
function readEncodedEmbedCode(element) {
|
|
662
|
+
var encoded = element.getAttribute(exports.EMBED_ATTR.CODE);
|
|
663
|
+
if (!encoded) {
|
|
664
|
+
return '';
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
return decodeEmbedCode(encoded);
|
|
668
|
+
}
|
|
669
|
+
catch (_a) {
|
|
670
|
+
return '';
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
exports.readEncodedEmbedCode = readEncodedEmbedCode;
|
|
674
|
+
/** Writes one `style` declaration block, replacing whatever was there. */
|
|
675
|
+
function writeStyle(element, style) {
|
|
676
|
+
element.setAttribute('style', style);
|
|
677
|
+
}
|
|
678
|
+
function applyEmbedElementLayout(element, updates) {
|
|
679
|
+
if (updates.width !== undefined) {
|
|
680
|
+
element.setAttribute(exports.EMBED_ATTR.WIDTH, String(clampEmbedWidth(updates.width)));
|
|
681
|
+
}
|
|
682
|
+
if (updates.align !== undefined) {
|
|
683
|
+
element.setAttribute(exports.EMBED_ATTR.ALIGN, normalizeEmbedAlign(updates.align));
|
|
684
|
+
}
|
|
685
|
+
writeStyle(element, embedBlockStyle({
|
|
686
|
+
width: clampEmbedWidth(parseFloat(element.getAttribute(exports.EMBED_ATTR.WIDTH) || '')),
|
|
687
|
+
align: normalizeEmbedAlign(element.getAttribute(exports.EMBED_ATTR.ALIGN) || ''),
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
exports.applyEmbedElementLayout = applyEmbedElementLayout;
|
|
691
|
+
/**
|
|
692
|
+
* Adds, updates or removes the caption element in place. The caption lives both
|
|
693
|
+
* as an attribute (so it survives a sandbox re-render) and as visible text.
|
|
694
|
+
*/
|
|
695
|
+
function applyEmbedElementCaption(element, caption) {
|
|
696
|
+
var trimmed = caption || '';
|
|
697
|
+
var node = element.querySelector(".".concat(exports.EMBED_CAPTION_CLASS_NAME));
|
|
698
|
+
if (!trimmed) {
|
|
699
|
+
element.removeAttribute(exports.EMBED_ATTR.CAPTION);
|
|
700
|
+
if (node && node.parentNode) {
|
|
701
|
+
node.parentNode.removeChild(node);
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
element.setAttribute(exports.EMBED_ATTR.CAPTION, trimmed);
|
|
706
|
+
if (!node) {
|
|
707
|
+
node = element.ownerDocument.createElement('figcaption');
|
|
708
|
+
node.setAttribute('class', exports.EMBED_CAPTION_CLASS_NAME);
|
|
709
|
+
writeStyle(node, exports.EMBED_CAPTION_STYLE);
|
|
710
|
+
element.appendChild(node);
|
|
711
|
+
}
|
|
712
|
+
node.textContent = trimmed;
|
|
713
|
+
}
|
|
714
|
+
exports.applyEmbedElementCaption = applyEmbedElementCaption;
|
|
715
|
+
/* ── The sandbox payload ────────────────────────────────────────────────────
|
|
716
|
+
*
|
|
717
|
+
* Three surfaces mount a sandboxed embed: the studio's Quill editor preview,
|
|
718
|
+
* the studio canvas (Solid) and the exported Next.js app (React). The WIRING
|
|
719
|
+
* has to differ — each framework owns its own effect and cleanup — but the
|
|
720
|
+
* payload must not, or an embed would behave differently depending on where it
|
|
721
|
+
* is looked at. Everything that decides behaviour lives here as data: the
|
|
722
|
+
* sandbox flags, the document the iframe is given, the height protocol and its
|
|
723
|
+
* bounds. Each surface only concatenates and mounts.
|
|
724
|
+
*/
|
|
725
|
+
/**
|
|
726
|
+
* `allow-same-origin` is deliberately absent: combined with `allow-scripts` it
|
|
727
|
+
* lets a frame reach into the host page and even remove its own sandbox, which
|
|
728
|
+
* is the whole thing this mode exists to prevent.
|
|
729
|
+
*/
|
|
730
|
+
exports.EMBED_SANDBOX_FLAGS_STRICT = 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-presentation';
|
|
731
|
+
/** The opt-in for widgets that refuse to render without storage access. */
|
|
732
|
+
exports.EMBED_SANDBOX_FLAGS_TRUSTED = "".concat(exports.EMBED_SANDBOX_FLAGS_STRICT, " allow-same-origin");
|
|
733
|
+
function resolveEmbedSandboxFlags(level) {
|
|
734
|
+
return level === 'trusted' ? exports.EMBED_SANDBOX_FLAGS_TRUSTED : exports.EMBED_SANDBOX_FLAGS_STRICT;
|
|
735
|
+
}
|
|
736
|
+
exports.resolveEmbedSandboxFlags = resolveEmbedSandboxFlags;
|
|
737
|
+
exports.EMBED_HEIGHT_MESSAGE_TYPE = 'tq-embed-height';
|
|
738
|
+
exports.EMBED_TOKEN_PLACEHOLDER = '__TQ_EMBED_TOKEN__';
|
|
739
|
+
exports.EMBED_MIN_HEIGHT = 80;
|
|
740
|
+
exports.EMBED_MAX_HEIGHT = 6000;
|
|
741
|
+
/** Until the frame reports back, this is what it occupies. */
|
|
742
|
+
exports.EMBED_INITIAL_HEIGHT = 320;
|
|
743
|
+
exports.EMBED_SANDBOX_DOC_HEAD = '<!doctype html><html><head><meta charset="utf-8">' +
|
|
744
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">' +
|
|
745
|
+
'<base target="_blank">' +
|
|
746
|
+
'<style>html,body{margin:0;padding:0;background:transparent}' +
|
|
747
|
+
'img,iframe,video,blockquote{max-width:100%}</style>' +
|
|
748
|
+
'<script>(function(){' +
|
|
749
|
+
"var token=".concat(JSON.stringify(exports.EMBED_TOKEN_PLACEHOLDER), ";") +
|
|
750
|
+
'var last=0;' +
|
|
751
|
+
'function report(){try{' +
|
|
752
|
+
'var doc=document.documentElement;var body=document.body;' +
|
|
753
|
+
'var height=Math.max(doc?doc.scrollHeight:0,doc?doc.offsetHeight:0,' +
|
|
754
|
+
'body?body.scrollHeight:0,body?body.offsetHeight:0);' +
|
|
755
|
+
'if(!height||height===last){return;}last=height;' +
|
|
756
|
+
"parent.postMessage({type:".concat(JSON.stringify(exports.EMBED_HEIGHT_MESSAGE_TYPE), ",token:token,height:height},\"*\");") +
|
|
757
|
+
'}catch(e){}}' +
|
|
758
|
+
'function watch(){report();' +
|
|
759
|
+
'if(typeof ResizeObserver==="function"&&document.documentElement){' +
|
|
760
|
+
'new ResizeObserver(report).observe(document.documentElement);}' +
|
|
761
|
+
'if(typeof MutationObserver==="function"&&document.body){' +
|
|
762
|
+
'new MutationObserver(report).observe(document.body,' +
|
|
763
|
+
'{childList:true,subtree:true,attributes:true});}' +
|
|
764
|
+
// Several providers paint well after `load` (they fetch, then swap in their
|
|
765
|
+
// own iframe), so a few late reports are what stops a widget from sitting in
|
|
766
|
+
// a box sized for its placeholder.
|
|
767
|
+
'var delays=[150,400,900,1800,3500];' +
|
|
768
|
+
'for(var i=0;i<delays.length;i++){setTimeout(report,delays[i]);}}' +
|
|
769
|
+
'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",watch);}' +
|
|
770
|
+
'else{watch();}window.addEventListener("load",report);' +
|
|
771
|
+
'})();</script></head><body>';
|
|
772
|
+
exports.EMBED_SANDBOX_DOC_TAIL = '</body></html>';
|
|
773
|
+
/**
|
|
774
|
+
* The document a sandboxed embed is given. Assign it to `iframe.srcdoc` as a
|
|
775
|
+
* property — writing it into the attribute would need HTML escaping.
|
|
776
|
+
*/
|
|
777
|
+
function buildEmbedSandboxDocument(code, token) {
|
|
778
|
+
return (exports.EMBED_SANDBOX_DOC_HEAD.replace(exports.EMBED_TOKEN_PLACEHOLDER, token) +
|
|
779
|
+
(code || '') +
|
|
780
|
+
exports.EMBED_SANDBOX_DOC_TAIL);
|
|
781
|
+
}
|
|
782
|
+
exports.buildEmbedSandboxDocument = buildEmbedSandboxDocument;
|
|
783
|
+
/**
|
|
784
|
+
* A sandboxed frame has an opaque origin, so `event.origin` is `"null"` and
|
|
785
|
+
* cannot identify it. The per-frame token baked into its document is the guard
|
|
786
|
+
* instead: a message is only ever matched against tokens this page minted.
|
|
787
|
+
*/
|
|
788
|
+
function parseEmbedHeightMessage(data) {
|
|
789
|
+
if (!data || typeof data !== 'object') {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
var message = data;
|
|
793
|
+
if (message.type !== exports.EMBED_HEIGHT_MESSAGE_TYPE || typeof message.token !== 'string') {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
var height = Number(message.height);
|
|
797
|
+
if (!isFinite(height) || height <= 0) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
token: message.token,
|
|
802
|
+
height: Math.min(Math.max(Math.round(height), exports.EMBED_MIN_HEIGHT), exports.EMBED_MAX_HEIGHT),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
exports.parseEmbedHeightMessage = parseEmbedHeightMessage;
|
|
806
|
+
var embedTokenCounter = 0;
|
|
807
|
+
function createEmbedToken() {
|
|
808
|
+
embedTokenCounter += 1;
|
|
809
|
+
return "tqe-".concat(Date.now().toString(36), "-").concat(embedTokenCounter.toString(36), "-").concat(Math.random()
|
|
810
|
+
.toString(36)
|
|
811
|
+
.slice(2, 8));
|
|
812
|
+
}
|
|
813
|
+
exports.createEmbedToken = createEmbedToken;
|
|
814
|
+
/**
|
|
815
|
+
* Restores every embed inside `root` to its canonical STORED form, in place.
|
|
816
|
+
*
|
|
817
|
+
* An editor reads its value back out of the live DOM (Quill hands back
|
|
818
|
+
* `root.innerHTML`), and by then the DOM carries things the content column must
|
|
819
|
+
* never keep: the preview an editor mounted next to a sandboxed embed, and the
|
|
820
|
+
* marker a runtime writes once it has handled a block. The editor normalizes a
|
|
821
|
+
* detached CLONE of its root through this before emitting a value, so what is
|
|
822
|
+
* stored is exactly what `buildEmbedElementHtml` would have produced — whatever
|
|
823
|
+
* the preview did to the live DOM.
|
|
824
|
+
*
|
|
825
|
+
* The frame restore is a self-heal for the other mounting strategy: the canvas
|
|
826
|
+
* and the published page mount INTO the frame, because they never read back.
|
|
827
|
+
*/
|
|
828
|
+
function normalizeEmbedsForStorage(root) {
|
|
829
|
+
var previews = root.querySelectorAll("[".concat(exports.EMBED_ATTR.PREVIEW, "]"));
|
|
830
|
+
for (var index = previews.length - 1; index >= 0; index -= 1) {
|
|
831
|
+
var preview = previews[index];
|
|
832
|
+
if (preview.parentNode) {
|
|
833
|
+
preview.parentNode.removeChild(preview);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
var blocks = root.querySelectorAll("[".concat(exports.EMBED_ATTR.MARKER, "]"));
|
|
837
|
+
for (var index = 0; index < blocks.length; index += 1) {
|
|
838
|
+
var block = blocks[index];
|
|
839
|
+
block.removeAttribute(exports.EMBED_ATTR.ACTIVATED);
|
|
840
|
+
var frame = findEmbedFrame(block);
|
|
841
|
+
if (!frame) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
var value = readEmbedValueFromElement(block);
|
|
845
|
+
if (value.mode !== 'sandbox') {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
frame.innerHTML = buildEmbedFrameChildren(value);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
exports.normalizeEmbedsForStorage = normalizeEmbedsForStorage;
|
|
852
|
+
//# sourceMappingURL=rich-text-embeds.js.map
|