@shoppexio/builder-runtime 0.1.1 → 0.1.3
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/dist/YouTubeEmbed.d.ts +13 -0
- package/dist/YouTubeEmbed.d.ts.map +1 -0
- package/dist/YouTubeEmbed.js +49 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts +7 -0
- package/dist/YouTubeEmbedBuilderBlock.d.ts.map +1 -0
- package/dist/YouTubeEmbedBuilderBlock.js +16 -0
- package/dist/block-style-settings.d.ts +5 -0
- package/dist/block-style-settings.d.ts.map +1 -0
- package/dist/block-style-settings.js +16 -0
- package/dist/builder-runtime.test.d.ts +2 -0
- package/dist/builder-runtime.test.d.ts.map +1 -0
- package/dist/builder-runtime.test.js +115 -0
- package/dist/content.d.ts +6 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +31 -7
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/layout.d.ts +3 -8
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +2 -10
- package/dist/manifest-setting-paths.d.ts +5 -0
- package/dist/manifest-setting-paths.d.ts.map +1 -0
- package/dist/manifest-setting-paths.js +40 -0
- package/dist/merchant-custom-page.d.ts +57 -0
- package/dist/merchant-custom-page.d.ts.map +1 -0
- package/dist/merchant-custom-page.js +63 -0
- package/dist/preview-fixtures.d.ts +16 -0
- package/dist/preview-fixtures.d.ts.map +1 -0
- package/dist/preview-fixtures.js +40 -0
- package/dist/preview-mode.d.ts +2 -0
- package/dist/preview-mode.d.ts.map +1 -0
- package/dist/preview-mode.js +7 -0
- package/dist/product-page.d.ts +13 -0
- package/dist/product-page.d.ts.map +1 -0
- package/dist/product-page.js +18 -0
- package/dist/react-runtime.test.d.ts +2 -0
- package/dist/react-runtime.test.d.ts.map +1 -0
- package/dist/react-runtime.test.js +332 -0
- package/dist/react.d.ts +37 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +138 -46
- package/dist/search-bar-settings.d.ts +33 -0
- package/dist/search-bar-settings.d.ts.map +1 -0
- package/dist/search-bar-settings.js +99 -0
- package/dist/standard-product-blocks.d.ts +48 -0
- package/dist/standard-product-blocks.d.ts.map +1 -0
- package/dist/standard-product-blocks.js +45 -0
- package/dist/standard-product-page.d.ts +69 -0
- package/dist/standard-product-page.d.ts.map +1 -0
- package/dist/standard-product-page.js +89 -0
- package/dist/storefront-google-fonts.d.ts +2 -0
- package/dist/storefront-google-fonts.d.ts.map +1 -0
- package/dist/storefront-google-fonts.js +28 -0
- package/dist/youtube-embed-block.d.ts +10 -0
- package/dist/youtube-embed-block.d.ts.map +1 -0
- package/dist/youtube-embed-block.js +19 -0
- package/dist/youtube.d.ts +5 -0
- package/dist/youtube.d.ts.map +1 -0
- package/dist/youtube.js +52 -0
- package/package.json +3 -3
- package/src/YouTubeEmbed.tsx +105 -0
- package/src/YouTubeEmbedBuilderBlock.tsx +49 -0
- package/src/block-style-settings.ts +24 -0
- package/src/builder-runtime.test.ts +69 -0
- package/src/content.ts +44 -9
- package/src/index.ts +8 -0
- package/src/layout.ts +11 -21
- package/src/manifest-setting-paths.test.ts +23 -0
- package/src/manifest-setting-paths.ts +55 -0
- package/src/merchant-custom-page.tsx +161 -0
- package/src/preview-fixtures.ts +56 -0
- package/src/preview-mode.ts +8 -0
- package/src/product-page.test.ts +37 -0
- package/src/product-page.ts +32 -0
- package/src/react-runtime.test.tsx +42 -0
- package/src/react.tsx +243 -49
- package/src/search-bar-settings.test.ts +72 -0
- package/src/search-bar-settings.ts +176 -0
- package/src/standard-product-blocks.test.tsx +93 -0
- package/src/standard-product-blocks.tsx +121 -0
- package/src/standard-product-page.test.ts +171 -0
- package/src/standard-product-page.ts +169 -0
- package/src/storefront-google-fonts.test.ts +31 -0
- package/src/storefront-google-fonts.ts +43 -0
- package/src/youtube-embed-block.test.ts +76 -0
- package/src/youtube-embed-block.ts +28 -0
- package/src/youtube-embed-builder-block.test.tsx +166 -0
- package/src/youtube.test.ts +48 -0
- package/src/youtube.ts +66 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
3
|
+
import {
|
|
4
|
+
getYouTubeEmbedBlockStyleProps,
|
|
5
|
+
readYouTubeEmbedBlockSettings,
|
|
6
|
+
} from './youtube-embed-block.js';
|
|
7
|
+
|
|
8
|
+
describe('readYouTubeEmbedBlockSettings', () => {
|
|
9
|
+
test('reads url, title, height, and privacy flag from block settings', () => {
|
|
10
|
+
expect(
|
|
11
|
+
readYouTubeEmbedBlockSettings({
|
|
12
|
+
id: 'yt-1',
|
|
13
|
+
settings: {
|
|
14
|
+
videoUrl: 'https://youtu.be/dQw4w9WgXcQ',
|
|
15
|
+
title: 'Launch trailer',
|
|
16
|
+
height: 480,
|
|
17
|
+
privacyEnhanced: true,
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
).toEqual({
|
|
21
|
+
videoUrl: 'https://youtu.be/dQw4w9WgXcQ',
|
|
22
|
+
title: 'Launch trailer',
|
|
23
|
+
height: 480,
|
|
24
|
+
privacyEnhanced: true,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('falls back to defaults for missing or invalid values', () => {
|
|
29
|
+
expect(
|
|
30
|
+
readYouTubeEmbedBlockSettings({
|
|
31
|
+
id: 'yt-1',
|
|
32
|
+
settings: {
|
|
33
|
+
videoUrl: 42,
|
|
34
|
+
title: ' ',
|
|
35
|
+
height: 0,
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
).toEqual({
|
|
39
|
+
videoUrl: '',
|
|
40
|
+
title: 'YouTube video',
|
|
41
|
+
height: undefined,
|
|
42
|
+
privacyEnhanced: false,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('readManifestStyleBlockProps', () => {
|
|
48
|
+
test('maps manifest style settings to inline styles', () => {
|
|
49
|
+
expect(
|
|
50
|
+
readManifestStyleBlockProps({
|
|
51
|
+
'style.background': '#111111',
|
|
52
|
+
'style.borderRadius': 12,
|
|
53
|
+
'style.padding': 16,
|
|
54
|
+
}),
|
|
55
|
+
).toEqual({
|
|
56
|
+
backgroundColor: '#111111',
|
|
57
|
+
borderRadius: '12px',
|
|
58
|
+
padding: '16px',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getYouTubeEmbedBlockStyleProps', () => {
|
|
64
|
+
test('delegates to shared manifest style reader', () => {
|
|
65
|
+
expect(
|
|
66
|
+
getYouTubeEmbedBlockStyleProps({
|
|
67
|
+
id: 'yt-1',
|
|
68
|
+
settings: {
|
|
69
|
+
'style.background': '#00000000',
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
).toEqual({
|
|
73
|
+
backgroundColor: '#00000000',
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BlockInstance } from '@shoppex/builder-contracts';
|
|
2
|
+
import { readManifestStyleBlockProps } from './block-style-settings.js';
|
|
3
|
+
|
|
4
|
+
export type YouTubeEmbedBlockInstance = Pick<BlockInstance, 'id' | 'settings'>;
|
|
5
|
+
|
|
6
|
+
export function readYouTubeEmbedBlockSettings(block: YouTubeEmbedBlockInstance) {
|
|
7
|
+
const videoUrl =
|
|
8
|
+
typeof block.settings.videoUrl === 'string' ? block.settings.videoUrl : '';
|
|
9
|
+
const title =
|
|
10
|
+
typeof block.settings.title === 'string' && block.settings.title.trim().length > 0
|
|
11
|
+
? block.settings.title.trim()
|
|
12
|
+
: 'YouTube video';
|
|
13
|
+
const heightRaw = block.settings.height;
|
|
14
|
+
const height =
|
|
15
|
+
typeof heightRaw === 'number' && heightRaw > 0 ? heightRaw : undefined;
|
|
16
|
+
const privacyEnhanced = block.settings.privacyEnhanced === true;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
videoUrl,
|
|
20
|
+
title,
|
|
21
|
+
height,
|
|
22
|
+
privacyEnhanced,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getYouTubeEmbedBlockStyleProps(block: YouTubeEmbedBlockInstance) {
|
|
27
|
+
return readManifestStyleBlockProps(block.settings);
|
|
28
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
import { act } from 'react';
|
|
4
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
5
|
+
import { YouTubeEmbedBuilderBlock } from './YouTubeEmbedBuilderBlock.js';
|
|
6
|
+
|
|
7
|
+
describe('YouTubeEmbedBuilderBlock', () => {
|
|
8
|
+
let dom: JSDOM;
|
|
9
|
+
let container: HTMLDivElement;
|
|
10
|
+
let root: Root;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
dom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
14
|
+
url: 'https://preview.shoppex.test/?shoppex-preview-mode=builder',
|
|
15
|
+
});
|
|
16
|
+
globalThis.window = dom.window as unknown as Window & typeof globalThis;
|
|
17
|
+
globalThis.document = dom.window.document;
|
|
18
|
+
container = dom.window.document.createElement('div');
|
|
19
|
+
dom.window.document.body.appendChild(container);
|
|
20
|
+
root = createRoot(container);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
act(() => {
|
|
25
|
+
root.unmount();
|
|
26
|
+
});
|
|
27
|
+
container.remove();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('renders a sandboxed YouTube iframe with builder block attributes', () => {
|
|
31
|
+
act(() => {
|
|
32
|
+
root.render(
|
|
33
|
+
<YouTubeEmbedBuilderBlock
|
|
34
|
+
block={{
|
|
35
|
+
id: 'youtube-1',
|
|
36
|
+
type: 'youtube-embed',
|
|
37
|
+
settings: {
|
|
38
|
+
videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
39
|
+
title: 'Demo video',
|
|
40
|
+
height: 360,
|
|
41
|
+
},
|
|
42
|
+
}}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const wrapper = container.querySelector('[data-builder-block="youtube-1"]');
|
|
48
|
+
expect(wrapper?.getAttribute('data-page-id')).toBe('home');
|
|
49
|
+
expect(wrapper?.getAttribute('data-builder-block-type')).toBe('youtube-embed');
|
|
50
|
+
|
|
51
|
+
const iframe = container.querySelector('iframe');
|
|
52
|
+
expect(iframe?.getAttribute('src')).toBe('https://www.youtube.com/embed/dQw4w9WgXcQ');
|
|
53
|
+
expect(iframe?.getAttribute('title')).toBe('Demo video');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('shows a builder placeholder when the url is empty', () => {
|
|
57
|
+
act(() => {
|
|
58
|
+
root.render(
|
|
59
|
+
<YouTubeEmbedBuilderBlock
|
|
60
|
+
block={{
|
|
61
|
+
id: 'youtube-2',
|
|
62
|
+
type: 'youtube-embed',
|
|
63
|
+
settings: {},
|
|
64
|
+
}}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(container.querySelector('iframe')).toBeNull();
|
|
70
|
+
expect(container.textContent).toContain('Paste a YouTube link or video ID');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('renders nothing on live storefront when the url is empty', () => {
|
|
74
|
+
const liveDom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
75
|
+
url: 'https://terminate.myshoppex.io/',
|
|
76
|
+
});
|
|
77
|
+
const liveContainer = liveDom.window.document.createElement('div');
|
|
78
|
+
liveDom.window.document.body.appendChild(liveContainer);
|
|
79
|
+
const liveRoot = createRoot(liveContainer);
|
|
80
|
+
|
|
81
|
+
const previousWindow = globalThis.window;
|
|
82
|
+
const previousDocument = globalThis.document;
|
|
83
|
+
globalThis.window = liveDom.window as unknown as Window & typeof globalThis;
|
|
84
|
+
globalThis.document = liveDom.window.document;
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
liveRoot.render(
|
|
88
|
+
<YouTubeEmbedBuilderBlock
|
|
89
|
+
block={{
|
|
90
|
+
id: 'youtube-3',
|
|
91
|
+
type: 'youtube-embed',
|
|
92
|
+
settings: {},
|
|
93
|
+
}}
|
|
94
|
+
/>,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(liveContainer.querySelector('[data-builder-block="youtube-3"]')).toBeNull();
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
liveRoot.unmount();
|
|
102
|
+
});
|
|
103
|
+
liveContainer.remove();
|
|
104
|
+
globalThis.window = previousWindow;
|
|
105
|
+
globalThis.document = previousDocument;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('renders nothing on live storefront when the url is invalid', () => {
|
|
109
|
+
const liveDom = new JSDOM('<!doctype html><html><body></body></html>', {
|
|
110
|
+
url: 'https://terminate.myshoppex.io/',
|
|
111
|
+
});
|
|
112
|
+
const liveContainer = liveDom.window.document.createElement('div');
|
|
113
|
+
liveDom.window.document.body.appendChild(liveContainer);
|
|
114
|
+
const liveRoot = createRoot(liveContainer);
|
|
115
|
+
|
|
116
|
+
const previousWindow = globalThis.window;
|
|
117
|
+
const previousDocument = globalThis.document;
|
|
118
|
+
globalThis.window = liveDom.window as unknown as Window & typeof globalThis;
|
|
119
|
+
globalThis.document = liveDom.window.document;
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
liveRoot.render(
|
|
123
|
+
<YouTubeEmbedBuilderBlock
|
|
124
|
+
block={{
|
|
125
|
+
id: 'youtube-invalid',
|
|
126
|
+
type: 'youtube-embed',
|
|
127
|
+
settings: {
|
|
128
|
+
videoUrl: 'https://example.com/not-youtube',
|
|
129
|
+
},
|
|
130
|
+
}}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(liveContainer.querySelector('[data-builder-block="youtube-invalid"]')).toBeNull();
|
|
136
|
+
|
|
137
|
+
act(() => {
|
|
138
|
+
liveRoot.unmount();
|
|
139
|
+
});
|
|
140
|
+
liveContainer.remove();
|
|
141
|
+
globalThis.window = previousWindow;
|
|
142
|
+
globalThis.document = previousDocument;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('uses youtube-nocookie when privacyEnhanced is enabled', () => {
|
|
146
|
+
act(() => {
|
|
147
|
+
root.render(
|
|
148
|
+
<YouTubeEmbedBuilderBlock
|
|
149
|
+
block={{
|
|
150
|
+
id: 'youtube-private',
|
|
151
|
+
type: 'youtube-embed',
|
|
152
|
+
settings: {
|
|
153
|
+
videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
154
|
+
privacyEnhanced: true,
|
|
155
|
+
},
|
|
156
|
+
}}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const iframe = container.querySelector('iframe');
|
|
162
|
+
expect(iframe?.getAttribute('src')).toBe(
|
|
163
|
+
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { buildYouTubeEmbedSrc, parseYouTubeVideoId } from './youtube.js';
|
|
3
|
+
|
|
4
|
+
describe('parseYouTubeVideoId', () => {
|
|
5
|
+
test('accepts bare video ids', () => {
|
|
6
|
+
expect(parseYouTubeVideoId('dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('parses watch urls', () => {
|
|
10
|
+
expect(parseYouTubeVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe(
|
|
11
|
+
'dQw4w9WgXcQ',
|
|
12
|
+
);
|
|
13
|
+
expect(parseYouTubeVideoId('youtube.com/watch?v=dQw4w9WgXcQ&t=12')).toBe(
|
|
14
|
+
'dQw4w9WgXcQ',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('parses youtu.be, embed, shorts, and live urls', () => {
|
|
19
|
+
expect(parseYouTubeVideoId('https://youtu.be/dQw4w9WgXcQ')).toBe('dQw4w9WgXcQ');
|
|
20
|
+
expect(parseYouTubeVideoId('https://www.youtube.com/embed/dQw4w9WgXcQ')).toBe(
|
|
21
|
+
'dQw4w9WgXcQ',
|
|
22
|
+
);
|
|
23
|
+
expect(parseYouTubeVideoId('https://www.youtube.com/shorts/dQw4w9WgXcQ')).toBe(
|
|
24
|
+
'dQw4w9WgXcQ',
|
|
25
|
+
);
|
|
26
|
+
expect(parseYouTubeVideoId('https://www.youtube.com/live/dQw4w9WgXcQ')).toBe(
|
|
27
|
+
'dQw4w9WgXcQ',
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('rejects invalid input', () => {
|
|
32
|
+
expect(parseYouTubeVideoId('')).toBeNull();
|
|
33
|
+
expect(parseYouTubeVideoId('not-a-url')).toBeNull();
|
|
34
|
+
expect(parseYouTubeVideoId('https://example.com/watch?v=dQw4w9WgXcQ')).toBeNull();
|
|
35
|
+
expect(parseYouTubeVideoId('https://www.youtube.com/watch?v=tooshort')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('buildYouTubeEmbedSrc', () => {
|
|
40
|
+
test('builds standard and privacy-enhanced embed urls', () => {
|
|
41
|
+
expect(buildYouTubeEmbedSrc('dQw4w9WgXcQ')).toBe(
|
|
42
|
+
'https://www.youtube.com/embed/dQw4w9WgXcQ',
|
|
43
|
+
);
|
|
44
|
+
expect(buildYouTubeEmbedSrc('dQw4w9WgXcQ', { privacyEnhanced: true })).toBe(
|
|
45
|
+
'https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/youtube.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[\w-]{11}$/;
|
|
2
|
+
|
|
3
|
+
function isYouTubeVideoId(value: string): boolean {
|
|
4
|
+
return YOUTUBE_VIDEO_ID_PATTERN.test(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseYouTubeVideoId(input: string): string | null {
|
|
8
|
+
const trimmed = input.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isYouTubeVideoId(trimmed)) {
|
|
14
|
+
return trimmed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const url = trimmed.startsWith('http://') || trimmed.startsWith('https://')
|
|
19
|
+
? new URL(trimmed)
|
|
20
|
+
: new URL(`https://${trimmed}`);
|
|
21
|
+
const host = url.hostname.replace(/^www\./, '');
|
|
22
|
+
|
|
23
|
+
if (host === 'youtu.be') {
|
|
24
|
+
const candidate = url.pathname.split('/').filter(Boolean)[0] ?? '';
|
|
25
|
+
return isYouTubeVideoId(candidate) ? candidate : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com') {
|
|
29
|
+
const watchId = url.searchParams.get('v');
|
|
30
|
+
if (watchId && isYouTubeVideoId(watchId)) {
|
|
31
|
+
return watchId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const embedMatch = url.pathname.match(/^\/embed\/([\w-]{11})/);
|
|
35
|
+
if (embedMatch?.[1] && isYouTubeVideoId(embedMatch[1])) {
|
|
36
|
+
return embedMatch[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const shortsMatch = url.pathname.match(/^\/shorts\/([\w-]{11})/);
|
|
40
|
+
if (shortsMatch?.[1] && isYouTubeVideoId(shortsMatch[1])) {
|
|
41
|
+
return shortsMatch[1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const liveMatch = url.pathname.match(/^\/live\/([\w-]{11})/);
|
|
45
|
+
if (liveMatch?.[1] && isYouTubeVideoId(liveMatch[1])) {
|
|
46
|
+
return liveMatch[1];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildYouTubeEmbedSrc(
|
|
57
|
+
videoId: string,
|
|
58
|
+
options?: { privacyEnhanced?: boolean },
|
|
59
|
+
): string {
|
|
60
|
+
if (!isYouTubeVideoId(videoId)) {
|
|
61
|
+
throw new Error(`Invalid YouTube video id: ${videoId}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const host = options?.privacyEnhanced ? 'www.youtube-nocookie.com' : 'www.youtube.com';
|
|
65
|
+
return `https://${host}/embed/${videoId}`;
|
|
66
|
+
}
|