@fsegurai/marked-extended-embeds 17.0.0-beta.0
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/README.custom.md +641 -0
- package/README.md +730 -0
- package/dist/index.cjs +744 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.esm.js +742 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/renderer.d.ts +23 -0
- package/dist/tokenizer.d.ts +10 -0
- package/dist/types.d.ts +136 -0
- package/dist/utils/constants.d.ts +29 -0
- package/dist/utils/helpers.d.ts +38 -0
- package/dist/utils/inject-styles.d.ts +4 -0
- package/package.json +53 -0
- package/src/global.d.ts +13 -0
- package/src/index.ts +61 -0
- package/src/renderer.ts +212 -0
- package/src/tokenizer.ts +133 -0
- package/src/types.ts +163 -0
- package/src/utils/constants.ts +154 -0
- package/src/utils/helpers.ts +318 -0
- package/src/utils/inject-styles.ts +15 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure styles are injected into the document
|
|
3
|
+
*/
|
|
4
|
+
function ensureStyles(id, styles) {
|
|
5
|
+
if (typeof window === 'undefined')
|
|
6
|
+
return;
|
|
7
|
+
if (typeof document === 'undefined')
|
|
8
|
+
return;
|
|
9
|
+
if (document.getElementById(id))
|
|
10
|
+
return;
|
|
11
|
+
const styleEl = document.createElement('style');
|
|
12
|
+
styleEl.id = id;
|
|
13
|
+
styleEl.textContent = styles;
|
|
14
|
+
document.head.appendChild(styleEl);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default options for the embed extension
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_OPTIONS = {
|
|
21
|
+
className: 'marked-extended-embed',
|
|
22
|
+
prefixId: 'embed',
|
|
23
|
+
defaultAspectRatio: '16:9',
|
|
24
|
+
allowFullscreen: true,
|
|
25
|
+
lazyLoad: true,
|
|
26
|
+
privacyMode: false,
|
|
27
|
+
injectStyles: true,
|
|
28
|
+
enableSandbox: true,
|
|
29
|
+
sandboxPermissions: ['allow-scripts', 'allow-same-origin', 'allow-popups', 'allow-presentation'],
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Provider-specific URL patterns for detection
|
|
33
|
+
*/
|
|
34
|
+
const PROVIDER_PATTERNS = {
|
|
35
|
+
youtube: [
|
|
36
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]+)/,
|
|
37
|
+
/youtube\.com\/shorts\/([a-zA-Z0-9_-]+)/,
|
|
38
|
+
],
|
|
39
|
+
vimeo: [
|
|
40
|
+
/vimeo\.com\/(?:video\/)?(\d+)/,
|
|
41
|
+
],
|
|
42
|
+
twitter: [
|
|
43
|
+
/twitter\.com\/\w+\/status\/(\d+)/,
|
|
44
|
+
/x\.com\/\w+\/status\/(\d+)/,
|
|
45
|
+
],
|
|
46
|
+
codepen: [
|
|
47
|
+
/codepen\.io\/([^\/]+)\/pen\/([a-zA-Z0-9]+)/,
|
|
48
|
+
],
|
|
49
|
+
codesandbox: [
|
|
50
|
+
/codesandbox\.io\/(?:s|embed)\/([a-zA-Z0-9-]+)/,
|
|
51
|
+
],
|
|
52
|
+
'github-gist': [
|
|
53
|
+
/gist\.github\.com\/([^\/]+)\/([a-zA-Z0-9]+)/,
|
|
54
|
+
],
|
|
55
|
+
spotify: [
|
|
56
|
+
/open\.spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/,
|
|
57
|
+
],
|
|
58
|
+
soundcloud: [
|
|
59
|
+
/soundcloud\.com\/([^\/]+\/[^\/]+)/,
|
|
60
|
+
],
|
|
61
|
+
slideshare: [
|
|
62
|
+
/slideshare\.net\/[^\/]+\/([^\/\?]+)/,
|
|
63
|
+
],
|
|
64
|
+
figma: [
|
|
65
|
+
/figma\.com\/(?:file|proto|design)\/([a-zA-Z0-9]+)(?:\/([^?]+))?/,
|
|
66
|
+
],
|
|
67
|
+
loom: [
|
|
68
|
+
/loom\.com\/(?:share|embed)\/([a-zA-Z0-9]+)/,
|
|
69
|
+
],
|
|
70
|
+
miro: [
|
|
71
|
+
/miro\.com\/app\/board\/([a-zA-Z0-9_=-]+)/,
|
|
72
|
+
],
|
|
73
|
+
mermaid: [],
|
|
74
|
+
excalidraw: [
|
|
75
|
+
/excalidraw\.com\/#json=([a-zA-Z0-9_-]+)/,
|
|
76
|
+
/excalidraw\.com\/#room=([a-zA-Z0-9_-]+)/,
|
|
77
|
+
],
|
|
78
|
+
drawio: [
|
|
79
|
+
/(?:app\.)?diagrams\.net\/.*[?&]src=([^&]+)/,
|
|
80
|
+
/drive\.google\.com.*\/file\/d\/([^\/]+)/,
|
|
81
|
+
],
|
|
82
|
+
'diagrams-net': [
|
|
83
|
+
/(?:app\.)?diagrams\.net\/.*[?&]src=([^&]+)/,
|
|
84
|
+
],
|
|
85
|
+
pdf: [],
|
|
86
|
+
iframe: [],
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Provider display names
|
|
90
|
+
*/
|
|
91
|
+
const PROVIDER_NAMES = {
|
|
92
|
+
youtube: 'YouTube',
|
|
93
|
+
vimeo: 'Vimeo',
|
|
94
|
+
twitter: 'Twitter',
|
|
95
|
+
codepen: 'CodePen',
|
|
96
|
+
codesandbox: 'CodeSandbox',
|
|
97
|
+
'github-gist': 'GitHub Gist',
|
|
98
|
+
spotify: 'Spotify',
|
|
99
|
+
soundcloud: 'SoundCloud',
|
|
100
|
+
slideshare: 'SlideShare',
|
|
101
|
+
figma: 'Figma',
|
|
102
|
+
loom: 'Loom',
|
|
103
|
+
miro: 'Miro',
|
|
104
|
+
mermaid: 'Mermaid Diagram',
|
|
105
|
+
excalidraw: 'Excalidraw',
|
|
106
|
+
drawio: 'Draw.io',
|
|
107
|
+
'diagrams-net': 'Diagrams.net',
|
|
108
|
+
pdf: 'PDF Document',
|
|
109
|
+
iframe: 'External Content',
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Default template for embeds
|
|
113
|
+
*/
|
|
114
|
+
const DEFAULT_TEMPLATE = `
|
|
115
|
+
<div class="{className}-container" id="{embedId}" data-provider="{provider}">
|
|
116
|
+
<div class="{className}-wrapper" style="{aspectRatioStyle}">
|
|
117
|
+
{loadingPlaceholder}
|
|
118
|
+
<iframe
|
|
119
|
+
class="{className}-iframe"
|
|
120
|
+
src="{embedUrl}"
|
|
121
|
+
title="{title}"
|
|
122
|
+
frameborder="0"
|
|
123
|
+
{allowFullscreen}
|
|
124
|
+
{sandbox}
|
|
125
|
+
{loading}
|
|
126
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
127
|
+
referrerpolicy="no-referrer-when-downgrade"
|
|
128
|
+
></iframe>
|
|
129
|
+
</div>
|
|
130
|
+
{caption}
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
/**
|
|
134
|
+
* Loading placeholder template
|
|
135
|
+
*/
|
|
136
|
+
const LOADING_PLACEHOLDER = `
|
|
137
|
+
<div class="{className}-loading" aria-label="Loading embed">
|
|
138
|
+
<div class="{className}-spinner"></div>
|
|
139
|
+
<span class="{className}-loading-text">Loading {providerName}...</span>
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
/**
|
|
143
|
+
* Default structural styles for embeds (minimal, layout-only)
|
|
144
|
+
*/
|
|
145
|
+
const DEFAULT_STYLES = `
|
|
146
|
+
.marked-extended-embed-container { position: relative; margin: 1.5rem 0; overflow: hidden; }
|
|
147
|
+
.marked-extended-embed-wrapper { position: relative; width: 100%; height: 0; overflow: hidden; }
|
|
148
|
+
.marked-extended-embed-iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; }
|
|
149
|
+
.marked-extended-embed-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; gap: 1rem; }
|
|
150
|
+
.marked-extended-embed-spinner { width: 40px; height: 40px; }
|
|
151
|
+
.marked-extended-embed-caption { padding: 0.75rem 1rem; text-align: center; }
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Generate a unique ID for embeds
|
|
156
|
+
*/
|
|
157
|
+
let embedCounter = 0;
|
|
158
|
+
function generateUniqueId(prefix = 'embed') {
|
|
159
|
+
embedCounter++;
|
|
160
|
+
return `${prefix}-${Date.now()}-${embedCounter}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Parse property string like 'title="Video" autoplay="true"'
|
|
164
|
+
*/
|
|
165
|
+
function parsePropString(propString) {
|
|
166
|
+
const props = {};
|
|
167
|
+
if (!propString || !propString.trim()) {
|
|
168
|
+
return props;
|
|
169
|
+
}
|
|
170
|
+
// Match key="value" or key='value' or key=value patterns
|
|
171
|
+
const matches = propString.matchAll(/(\w+)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g);
|
|
172
|
+
for (const match of matches) {
|
|
173
|
+
const key = match[1];
|
|
174
|
+
const value = match[2] || match[3] || match[4];
|
|
175
|
+
props[key] = value;
|
|
176
|
+
}
|
|
177
|
+
return props;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Detect embed provider from URL
|
|
181
|
+
*/
|
|
182
|
+
function detectProvider(url) {
|
|
183
|
+
for (const [provider, patterns] of Object.entries(PROVIDER_PATTERNS)) {
|
|
184
|
+
for (const pattern of patterns) {
|
|
185
|
+
if (pattern.test(url)) {
|
|
186
|
+
return provider;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Parse embed URL and extract provider-specific information
|
|
194
|
+
*/
|
|
195
|
+
function parseEmbedUrl(url, provider) {
|
|
196
|
+
const patterns = PROVIDER_PATTERNS[provider];
|
|
197
|
+
for (const pattern of patterns) {
|
|
198
|
+
const match = url.match(pattern);
|
|
199
|
+
if (match) {
|
|
200
|
+
return buildEmbedInfo(provider, match, url);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Build embed information based on provider
|
|
207
|
+
*/
|
|
208
|
+
function buildEmbedInfo(provider, match, originalUrl) {
|
|
209
|
+
switch (provider) {
|
|
210
|
+
case 'youtube': {
|
|
211
|
+
const videoId = match[1];
|
|
212
|
+
return {
|
|
213
|
+
provider,
|
|
214
|
+
id: videoId,
|
|
215
|
+
embedUrl: `https://www.youtube.com/embed/${videoId}`,
|
|
216
|
+
originalUrl,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case 'vimeo': {
|
|
220
|
+
const videoId = match[1];
|
|
221
|
+
return {
|
|
222
|
+
provider,
|
|
223
|
+
id: videoId,
|
|
224
|
+
embedUrl: `https://player.vimeo.com/video/${videoId}`,
|
|
225
|
+
originalUrl,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
case 'twitter': {
|
|
229
|
+
const tweetId = match[1];
|
|
230
|
+
return {
|
|
231
|
+
provider,
|
|
232
|
+
id: tweetId,
|
|
233
|
+
embedUrl: `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}`,
|
|
234
|
+
originalUrl,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case 'codepen': {
|
|
238
|
+
const username = match[1];
|
|
239
|
+
const penId = match[2];
|
|
240
|
+
return {
|
|
241
|
+
provider,
|
|
242
|
+
id: penId,
|
|
243
|
+
embedUrl: `https://codepen.io/${username}/embed/${penId}`,
|
|
244
|
+
originalUrl,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
case 'codesandbox': {
|
|
248
|
+
const sandboxId = match[1];
|
|
249
|
+
return {
|
|
250
|
+
provider,
|
|
251
|
+
id: sandboxId,
|
|
252
|
+
embedUrl: `https://codesandbox.io/embed/${sandboxId}`,
|
|
253
|
+
originalUrl,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
case 'github-gist': {
|
|
257
|
+
const username = match[1];
|
|
258
|
+
const gistId = match[2];
|
|
259
|
+
return {
|
|
260
|
+
provider,
|
|
261
|
+
id: gistId,
|
|
262
|
+
embedUrl: `https://gist.github.com/${username}/${gistId}`,
|
|
263
|
+
originalUrl,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
case 'spotify': {
|
|
267
|
+
const type = match[1];
|
|
268
|
+
const id = match[2];
|
|
269
|
+
return {
|
|
270
|
+
provider,
|
|
271
|
+
id,
|
|
272
|
+
embedUrl: `https://open.spotify.com/embed/${type}/${id}`,
|
|
273
|
+
originalUrl,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
case 'soundcloud': {
|
|
277
|
+
const trackPath = match[1];
|
|
278
|
+
return {
|
|
279
|
+
provider,
|
|
280
|
+
id: trackPath,
|
|
281
|
+
embedUrl: `https://w.soundcloud.com/player/?url=https://soundcloud.com/${trackPath}`,
|
|
282
|
+
originalUrl,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
case 'figma': {
|
|
286
|
+
const fileId = match[1];
|
|
287
|
+
return {
|
|
288
|
+
provider,
|
|
289
|
+
id: fileId,
|
|
290
|
+
embedUrl: `https://www.figma.com/embed?embed_host=marked-extensions&url=${encodeURIComponent(originalUrl)}`,
|
|
291
|
+
originalUrl,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
case 'loom': {
|
|
295
|
+
const videoId = match[1];
|
|
296
|
+
return {
|
|
297
|
+
provider,
|
|
298
|
+
id: videoId,
|
|
299
|
+
embedUrl: `https://www.loom.com/embed/${videoId}`,
|
|
300
|
+
originalUrl,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
case 'miro': {
|
|
304
|
+
const boardId = match[1];
|
|
305
|
+
return {
|
|
306
|
+
provider,
|
|
307
|
+
id: boardId,
|
|
308
|
+
embedUrl: `https://miro.com/app/live-embed/${boardId}`,
|
|
309
|
+
originalUrl,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
case 'excalidraw': {
|
|
313
|
+
// Handle both #json= and #room= URLs
|
|
314
|
+
const id = match[1];
|
|
315
|
+
if (originalUrl.includes('#room=')) {
|
|
316
|
+
return {
|
|
317
|
+
provider,
|
|
318
|
+
id,
|
|
319
|
+
embedUrl: `https://excalidraw.com/#room=${id}`,
|
|
320
|
+
originalUrl,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
provider,
|
|
325
|
+
id,
|
|
326
|
+
embedUrl: `https://excalidraw.com/#json=${id}`,
|
|
327
|
+
originalUrl,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
case 'drawio':
|
|
331
|
+
case 'diagrams-net': {
|
|
332
|
+
// For draw.io, we need to use the original URL with viewer
|
|
333
|
+
const fileUrl = match[1] || encodeURIComponent(originalUrl);
|
|
334
|
+
return {
|
|
335
|
+
provider,
|
|
336
|
+
id: fileUrl,
|
|
337
|
+
embedUrl: `https://viewer.diagrams.net/?highlight=0000ff&edit=_blank&layers=1&nav=1&title=diagram#U${fileUrl}`,
|
|
338
|
+
originalUrl,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
default:
|
|
342
|
+
return {
|
|
343
|
+
provider,
|
|
344
|
+
id: '',
|
|
345
|
+
embedUrl: originalUrl,
|
|
346
|
+
originalUrl,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Build embed URL with parameters
|
|
352
|
+
*/
|
|
353
|
+
function buildEmbedUrlWithParams(baseUrl, params = {}, providerConfig) {
|
|
354
|
+
const url = new URL(baseUrl);
|
|
355
|
+
// Apply custom domain if specified
|
|
356
|
+
if (providerConfig?.domain) {
|
|
357
|
+
url.hostname = providerConfig.domain;
|
|
358
|
+
}
|
|
359
|
+
// Add provider-specific params
|
|
360
|
+
if (providerConfig?.params) {
|
|
361
|
+
Object.entries(providerConfig.params).forEach(([key, value]) => {
|
|
362
|
+
url.searchParams.set(key, value);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// Add custom params
|
|
366
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
367
|
+
url.searchParams.set(key, value);
|
|
368
|
+
});
|
|
369
|
+
return url.toString();
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Replace template placeholders with values
|
|
373
|
+
*/
|
|
374
|
+
function replaceTemplatePlaceholders(template, values) {
|
|
375
|
+
let result = template;
|
|
376
|
+
for (const [key, value] of Object.entries(values)) {
|
|
377
|
+
const regex = new RegExp(`\\{${key}\\}`, 'g');
|
|
378
|
+
result = result.replace(regex, value || '');
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get aspect ratio style
|
|
384
|
+
*/
|
|
385
|
+
function getAspectRatioStyle(aspectRatio, customWidth, customHeight) {
|
|
386
|
+
if (customWidth && customHeight) {
|
|
387
|
+
return `width: ${customWidth}; height: ${customHeight};`;
|
|
388
|
+
}
|
|
389
|
+
const ratios = {
|
|
390
|
+
'16:9': '56.25%',
|
|
391
|
+
'4:3': '75%',
|
|
392
|
+
'1:1': '100%',
|
|
393
|
+
'21:9': '42.85%',
|
|
394
|
+
};
|
|
395
|
+
const padding = ratios[aspectRatio] || ratios['16:9'];
|
|
396
|
+
return `padding-bottom: ${padding};`;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Build sandbox attribute value
|
|
400
|
+
*/
|
|
401
|
+
function buildSandboxAttribute(permissions) {
|
|
402
|
+
return permissions.join(' ');
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Escape HTML to prevent XSS
|
|
406
|
+
*/
|
|
407
|
+
function escapeHtml(text) {
|
|
408
|
+
const map = {
|
|
409
|
+
'&': '&',
|
|
410
|
+
'<': '<',
|
|
411
|
+
'>': '>',
|
|
412
|
+
'"': '"',
|
|
413
|
+
'\'': ''',
|
|
414
|
+
};
|
|
415
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Render an embed
|
|
420
|
+
*/
|
|
421
|
+
function renderEmbed(options) {
|
|
422
|
+
const { embedId, meta, className, template, lazyLoad, enableSandbox, sandboxPermissions, providers } = options;
|
|
423
|
+
const provider = meta.provider;
|
|
424
|
+
const providerConfig = providers[provider];
|
|
425
|
+
const providerName = PROVIDER_NAMES[provider] || provider;
|
|
426
|
+
// Build embed URL with parameters
|
|
427
|
+
const params = {
|
|
428
|
+
...meta.params,
|
|
429
|
+
};
|
|
430
|
+
// Add provider-specific params
|
|
431
|
+
if (meta.autoplay)
|
|
432
|
+
params['autoplay'] = '1';
|
|
433
|
+
if (meta.muted)
|
|
434
|
+
params['mute'] = '1';
|
|
435
|
+
if (meta.loop)
|
|
436
|
+
params['loop'] = '1';
|
|
437
|
+
if (meta.startTime)
|
|
438
|
+
params['start'] = meta.startTime;
|
|
439
|
+
if (meta.controls === false)
|
|
440
|
+
params['controls'] = '0';
|
|
441
|
+
// Privacy mode for YouTube
|
|
442
|
+
if (provider === 'youtube' && (meta.privacyMode || providerConfig?.domain)) {
|
|
443
|
+
const urlObj = new URL(meta.url);
|
|
444
|
+
urlObj.hostname = 'www.youtube-nocookie.com';
|
|
445
|
+
meta.url = urlObj.toString();
|
|
446
|
+
}
|
|
447
|
+
const embedUrl = buildEmbedUrlWithParams(meta.url, params, providerConfig);
|
|
448
|
+
// Build aspect ratio style
|
|
449
|
+
const aspectRatio = meta.aspectRatio || '16:9';
|
|
450
|
+
const aspectRatioStyle = getAspectRatioStyle(aspectRatio, meta.width, meta.height);
|
|
451
|
+
// Build attributes
|
|
452
|
+
const allowFullscreen = meta.allowFullscreen !== false ? 'allowfullscreen' : '';
|
|
453
|
+
const loading = lazyLoad ? 'loading="lazy"' : '';
|
|
454
|
+
const sandbox = enableSandbox
|
|
455
|
+
? `sandbox="${buildSandboxAttribute(sandboxPermissions)}"`
|
|
456
|
+
: '';
|
|
457
|
+
// Build loading placeholder
|
|
458
|
+
const loadingPlaceholder = lazyLoad
|
|
459
|
+
? replaceTemplatePlaceholders(LOADING_PLACEHOLDER, {
|
|
460
|
+
className,
|
|
461
|
+
providerName,
|
|
462
|
+
})
|
|
463
|
+
: '';
|
|
464
|
+
// Build caption
|
|
465
|
+
const caption = meta.title
|
|
466
|
+
? `<div class="${className}-caption">${escapeHtml(meta.title)}</div>`
|
|
467
|
+
: '';
|
|
468
|
+
// Get template
|
|
469
|
+
const templateToUse = template || providerConfig?.template || DEFAULT_TEMPLATE;
|
|
470
|
+
// Replace placeholders
|
|
471
|
+
return replaceTemplatePlaceholders(templateToUse, {
|
|
472
|
+
className,
|
|
473
|
+
embedId,
|
|
474
|
+
provider,
|
|
475
|
+
providerName,
|
|
476
|
+
embedUrl,
|
|
477
|
+
title: escapeHtml(meta.title || `${providerName} embed`),
|
|
478
|
+
aspectRatioStyle,
|
|
479
|
+
allowFullscreen,
|
|
480
|
+
sandbox,
|
|
481
|
+
loading,
|
|
482
|
+
loadingPlaceholder,
|
|
483
|
+
caption,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Render special embeds (mermaid, excalidraw, pdf, drawio)
|
|
488
|
+
*/
|
|
489
|
+
function renderSpecialEmbed(options) {
|
|
490
|
+
const { embedId, meta, className } = options;
|
|
491
|
+
const provider = meta.provider;
|
|
492
|
+
switch (provider) {
|
|
493
|
+
case 'mermaid':
|
|
494
|
+
return renderMermaidEmbed(embedId, meta, className);
|
|
495
|
+
case 'excalidraw':
|
|
496
|
+
return renderExcalidrawEmbed(embedId, meta, className);
|
|
497
|
+
case 'drawio':
|
|
498
|
+
case 'diagrams-net':
|
|
499
|
+
return renderDrawioEmbed(embedId, meta, className);
|
|
500
|
+
case 'pdf':
|
|
501
|
+
return renderPdfEmbed(embedId, meta, className);
|
|
502
|
+
default:
|
|
503
|
+
return renderEmbed(options);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Render Mermaid diagram
|
|
508
|
+
*/
|
|
509
|
+
function renderMermaidEmbed(embedId, meta, className) {
|
|
510
|
+
// Extract diagram code from URL (it should be the content after 'mermaid://')
|
|
511
|
+
const diagramCode = meta.url.replace(/^mermaid:\/\//, '');
|
|
512
|
+
return `
|
|
513
|
+
<div class="${className}-container ${className}-mermaid" id="${embedId}" data-provider="mermaid">
|
|
514
|
+
<div class="mermaid">
|
|
515
|
+
${escapeHtml(diagramCode)}
|
|
516
|
+
</div>
|
|
517
|
+
${meta.title ? `<div class="${className}-caption">${escapeHtml(meta.title)}</div>` : ''}
|
|
518
|
+
</div>
|
|
519
|
+
`;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Render Excalidraw diagram
|
|
523
|
+
*/
|
|
524
|
+
function renderExcalidrawEmbed(embedId, meta, className) {
|
|
525
|
+
// Excalidraw embeds use iframe with the full URL
|
|
526
|
+
const embedUrl = meta.url;
|
|
527
|
+
const aspectRatioStyle = getAspectRatioStyle(meta.aspectRatio || '16:9', meta.width, meta.height);
|
|
528
|
+
return `
|
|
529
|
+
<div class="${className}-container ${className}-excalidraw" id="${embedId}" data-provider="excalidraw">
|
|
530
|
+
<div class="${className}-wrapper" style="${aspectRatioStyle}">
|
|
531
|
+
<iframe
|
|
532
|
+
class="${className}-iframe"
|
|
533
|
+
src="${embedUrl}"
|
|
534
|
+
title="${escapeHtml(meta.title || 'Excalidraw diagram')}"
|
|
535
|
+
frameborder="0"
|
|
536
|
+
allowfullscreen
|
|
537
|
+
></iframe>
|
|
538
|
+
</div>
|
|
539
|
+
${meta.title ? `<div class="${className}-caption">${escapeHtml(meta.title)}</div>` : ''}
|
|
540
|
+
</div>
|
|
541
|
+
`;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Render Draw.io / Diagrams.net diagram
|
|
545
|
+
*/
|
|
546
|
+
function renderDrawioEmbed(embedId, meta, className) {
|
|
547
|
+
const embedUrl = meta.url;
|
|
548
|
+
const aspectRatioStyle = getAspectRatioStyle(meta.aspectRatio || '16:9', meta.width, meta.height);
|
|
549
|
+
return `
|
|
550
|
+
<div class="${className}-container ${className}-drawio" id="${embedId}" data-provider="drawio">
|
|
551
|
+
<div class="${className}-wrapper" style="${aspectRatioStyle}">
|
|
552
|
+
<iframe
|
|
553
|
+
class="${className}-iframe"
|
|
554
|
+
src="${embedUrl}"
|
|
555
|
+
title="${escapeHtml(meta.title || 'Draw.io diagram')}"
|
|
556
|
+
frameborder="0"
|
|
557
|
+
allowfullscreen
|
|
558
|
+
></iframe>
|
|
559
|
+
</div>
|
|
560
|
+
${meta.title ? `<div class="${className}-caption">${escapeHtml(meta.title)}</div>` : ''}
|
|
561
|
+
</div>
|
|
562
|
+
`;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Render PDF embed
|
|
566
|
+
*/
|
|
567
|
+
function renderPdfEmbed(embedId, meta, className) {
|
|
568
|
+
const aspectRatioStyle = getAspectRatioStyle('4:3', meta.width, meta.height);
|
|
569
|
+
return `
|
|
570
|
+
<div class="${className}-container ${className}-pdf" id="${embedId}" data-provider="pdf">
|
|
571
|
+
<div class="${className}-wrapper" style="${aspectRatioStyle}">
|
|
572
|
+
<iframe
|
|
573
|
+
class="${className}-iframe"
|
|
574
|
+
src="${meta.url}"
|
|
575
|
+
title="${escapeHtml(meta.title || 'PDF document')}"
|
|
576
|
+
frameborder="0"
|
|
577
|
+
type="application/pdf"
|
|
578
|
+
></iframe>
|
|
579
|
+
</div>
|
|
580
|
+
${meta.title ? `<div class="${className}-caption">${escapeHtml(meta.title)}</div>` : ''}
|
|
581
|
+
</div>
|
|
582
|
+
`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Create embed tokenizer
|
|
587
|
+
*/
|
|
588
|
+
function createEmbedTokenizer() {
|
|
589
|
+
return {
|
|
590
|
+
name: 'embed',
|
|
591
|
+
level: 'block',
|
|
592
|
+
tokenizer(src) {
|
|
593
|
+
// Regex for embed blocks: ::::embed{props}\nurl\n::::embedend
|
|
594
|
+
const match = src.match(/^::::embed(?:\{([^}]*)})?s*\n([\s\S]*?)::::embedend/);
|
|
595
|
+
if (!match)
|
|
596
|
+
return undefined;
|
|
597
|
+
const [raw, propString = '', content] = match;
|
|
598
|
+
const url = content.trim();
|
|
599
|
+
if (!url)
|
|
600
|
+
return undefined;
|
|
601
|
+
// Parse properties
|
|
602
|
+
const props = parsePropString(propString);
|
|
603
|
+
// Detect provider from URL or use explicit provider
|
|
604
|
+
let provider = props['provider'];
|
|
605
|
+
if (!provider) {
|
|
606
|
+
const detectedProvider = detectProvider(url);
|
|
607
|
+
if (!detectedProvider)
|
|
608
|
+
return undefined;
|
|
609
|
+
provider = detectedProvider;
|
|
610
|
+
}
|
|
611
|
+
// Parse embed URL
|
|
612
|
+
let embedUrl = url;
|
|
613
|
+
if (!['mermaid', 'excalidraw', 'pdf', 'iframe'].includes(provider)) {
|
|
614
|
+
const parsed = parseEmbedUrl(url, provider);
|
|
615
|
+
if (parsed) {
|
|
616
|
+
embedUrl = parsed.embedUrl;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const meta = {
|
|
620
|
+
provider,
|
|
621
|
+
url: embedUrl,
|
|
622
|
+
title: props['title'],
|
|
623
|
+
aspectRatio: (props['aspectRatio'] || props['ratio']),
|
|
624
|
+
width: props['width'],
|
|
625
|
+
height: props['height'],
|
|
626
|
+
allowFullscreen: props['allowFullscreen'] === 'false' ? false : undefined,
|
|
627
|
+
autoplay: props['autoplay'] === 'true',
|
|
628
|
+
lazyLoad: props['lazyLoad'] !== 'false',
|
|
629
|
+
privacyMode: props['privacyMode'] === 'true' || props['privacy'] === 'true',
|
|
630
|
+
startTime: props['startTime'] || props['start'],
|
|
631
|
+
muted: props['muted'] === 'true',
|
|
632
|
+
loop: props['loop'] === 'true',
|
|
633
|
+
className: props['className'] || props['class'],
|
|
634
|
+
id: props['id'],
|
|
635
|
+
theme: props['theme'] || 'light',
|
|
636
|
+
controls: props['controls'] !== 'false',
|
|
637
|
+
params: {},
|
|
638
|
+
};
|
|
639
|
+
// Parse additional params (any prop starting with 'param-')
|
|
640
|
+
Object.entries(props).forEach(([key, value]) => {
|
|
641
|
+
if (key.startsWith('param-')) {
|
|
642
|
+
const paramName = key.replace('param-', '');
|
|
643
|
+
meta.params[paramName] = value;
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
return {
|
|
647
|
+
type: 'embed',
|
|
648
|
+
raw,
|
|
649
|
+
meta,
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Create renderer for embeds
|
|
656
|
+
*/
|
|
657
|
+
function createEmbedRenderer(options) {
|
|
658
|
+
return {
|
|
659
|
+
name: 'embed',
|
|
660
|
+
renderer(token) {
|
|
661
|
+
const embedToken = token;
|
|
662
|
+
const meta = embedToken.meta;
|
|
663
|
+
// Generate unique ID
|
|
664
|
+
const embedId = meta.id || generateUniqueId(options.prefixId);
|
|
665
|
+
// Check if special embed (mermaid, excalidraw, pdf, drawio)
|
|
666
|
+
const isSpecialEmbed = ['mermaid', 'excalidraw', 'pdf', 'drawio', 'diagrams-net'].includes(meta.provider);
|
|
667
|
+
const renderOptions = {
|
|
668
|
+
embedId,
|
|
669
|
+
meta: {
|
|
670
|
+
...meta,
|
|
671
|
+
// Override allowFullscreen from global config if meta doesn't specify
|
|
672
|
+
allowFullscreen: meta.allowFullscreen !== undefined ? meta.allowFullscreen : options.allowFullscreen,
|
|
673
|
+
},
|
|
674
|
+
className: meta.className || options.className,
|
|
675
|
+
template: options.template,
|
|
676
|
+
lazyLoad: meta.lazyLoad !== false && options.lazyLoad,
|
|
677
|
+
enableSandbox: options.enableSandbox,
|
|
678
|
+
sandboxPermissions: options.sandboxPermissions,
|
|
679
|
+
providers: options.providers || {},
|
|
680
|
+
};
|
|
681
|
+
// Render embed
|
|
682
|
+
const html = isSpecialEmbed
|
|
683
|
+
? renderSpecialEmbed(renderOptions)
|
|
684
|
+
: renderEmbed(renderOptions);
|
|
685
|
+
// Trigger onEmbedLoad callback if defined
|
|
686
|
+
if (options.onEmbedLoad && typeof options.onEmbedLoad === 'function') {
|
|
687
|
+
setTimeout(() => {
|
|
688
|
+
try {
|
|
689
|
+
options.onEmbedLoad(embedId, meta.provider);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
console.error('Error in onEmbedLoad callback:', error);
|
|
693
|
+
}
|
|
694
|
+
}, 0);
|
|
695
|
+
}
|
|
696
|
+
return html;
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Marked Extended Embeds extension
|
|
703
|
+
* Adds support for rich media embeds from various platforms
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```markdown
|
|
707
|
+
* ::::embed{title="My Video" aspectRatio="16:9"}
|
|
708
|
+
* https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
709
|
+
* ::::embedend
|
|
710
|
+
*
|
|
711
|
+
* ::::embed{provider="codepen" theme="dark"}
|
|
712
|
+
* https://codepen.io/username/pen/abc123
|
|
713
|
+
* ::::embedend
|
|
714
|
+
* ```
|
|
715
|
+
*
|
|
716
|
+
* @param options - Configuration options
|
|
717
|
+
* @returns Marked extension object
|
|
718
|
+
*/
|
|
719
|
+
function markedExtendedEmbeds(options = {}) {
|
|
720
|
+
// Merge options with defaults
|
|
721
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
722
|
+
// Inject styles if needed
|
|
723
|
+
if (config.injectStyles) {
|
|
724
|
+
ensureStyles('marked-extended-embeds-styles', DEFAULT_STYLES);
|
|
725
|
+
}
|
|
726
|
+
// Return the extension
|
|
727
|
+
return {
|
|
728
|
+
walkTokens(token) {
|
|
729
|
+
if (token.type !== 'embed')
|
|
730
|
+
return;
|
|
731
|
+
const embedToken = token;
|
|
732
|
+
// Apply custom token modifications if configured
|
|
733
|
+
if (config.customizeToken && typeof config.customizeToken === 'function') {
|
|
734
|
+
config.customizeToken(embedToken);
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
extensions: [createEmbedTokenizer(), createEmbedRenderer(config)],
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export { markedExtendedEmbeds as default };
|
|
742
|
+
//# sourceMappingURL=index.esm.js.map
|