@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.
@@ -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
+ '&': '&amp;',
410
+ '<': '&lt;',
411
+ '>': '&gt;',
412
+ '"': '&quot;',
413
+ '\'': '&#039;',
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