vidply 1.2.0 → 1.2.1
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.md +946 -946
- package/dist/dev/{vidply.AudioDescriptionManager-R47Q4YR4.js → vidply.AudioDescriptionManager-6RFVF27Z.js} +275 -19
- package/dist/dev/vidply.AudioDescriptionManager-6RFVF27Z.js.map +7 -0
- package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js → vidply.CaptionStyleMenu-R2SCQ6VP.js} +4 -4
- package/dist/dev/{vidply.DASHRenderer-PWEMC3BT.js → vidply.DASHRenderer-EOVEW73H.js} +3 -3
- package/dist/dev/vidply.DASHRenderer-EOVEW73H.js.map +7 -0
- package/dist/dev/{vidply.FloatingPlayerManager-PKNNNAS2.js → vidply.FloatingPlayerManager-V5XQWS75.js} +5 -5
- package/dist/dev/vidply.FloatingPlayerManager-V5XQWS75.js.map +7 -0
- package/dist/dev/{vidply.HLSRenderer-E7RQ74HI.js → vidply.HLSRenderer-ZBEFVH5W.js} +4 -4
- package/dist/dev/vidply.HLSRenderer-ZBEFVH5W.js.map +7 -0
- package/dist/dev/vidply.HTML5Renderer-P7ICKYU4.js +12 -0
- package/dist/dev/{vidply.SignLanguageManager-FET5FQZ7.js → vidply.SignLanguageManager-7IAJOVF6.js} +8 -8
- package/dist/dev/vidply.SignLanguageManager-7IAJOVF6.js.map +7 -0
- package/dist/dev/{vidply.SoundCloudRenderer-RVSJZODO.js → vidply.SoundCloudRenderer-OA7QQYGN.js} +3 -3
- package/dist/dev/vidply.SoundCloudRenderer-OA7QQYGN.js.map +7 -0
- package/dist/dev/{vidply.TranscriptManager-5GXOQCLX.js → vidply.TranscriptManager-BPM3T23L.js} +10 -10
- package/dist/dev/vidply.TranscriptManager-BPM3T23L.js.map +7 -0
- package/dist/dev/{vidply.VimeoRenderer-ER2LSBBW.js → vidply.VimeoRenderer-BRHFYPNJ.js} +3 -3
- package/dist/dev/vidply.VimeoRenderer-BRHFYPNJ.js.map +7 -0
- package/dist/dev/{vidply.YouTubeRenderer-NFTOWJAB.js → vidply.YouTubeRenderer-HRTN4EJ5.js} +3 -3
- package/dist/dev/vidply.YouTubeRenderer-HRTN4EJ5.js.map +7 -0
- package/dist/dev/{vidply.chunk-6RQA4ROD.js → vidply.chunk-5IK3IC5V.js} +2 -2
- package/dist/dev/{vidply.chunk-JJ47NO24.js → vidply.chunk-C55OBT5H.js} +2 -2
- package/dist/dev/vidply.chunk-C55OBT5H.js.map +7 -0
- package/dist/dev/{vidply.chunk-DZSDO3B7.js → vidply.chunk-HOYEMJSD.js} +2 -2
- package/dist/dev/{vidply.chunk-VMSQAZM7.js → vidply.chunk-JAKZNGOR.js} +2 -2
- package/dist/dev/{vidply.chunk-64WXMTGL.js → vidply.chunk-LDVFAFGC.js} +5 -5
- package/dist/dev/vidply.chunk-LDVFAFGC.js.map +7 -0
- package/dist/dev/{vidply.chunk-RZWZGDV7.js → vidply.chunk-NSMAZCMB.js} +6 -6
- package/dist/dev/vidply.chunk-NSMAZCMB.js.map +7 -0
- package/dist/dev/{vidply.chunk-KI7C3QVL.js → vidply.chunk-RQG3MA7W.js} +2 -2
- package/dist/dev/vidply.chunk-RQG3MA7W.js.map +7 -0
- package/dist/dev/{vidply.chunk-BNFIUL2Z.js → vidply.chunk-UHMVDBDB.js} +3 -3
- package/dist/dev/vidply.chunk-UHMVDBDB.js.map +7 -0
- package/dist/dev/{vidply.chunk-NQPURMQY.js → vidply.chunk-W5BR65JY.js} +6 -6
- package/dist/dev/vidply.chunk-W5BR65JY.js.map +7 -0
- package/dist/dev/{vidply.chunk-W25HUCHS.js → vidply.chunk-XAK3DPTO.js} +2 -2
- package/dist/dev/vidply.chunk-XAK3DPTO.js.map +7 -0
- package/dist/dev/{vidply.chunk-SK4V2DAU.js → vidply.chunk-YVCYQZJI.js} +3 -3
- package/dist/dev/{vidply.de-4YAT7FFM.js → vidply.de-5W5LBFSJ.js} +2 -2
- package/dist/dev/vidply.de-5W5LBFSJ.js.map +7 -0
- package/dist/dev/{vidply.es-LEWIRUBH.js → vidply.es-SHDAT6L6.js} +2 -2
- package/dist/dev/vidply.es-SHDAT6L6.js.map +7 -0
- package/dist/dev/vidply.esm.js +73 -34
- package/dist/dev/vidply.esm.js.map +2 -2
- package/dist/dev/{vidply.fr-Q4GV2NOE.js → vidply.fr-2JANOMNB.js} +2 -2
- package/dist/dev/vidply.fr-2JANOMNB.js.map +7 -0
- package/dist/dev/{vidply.ja-S6ITA6WU.js → vidply.ja-WWMSTEJH.js} +2 -2
- package/dist/dev/vidply.ja-WWMSTEJH.js.map +7 -0
- package/dist/legacy/vidply.js +345 -32
- package/dist/legacy/vidply.js.map +3 -3
- package/dist/legacy/vidply.min.js +2 -2
- package/dist/legacy/vidply.min.meta.json +66 -47
- package/dist/prod/vidply.AudioDescriptionManager-YTDVDQBO.min.js +6 -0
- package/dist/prod/{vidply.CaptionStyleMenu-M3KVQWB5.min.js → vidply.CaptionStyleMenu-2AFF3HF2.min.js} +2 -2
- package/dist/prod/{vidply.DASHRenderer-DN3S27AV.min.js → vidply.DASHRenderer-URDOTOKS.min.js} +2 -2
- package/dist/prod/{vidply.FloatingPlayerManager-SKAZ4J43.min.js → vidply.FloatingPlayerManager-QUBFK7CQ.min.js} +2 -2
- package/dist/prod/{vidply.HLSRenderer-62HH4YTK.min.js → vidply.HLSRenderer-46KNUVAB.min.js} +2 -2
- package/dist/prod/vidply.HTML5Renderer-7TQDMIGY.min.js +6 -0
- package/dist/prod/{vidply.SignLanguageManager-V3OBTSSF.min.js → vidply.SignLanguageManager-H4C36EVL.min.js} +2 -2
- package/dist/prod/{vidply.SoundCloudRenderer-CXRVZIAJ.min.js → vidply.SoundCloudRenderer-OYSWMW7D.min.js} +2 -2
- package/dist/prod/{vidply.TranscriptManager-SYM3DFUY.min.js → vidply.TranscriptManager-BA3DAS63.min.js} +2 -2
- package/dist/prod/{vidply.VimeoRenderer-72TWRYFQ.min.js → vidply.VimeoRenderer-NF3COTHU.min.js} +2 -2
- package/dist/prod/{vidply.YouTubeRenderer-UMHEV3KD.min.js → vidply.YouTubeRenderer-UCQKYIJ4.min.js} +2 -2
- package/dist/prod/{vidply.chunk-Z3YDSA7O.min.js → vidply.chunk-2HNCP64N.min.js} +1 -1
- package/dist/prod/{vidply.chunk-EKEYJWMG.min.js → vidply.chunk-6ECBIYVI.min.js} +2 -2
- package/dist/prod/{vidply.chunk-YEENJS3Q.min.js → vidply.chunk-7S5AFTST.min.js} +2 -2
- package/dist/prod/{vidply.chunk-P4UXOHWA.min.js → vidply.chunk-EUOA2DJK.min.js} +1 -1
- package/dist/prod/{vidply.chunk-2TB6MUHO.min.js → vidply.chunk-G6KGXNEW.min.js} +1 -1
- package/dist/prod/{vidply.chunk-ABPXVDLZ.min.js → vidply.chunk-JPMAK3NS.min.js} +1 -1
- package/dist/prod/{vidply.chunk-P3PQBY7N.min.js → vidply.chunk-L4O44K63.min.js} +1 -1
- package/dist/prod/{vidply.chunk-WX3MVEAL.min.js → vidply.chunk-M76ABMKB.min.js} +1 -1
- package/dist/prod/{vidply.chunk-TM6AXJVD.min.js → vidply.chunk-WRVDVD33.min.js} +2 -2
- package/dist/prod/{vidply.chunk-RI3X7WHK.min.js → vidply.chunk-X2FQEQYR.min.js} +2 -2
- package/dist/prod/{vidply.chunk-UK7YGOB5.min.js → vidply.chunk-XXUH7ADU.min.js} +2 -2
- package/dist/prod/{vidply.de-BCHXRQ53.min.js → vidply.de-JMCATTKZ.min.js} +1 -1
- package/dist/prod/{vidply.es-PICAKMZD.min.js → vidply.es-YVG37V7Z.min.js} +1 -1
- package/dist/prod/vidply.esm.min.js +2 -2
- package/dist/prod/{vidply.fr-ZILI7CDR.min.js → vidply.fr-OGKGN2R4.min.js} +1 -1
- package/dist/prod/{vidply.ja-5WQ4OK4J.min.js → vidply.ja-Z6DS7QLS.min.js} +1 -1
- package/dist/vidply.css +5796 -5796
- package/dist/vidply.esm.min.meta.json +148 -135
- package/package.json +7 -7
- package/src/controls/CaptionManager.ts +598 -598
- package/src/controls/ControlBar.ts +4159 -4159
- package/src/controls/KeyboardManager.ts +383 -383
- package/src/controls/SettingsDialog.ts +488 -488
- package/src/controls/TranscriptManager.ts +2426 -2426
- package/src/core/AudioDescriptionManager.ts +863 -774
- package/src/core/DescriptionSpeechManager.ts +232 -0
- package/src/core/FloatingPlayerManager.ts +674 -674
- package/src/core/LazyInit.ts +80 -80
- package/src/core/MetadataAlertsManager.ts +514 -514
- package/src/core/Player.ts +2915 -2872
- package/src/core/PosterManager.ts +169 -169
- package/src/core/PseudoFullscreen.ts +179 -179
- package/src/core/ResponsiveManager.ts +231 -231
- package/src/core/ResumeManager.ts +236 -236
- package/src/core/SignLanguageManager.ts +1308 -1308
- package/src/core/ThemeManager.ts +157 -157
- package/src/features/PlaylistManager.ts +1838 -1827
- package/src/i18n/i18n.ts +223 -223
- package/src/i18n/languages/de.ts +217 -217
- package/src/i18n/languages/en.ts +218 -218
- package/src/i18n/languages/es.ts +214 -214
- package/src/i18n/languages/fr.ts +214 -214
- package/src/i18n/languages/ja.ts +214 -214
- package/src/icons/Icons.ts +167 -167
- package/src/index.ts +196 -193
- package/src/renderers/DASHRenderer.ts +1077 -1077
- package/src/renderers/HLSRenderer.ts +944 -944
- package/src/renderers/HTML5Renderer.ts +389 -389
- package/src/renderers/SoundCloudRenderer.ts +437 -437
- package/src/renderers/VimeoRenderer.ts +298 -298
- package/src/renderers/YouTubeRenderer.ts +290 -290
- package/src/styles/vidply.css +5796 -5796
- package/src/types/events.ts +124 -122
- package/src/types/globals.d.ts +313 -313
- package/src/types/options.ts +185 -182
- package/src/types/renderer.ts +58 -58
- package/src/utils/DOMUtils.ts +281 -281
- package/src/utils/DownloadInfo.ts +171 -171
- package/src/utils/DraggablePanel.ts +539 -539
- package/src/utils/DraggablePanelMenu.ts +195 -195
- package/src/utils/DraggableResizable.ts +997 -997
- package/src/utils/EventEmitter.ts +59 -59
- package/src/utils/FormUtils.ts +95 -95
- package/src/utils/MenuFactory.ts +483 -483
- package/src/utils/Sanitize.ts +70 -70
- package/src/utils/SettingsMenuFactory.ts +289 -289
- package/src/utils/StorageManager.ts +287 -287
- package/src/utils/TrackLabelUtils.ts +52 -52
- package/src/utils/UrlSafe.ts +69 -69
- package/src/utils/VideoFrameCapture.ts +114 -114
- package/src/utils/WindowComponents.ts +250 -250
- package/dist/dev/vidply.AudioDescriptionManager-R47Q4YR4.js.map +0 -7
- package/dist/dev/vidply.DASHRenderer-PWEMC3BT.js.map +0 -7
- package/dist/dev/vidply.FloatingPlayerManager-PKNNNAS2.js.map +0 -7
- package/dist/dev/vidply.HLSRenderer-E7RQ74HI.js.map +0 -7
- package/dist/dev/vidply.HTML5Renderer-BU4QJJJY.js +0 -12
- package/dist/dev/vidply.SignLanguageManager-FET5FQZ7.js.map +0 -7
- package/dist/dev/vidply.SoundCloudRenderer-RVSJZODO.js.map +0 -7
- package/dist/dev/vidply.TranscriptManager-5GXOQCLX.js.map +0 -7
- package/dist/dev/vidply.VimeoRenderer-ER2LSBBW.js.map +0 -7
- package/dist/dev/vidply.YouTubeRenderer-NFTOWJAB.js.map +0 -7
- package/dist/dev/vidply.chunk-64WXMTGL.js.map +0 -7
- package/dist/dev/vidply.chunk-BNFIUL2Z.js.map +0 -7
- package/dist/dev/vidply.chunk-JJ47NO24.js.map +0 -7
- package/dist/dev/vidply.chunk-KI7C3QVL.js.map +0 -7
- package/dist/dev/vidply.chunk-NQPURMQY.js.map +0 -7
- package/dist/dev/vidply.chunk-RZWZGDV7.js.map +0 -7
- package/dist/dev/vidply.chunk-W25HUCHS.js.map +0 -7
- package/dist/dev/vidply.de-4YAT7FFM.js.map +0 -7
- package/dist/dev/vidply.es-LEWIRUBH.js.map +0 -7
- package/dist/dev/vidply.fr-Q4GV2NOE.js.map +0 -7
- package/dist/dev/vidply.ja-S6ITA6WU.js.map +0 -7
- package/dist/prod/vidply.AudioDescriptionManager-RHLIJW5C.min.js +0 -6
- package/dist/prod/vidply.HTML5Renderer-IU5T3WEW.min.js +0 -6
- /package/dist/dev/{vidply.CaptionStyleMenu-7XHRBWUM.js.map → vidply.CaptionStyleMenu-R2SCQ6VP.js.map} +0 -0
- /package/dist/dev/{vidply.HTML5Renderer-BU4QJJJY.js.map → vidply.HTML5Renderer-P7ICKYU4.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-6RQA4ROD.js.map → vidply.chunk-5IK3IC5V.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-DZSDO3B7.js.map → vidply.chunk-HOYEMJSD.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-VMSQAZM7.js.map → vidply.chunk-JAKZNGOR.js.map} +0 -0
- /package/dist/dev/{vidply.chunk-SK4V2DAU.js.map → vidply.chunk-YVCYQZJI.js.map} +0 -0
package/src/utils/DOMUtils.ts
CHANGED
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Options accepted by {@link DOMUtils.createElement}.
|
|
3
|
-
*
|
|
4
|
-
* `innerHTML` is deliberately **not** exposed here. Code that needs to
|
|
5
|
-
* inject raw markup must do so explicitly at the call site where the
|
|
6
|
-
* source of the markup is auditable; DOMUtils is the "safe by default"
|
|
7
|
-
* surface.
|
|
8
|
-
*/
|
|
9
|
-
export interface CreateElementOptions {
|
|
10
|
-
className?: string;
|
|
11
|
-
attributes?: Record<string, string | undefined>;
|
|
12
|
-
textContent?: string;
|
|
13
|
-
style?: Partial<CSSStyleDeclaration>;
|
|
14
|
-
children?: (Node | null | undefined)[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createElementImpl<K extends keyof HTMLElementTagNameMap>(
|
|
18
|
-
tag: K,
|
|
19
|
-
options?: CreateElementOptions
|
|
20
|
-
): HTMLElementTagNameMap[K];
|
|
21
|
-
function createElementImpl(tag: string, options?: CreateElementOptions): HTMLElement;
|
|
22
|
-
function createElementImpl(tag: string, options: CreateElementOptions = {}): HTMLElement {
|
|
23
|
-
const element = document.createElement(tag);
|
|
24
|
-
|
|
25
|
-
if (options.className) {
|
|
26
|
-
element.className = options.className;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (options.attributes) {
|
|
30
|
-
for (const [key, value] of Object.entries(options.attributes)) {
|
|
31
|
-
if (value !== undefined) {
|
|
32
|
-
element.setAttribute(key, value);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (options.textContent) {
|
|
38
|
-
element.textContent = options.textContent;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (options.style) {
|
|
42
|
-
Object.assign(element.style, options.style);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (options.children) {
|
|
46
|
-
for (const child of options.children) {
|
|
47
|
-
if (child) element.appendChild(child);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return element;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const DOMUtils = {
|
|
55
|
-
createElement: createElementImpl,
|
|
56
|
-
|
|
57
|
-
show(element: HTMLElement | null | undefined): void {
|
|
58
|
-
if (element?.style) {
|
|
59
|
-
element.style.display = '';
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
hide(element: HTMLElement | null | undefined): void {
|
|
64
|
-
if (element?.style) {
|
|
65
|
-
element.style.display = 'none';
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
fadeIn(element: HTMLElement | null, duration = 300, onComplete?: () => void): void {
|
|
70
|
-
if (!element) return;
|
|
71
|
-
|
|
72
|
-
element.style.opacity = '0';
|
|
73
|
-
element.style.display = '';
|
|
74
|
-
element.style.transition = `opacity ${duration}ms ease`;
|
|
75
|
-
|
|
76
|
-
// Force reflow to ensure transition works
|
|
77
|
-
void element.offsetHeight;
|
|
78
|
-
|
|
79
|
-
element.style.opacity = '1';
|
|
80
|
-
|
|
81
|
-
if (onComplete) {
|
|
82
|
-
let called = false;
|
|
83
|
-
const cleanup = () => {
|
|
84
|
-
if (called) return;
|
|
85
|
-
called = true;
|
|
86
|
-
element.removeEventListener('transitionend', cleanup);
|
|
87
|
-
onComplete();
|
|
88
|
-
};
|
|
89
|
-
element.addEventListener('transitionend', cleanup, { once: true });
|
|
90
|
-
setTimeout(cleanup, duration + 50);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
fadeOut(element: HTMLElement | null, duration = 300, onComplete?: () => void): void {
|
|
95
|
-
if (!element) return;
|
|
96
|
-
|
|
97
|
-
element.style.transition = `opacity ${duration}ms ease`;
|
|
98
|
-
element.style.opacity = '0';
|
|
99
|
-
|
|
100
|
-
let called = false;
|
|
101
|
-
const cleanup = () => {
|
|
102
|
-
if (called) return;
|
|
103
|
-
called = true;
|
|
104
|
-
element.removeEventListener('transitionend', cleanup);
|
|
105
|
-
element.style.display = 'none';
|
|
106
|
-
if (onComplete) onComplete();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
element.addEventListener('transitionend', cleanup, { once: true });
|
|
110
|
-
setTimeout(cleanup, duration + 50);
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
offset(element: HTMLElement | null): { top: number; left: number; width: number; height: number } {
|
|
114
|
-
if (!element) return { top: 0, left: 0, width: 0, height: 0 };
|
|
115
|
-
|
|
116
|
-
const rect = element.getBoundingClientRect();
|
|
117
|
-
return {
|
|
118
|
-
top: rect.top + window.scrollY,
|
|
119
|
-
left: rect.left + window.scrollX,
|
|
120
|
-
width: rect.width,
|
|
121
|
-
height: rect.height
|
|
122
|
-
};
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
escapeHTML(str: string): string {
|
|
126
|
-
const escapeMap: Record<string, string> = {
|
|
127
|
-
'&': '&',
|
|
128
|
-
'<': '<',
|
|
129
|
-
'>': '>',
|
|
130
|
-
'"': '"',
|
|
131
|
-
"'": '''
|
|
132
|
-
};
|
|
133
|
-
return str.replace(/[&<>"']/g, char => escapeMap[char] ?? char);
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Render a WebVTT cue's text safely.
|
|
138
|
-
*
|
|
139
|
-
* The previous implementation ran a regex-based blacklist over the cue
|
|
140
|
-
* string and assigned the result to `innerHTML`, which is a known-unsafe
|
|
141
|
-
* pattern (mutation-XSS bypasses, attribute-name tricks, etc.). Caption
|
|
142
|
-
* text on most sites is fetched verbatim from external `.vtt` files that
|
|
143
|
-
* the embedding page has no control over (third-party HLS/DASH manifests,
|
|
144
|
-
* user-supplied playlists, ...) so this code path is reachable by
|
|
145
|
-
* untrusted authors.
|
|
146
|
-
*
|
|
147
|
-
* The new implementation tokenizes only the WebVTT inline tags allowed by
|
|
148
|
-
* the spec (`<b>`, `<i>`, `<u>`, `<c[.class]>`, `<v authorName>`) and
|
|
149
|
-
* builds the resulting DOM via `document.createElement` /
|
|
150
|
-
* `document.createTextNode`. Anything else (script, iframe, attributes,
|
|
151
|
-
* URL schemes, character refs, ...) is rendered as literal text.
|
|
152
|
-
*
|
|
153
|
-
* Cue input is hard-capped at 10,000 characters before parsing to
|
|
154
|
-
* eliminate ReDoS and runaway-DOM concerns.
|
|
155
|
-
*/
|
|
156
|
-
renderVTTToDOM(text: string): DocumentFragment {
|
|
157
|
-
const MAX_CUE_LENGTH = 10_000;
|
|
158
|
-
const safeInput = text.length > MAX_CUE_LENGTH ? text.slice(0, MAX_CUE_LENGTH) : text;
|
|
159
|
-
const fragment = document.createDocumentFragment();
|
|
160
|
-
const stack: HTMLElement[] = [];
|
|
161
|
-
|
|
162
|
-
const append = (node: Node): void => {
|
|
163
|
-
const target = stack[stack.length - 1] ?? fragment;
|
|
164
|
-
target.appendChild(node);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const tagPattern = /<(\/)?([a-z])(?:\.([\w.-]{1,200}))?(?:\s+([^<>]{0,500}))?>/i;
|
|
168
|
-
let cursor = 0;
|
|
169
|
-
|
|
170
|
-
while (cursor < safeInput.length) {
|
|
171
|
-
const remaining = safeInput.slice(cursor);
|
|
172
|
-
const match = tagPattern.exec(remaining);
|
|
173
|
-
if (!match || match.index === undefined) {
|
|
174
|
-
append(document.createTextNode(remaining));
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (match.index > 0) {
|
|
179
|
-
append(document.createTextNode(remaining.slice(0, match.index)));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const [, closing, tagLetter, classList, voiceName] = match;
|
|
183
|
-
const tag = (tagLetter || '').toLowerCase();
|
|
184
|
-
|
|
185
|
-
if (closing) {
|
|
186
|
-
const top = stack[stack.length - 1];
|
|
187
|
-
if (top && top.dataset.vttTag === tag) {
|
|
188
|
-
stack.pop();
|
|
189
|
-
}
|
|
190
|
-
} else if (tag === 'b' || tag === 'i' || tag === 'u') {
|
|
191
|
-
const elementTag = tag === 'b' ? 'strong' : tag === 'i' ? 'em' : 'u';
|
|
192
|
-
const node = document.createElement(elementTag);
|
|
193
|
-
node.dataset.vttTag = tag;
|
|
194
|
-
append(node);
|
|
195
|
-
stack.push(node);
|
|
196
|
-
} else if (tag === 'c') {
|
|
197
|
-
const span = document.createElement('span');
|
|
198
|
-
span.dataset.vttTag = tag;
|
|
199
|
-
span.classList.add('caption-class');
|
|
200
|
-
if (classList) {
|
|
201
|
-
for (const cls of classList.split('.').filter(Boolean)) {
|
|
202
|
-
if (/^[\w-]+$/.test(cls)) {
|
|
203
|
-
span.classList.add(`caption-class-${cls}`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
append(span);
|
|
208
|
-
stack.push(span);
|
|
209
|
-
} else if (tag === 'v') {
|
|
210
|
-
const span = document.createElement('span');
|
|
211
|
-
span.dataset.vttTag = tag;
|
|
212
|
-
span.classList.add('caption-voice');
|
|
213
|
-
if (voiceName) {
|
|
214
|
-
// Voice name is text; never an attribute. data-voice mirrors the
|
|
215
|
-
// historical hook used by `caption-voice` styles.
|
|
216
|
-
span.dataset.voice = voiceName.trim().slice(0, 200);
|
|
217
|
-
}
|
|
218
|
-
append(span);
|
|
219
|
-
stack.push(span);
|
|
220
|
-
} else {
|
|
221
|
-
// Unknown tag — render literally as escaped text so authors can see
|
|
222
|
-
// the bug without it becoming an injection vector.
|
|
223
|
-
append(document.createTextNode(match[0]));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
cursor += match.index + match[0].length;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return fragment;
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
createTooltip(text: string, classPrefix = 'vidply'): HTMLElement {
|
|
233
|
-
return this.createElement('span', {
|
|
234
|
-
className: `${classPrefix}-tooltip`,
|
|
235
|
-
textContent: text,
|
|
236
|
-
attributes: { 'aria-hidden': 'true' }
|
|
237
|
-
});
|
|
238
|
-
},
|
|
239
|
-
|
|
240
|
-
attachTooltip(element: HTMLElement | null, text: string, classPrefix = 'vidply'): void {
|
|
241
|
-
if (!element || !text) return;
|
|
242
|
-
|
|
243
|
-
element.querySelector(`.${classPrefix}-tooltip`)?.remove();
|
|
244
|
-
|
|
245
|
-
const tooltip = this.createTooltip(text, classPrefix);
|
|
246
|
-
element.appendChild(tooltip);
|
|
247
|
-
|
|
248
|
-
const visibleClass = `${classPrefix}-tooltip-visible`;
|
|
249
|
-
const show = () => tooltip.classList.add(visibleClass);
|
|
250
|
-
const hide = () => tooltip.classList.remove(visibleClass);
|
|
251
|
-
|
|
252
|
-
element.addEventListener('mouseenter', show);
|
|
253
|
-
element.addEventListener('mouseleave', hide);
|
|
254
|
-
element.addEventListener('focus', show);
|
|
255
|
-
element.addEventListener('blur', hide);
|
|
256
|
-
},
|
|
257
|
-
|
|
258
|
-
createButtonText(text: string, classPrefix = 'vidply'): HTMLElement {
|
|
259
|
-
return this.createElement('span', {
|
|
260
|
-
className: `${classPrefix}-button-text`,
|
|
261
|
-
textContent: text,
|
|
262
|
-
attributes: { 'aria-hidden': 'true' }
|
|
263
|
-
});
|
|
264
|
-
},
|
|
265
|
-
|
|
266
|
-
addClass(element: HTMLElement | null | undefined, className: string): void {
|
|
267
|
-
element?.classList?.add(className);
|
|
268
|
-
},
|
|
269
|
-
|
|
270
|
-
removeClass(element: HTMLElement | null | undefined, className: string): void {
|
|
271
|
-
element?.classList?.remove(className);
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
toggleClass(element: HTMLElement | null | undefined, className: string): void {
|
|
275
|
-
element?.classList?.toggle(className);
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
hasClass(element: HTMLElement | null | undefined, className: string): boolean {
|
|
279
|
-
return element?.classList?.contains(className) ?? false;
|
|
280
|
-
}
|
|
281
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Options accepted by {@link DOMUtils.createElement}.
|
|
3
|
+
*
|
|
4
|
+
* `innerHTML` is deliberately **not** exposed here. Code that needs to
|
|
5
|
+
* inject raw markup must do so explicitly at the call site where the
|
|
6
|
+
* source of the markup is auditable; DOMUtils is the "safe by default"
|
|
7
|
+
* surface.
|
|
8
|
+
*/
|
|
9
|
+
export interface CreateElementOptions {
|
|
10
|
+
className?: string;
|
|
11
|
+
attributes?: Record<string, string | undefined>;
|
|
12
|
+
textContent?: string;
|
|
13
|
+
style?: Partial<CSSStyleDeclaration>;
|
|
14
|
+
children?: (Node | null | undefined)[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createElementImpl<K extends keyof HTMLElementTagNameMap>(
|
|
18
|
+
tag: K,
|
|
19
|
+
options?: CreateElementOptions
|
|
20
|
+
): HTMLElementTagNameMap[K];
|
|
21
|
+
function createElementImpl(tag: string, options?: CreateElementOptions): HTMLElement;
|
|
22
|
+
function createElementImpl(tag: string, options: CreateElementOptions = {}): HTMLElement {
|
|
23
|
+
const element = document.createElement(tag);
|
|
24
|
+
|
|
25
|
+
if (options.className) {
|
|
26
|
+
element.className = options.className;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.attributes) {
|
|
30
|
+
for (const [key, value] of Object.entries(options.attributes)) {
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
element.setAttribute(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (options.textContent) {
|
|
38
|
+
element.textContent = options.textContent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (options.style) {
|
|
42
|
+
Object.assign(element.style, options.style);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.children) {
|
|
46
|
+
for (const child of options.children) {
|
|
47
|
+
if (child) element.appendChild(child);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return element;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DOMUtils = {
|
|
55
|
+
createElement: createElementImpl,
|
|
56
|
+
|
|
57
|
+
show(element: HTMLElement | null | undefined): void {
|
|
58
|
+
if (element?.style) {
|
|
59
|
+
element.style.display = '';
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
hide(element: HTMLElement | null | undefined): void {
|
|
64
|
+
if (element?.style) {
|
|
65
|
+
element.style.display = 'none';
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
fadeIn(element: HTMLElement | null, duration = 300, onComplete?: () => void): void {
|
|
70
|
+
if (!element) return;
|
|
71
|
+
|
|
72
|
+
element.style.opacity = '0';
|
|
73
|
+
element.style.display = '';
|
|
74
|
+
element.style.transition = `opacity ${duration}ms ease`;
|
|
75
|
+
|
|
76
|
+
// Force reflow to ensure transition works
|
|
77
|
+
void element.offsetHeight;
|
|
78
|
+
|
|
79
|
+
element.style.opacity = '1';
|
|
80
|
+
|
|
81
|
+
if (onComplete) {
|
|
82
|
+
let called = false;
|
|
83
|
+
const cleanup = () => {
|
|
84
|
+
if (called) return;
|
|
85
|
+
called = true;
|
|
86
|
+
element.removeEventListener('transitionend', cleanup);
|
|
87
|
+
onComplete();
|
|
88
|
+
};
|
|
89
|
+
element.addEventListener('transitionend', cleanup, { once: true });
|
|
90
|
+
setTimeout(cleanup, duration + 50);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
fadeOut(element: HTMLElement | null, duration = 300, onComplete?: () => void): void {
|
|
95
|
+
if (!element) return;
|
|
96
|
+
|
|
97
|
+
element.style.transition = `opacity ${duration}ms ease`;
|
|
98
|
+
element.style.opacity = '0';
|
|
99
|
+
|
|
100
|
+
let called = false;
|
|
101
|
+
const cleanup = () => {
|
|
102
|
+
if (called) return;
|
|
103
|
+
called = true;
|
|
104
|
+
element.removeEventListener('transitionend', cleanup);
|
|
105
|
+
element.style.display = 'none';
|
|
106
|
+
if (onComplete) onComplete();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
element.addEventListener('transitionend', cleanup, { once: true });
|
|
110
|
+
setTimeout(cleanup, duration + 50);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
offset(element: HTMLElement | null): { top: number; left: number; width: number; height: number } {
|
|
114
|
+
if (!element) return { top: 0, left: 0, width: 0, height: 0 };
|
|
115
|
+
|
|
116
|
+
const rect = element.getBoundingClientRect();
|
|
117
|
+
return {
|
|
118
|
+
top: rect.top + window.scrollY,
|
|
119
|
+
left: rect.left + window.scrollX,
|
|
120
|
+
width: rect.width,
|
|
121
|
+
height: rect.height
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
escapeHTML(str: string): string {
|
|
126
|
+
const escapeMap: Record<string, string> = {
|
|
127
|
+
'&': '&',
|
|
128
|
+
'<': '<',
|
|
129
|
+
'>': '>',
|
|
130
|
+
'"': '"',
|
|
131
|
+
"'": '''
|
|
132
|
+
};
|
|
133
|
+
return str.replace(/[&<>"']/g, char => escapeMap[char] ?? char);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Render a WebVTT cue's text safely.
|
|
138
|
+
*
|
|
139
|
+
* The previous implementation ran a regex-based blacklist over the cue
|
|
140
|
+
* string and assigned the result to `innerHTML`, which is a known-unsafe
|
|
141
|
+
* pattern (mutation-XSS bypasses, attribute-name tricks, etc.). Caption
|
|
142
|
+
* text on most sites is fetched verbatim from external `.vtt` files that
|
|
143
|
+
* the embedding page has no control over (third-party HLS/DASH manifests,
|
|
144
|
+
* user-supplied playlists, ...) so this code path is reachable by
|
|
145
|
+
* untrusted authors.
|
|
146
|
+
*
|
|
147
|
+
* The new implementation tokenizes only the WebVTT inline tags allowed by
|
|
148
|
+
* the spec (`<b>`, `<i>`, `<u>`, `<c[.class]>`, `<v authorName>`) and
|
|
149
|
+
* builds the resulting DOM via `document.createElement` /
|
|
150
|
+
* `document.createTextNode`. Anything else (script, iframe, attributes,
|
|
151
|
+
* URL schemes, character refs, ...) is rendered as literal text.
|
|
152
|
+
*
|
|
153
|
+
* Cue input is hard-capped at 10,000 characters before parsing to
|
|
154
|
+
* eliminate ReDoS and runaway-DOM concerns.
|
|
155
|
+
*/
|
|
156
|
+
renderVTTToDOM(text: string): DocumentFragment {
|
|
157
|
+
const MAX_CUE_LENGTH = 10_000;
|
|
158
|
+
const safeInput = text.length > MAX_CUE_LENGTH ? text.slice(0, MAX_CUE_LENGTH) : text;
|
|
159
|
+
const fragment = document.createDocumentFragment();
|
|
160
|
+
const stack: HTMLElement[] = [];
|
|
161
|
+
|
|
162
|
+
const append = (node: Node): void => {
|
|
163
|
+
const target = stack[stack.length - 1] ?? fragment;
|
|
164
|
+
target.appendChild(node);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const tagPattern = /<(\/)?([a-z])(?:\.([\w.-]{1,200}))?(?:\s+([^<>]{0,500}))?>/i;
|
|
168
|
+
let cursor = 0;
|
|
169
|
+
|
|
170
|
+
while (cursor < safeInput.length) {
|
|
171
|
+
const remaining = safeInput.slice(cursor);
|
|
172
|
+
const match = tagPattern.exec(remaining);
|
|
173
|
+
if (!match || match.index === undefined) {
|
|
174
|
+
append(document.createTextNode(remaining));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (match.index > 0) {
|
|
179
|
+
append(document.createTextNode(remaining.slice(0, match.index)));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const [, closing, tagLetter, classList, voiceName] = match;
|
|
183
|
+
const tag = (tagLetter || '').toLowerCase();
|
|
184
|
+
|
|
185
|
+
if (closing) {
|
|
186
|
+
const top = stack[stack.length - 1];
|
|
187
|
+
if (top && top.dataset.vttTag === tag) {
|
|
188
|
+
stack.pop();
|
|
189
|
+
}
|
|
190
|
+
} else if (tag === 'b' || tag === 'i' || tag === 'u') {
|
|
191
|
+
const elementTag = tag === 'b' ? 'strong' : tag === 'i' ? 'em' : 'u';
|
|
192
|
+
const node = document.createElement(elementTag);
|
|
193
|
+
node.dataset.vttTag = tag;
|
|
194
|
+
append(node);
|
|
195
|
+
stack.push(node);
|
|
196
|
+
} else if (tag === 'c') {
|
|
197
|
+
const span = document.createElement('span');
|
|
198
|
+
span.dataset.vttTag = tag;
|
|
199
|
+
span.classList.add('caption-class');
|
|
200
|
+
if (classList) {
|
|
201
|
+
for (const cls of classList.split('.').filter(Boolean)) {
|
|
202
|
+
if (/^[\w-]+$/.test(cls)) {
|
|
203
|
+
span.classList.add(`caption-class-${cls}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
append(span);
|
|
208
|
+
stack.push(span);
|
|
209
|
+
} else if (tag === 'v') {
|
|
210
|
+
const span = document.createElement('span');
|
|
211
|
+
span.dataset.vttTag = tag;
|
|
212
|
+
span.classList.add('caption-voice');
|
|
213
|
+
if (voiceName) {
|
|
214
|
+
// Voice name is text; never an attribute. data-voice mirrors the
|
|
215
|
+
// historical hook used by `caption-voice` styles.
|
|
216
|
+
span.dataset.voice = voiceName.trim().slice(0, 200);
|
|
217
|
+
}
|
|
218
|
+
append(span);
|
|
219
|
+
stack.push(span);
|
|
220
|
+
} else {
|
|
221
|
+
// Unknown tag — render literally as escaped text so authors can see
|
|
222
|
+
// the bug without it becoming an injection vector.
|
|
223
|
+
append(document.createTextNode(match[0]));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cursor += match.index + match[0].length;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return fragment;
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
createTooltip(text: string, classPrefix = 'vidply'): HTMLElement {
|
|
233
|
+
return this.createElement('span', {
|
|
234
|
+
className: `${classPrefix}-tooltip`,
|
|
235
|
+
textContent: text,
|
|
236
|
+
attributes: { 'aria-hidden': 'true' }
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
attachTooltip(element: HTMLElement | null, text: string, classPrefix = 'vidply'): void {
|
|
241
|
+
if (!element || !text) return;
|
|
242
|
+
|
|
243
|
+
element.querySelector(`.${classPrefix}-tooltip`)?.remove();
|
|
244
|
+
|
|
245
|
+
const tooltip = this.createTooltip(text, classPrefix);
|
|
246
|
+
element.appendChild(tooltip);
|
|
247
|
+
|
|
248
|
+
const visibleClass = `${classPrefix}-tooltip-visible`;
|
|
249
|
+
const show = () => tooltip.classList.add(visibleClass);
|
|
250
|
+
const hide = () => tooltip.classList.remove(visibleClass);
|
|
251
|
+
|
|
252
|
+
element.addEventListener('mouseenter', show);
|
|
253
|
+
element.addEventListener('mouseleave', hide);
|
|
254
|
+
element.addEventListener('focus', show);
|
|
255
|
+
element.addEventListener('blur', hide);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
createButtonText(text: string, classPrefix = 'vidply'): HTMLElement {
|
|
259
|
+
return this.createElement('span', {
|
|
260
|
+
className: `${classPrefix}-button-text`,
|
|
261
|
+
textContent: text,
|
|
262
|
+
attributes: { 'aria-hidden': 'true' }
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
addClass(element: HTMLElement | null | undefined, className: string): void {
|
|
267
|
+
element?.classList?.add(className);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
removeClass(element: HTMLElement | null | undefined, className: string): void {
|
|
271
|
+
element?.classList?.remove(className);
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
toggleClass(element: HTMLElement | null | undefined, className: string): void {
|
|
275
|
+
element?.classList?.toggle(className);
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
hasClass(element: HTMLElement | null | undefined, className: string): boolean {
|
|
279
|
+
return element?.classList?.contains(className) ?? false;
|
|
280
|
+
}
|
|
281
|
+
};
|