@brett_lamy/docstream 0.1.0 → 0.3.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 +215 -0
- package/package.json +34 -3
- package/src/docs/DocsRenderer.tsx +30 -2
- package/src/gitbook/ast.ts +4 -0
- package/src/gitbook/index.ts +6 -0
- package/src/gitbook/parse.ts +33 -5
- package/src/gitbook/serialize.ts +6 -2
- package/src/index.ts +28 -0
- package/src/playground/ReactCodePreview.tsx +161 -0
- package/src/playground/filesystem.ts +202 -0
- package/src/playground/index.ts +19 -0
- package/src/replay/ReplayPreview.tsx +398 -0
- package/src/replay/index.ts +10 -0
- package/src/replay/sanitize.ts +83 -0
- package/src/replay/styles.css +87 -0
- package/src/replay/url.ts +41 -0
- package/src/styles.css +108 -21
- package/src/video/VideoEmbed.tsx +62 -0
- package/src/video/index.ts +2 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { EventType, IncrementalSource } from 'rrweb';
|
|
2
|
+
import type { eventWithTime } from 'rrweb';
|
|
3
|
+
|
|
4
|
+
const TRANSPARENT_PIXEL =
|
|
5
|
+
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
6
|
+
const EMPTY_STYLESHEET = 'data:text/css,';
|
|
7
|
+
|
|
8
|
+
function isLocalNetworkUrl(value: string): boolean {
|
|
9
|
+
let url: URL;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
|
|
16
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
17
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) return true;
|
|
18
|
+
if (host === '0.0.0.0' || host === '::1' || host.startsWith('127.') || host.startsWith('10.')) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (host.startsWith('192.168.')) return true;
|
|
22
|
+
const private172 = host.match(/^172\.(\d{1,3})\./);
|
|
23
|
+
if (private172 && Number(private172[1]) >= 16 && Number(private172[1]) <= 31) return true;
|
|
24
|
+
return host.includes(':') && (host.startsWith('fc') || host.startsWith('fd'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sanitizeCss(value: string): string {
|
|
28
|
+
return value.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (whole, quote: string, raw: string) =>
|
|
29
|
+
isLocalNetworkUrl(raw.trim()) ? `url(${quote}${TRANSPARENT_PIXEL}${quote})` : whole,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeAttributes(attributes: Record<string, unknown>): void {
|
|
34
|
+
for (const key of Object.keys(attributes)) {
|
|
35
|
+
const value = attributes[key];
|
|
36
|
+
if (typeof value !== 'string') continue;
|
|
37
|
+
const lower = key.toLowerCase();
|
|
38
|
+
if (lower === 'src' || lower === 'poster' || lower === 'background') {
|
|
39
|
+
if (isLocalNetworkUrl(value)) attributes[key] = TRANSPARENT_PIXEL;
|
|
40
|
+
} else if (lower === 'data') {
|
|
41
|
+
if (isLocalNetworkUrl(value)) attributes[key] = 'about:blank';
|
|
42
|
+
} else if (lower === 'style' || lower === '_csstext') {
|
|
43
|
+
attributes[key] = sanitizeCss(value);
|
|
44
|
+
} else if (lower === 'href' || lower === 'xlink:href') {
|
|
45
|
+
if (isLocalNetworkUrl(value)) attributes[key] = EMPTY_STYLESHEET;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function walkNode(node: unknown): void {
|
|
51
|
+
if (!node || typeof node !== 'object') return;
|
|
52
|
+
const value = node as { attributes?: unknown; childNodes?: unknown };
|
|
53
|
+
if (value.attributes && typeof value.attributes === 'object') {
|
|
54
|
+
sanitizeAttributes(value.attributes as Record<string, unknown>);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value.childNodes)) value.childNodes.forEach(walkNode);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Prevent a replay iframe from making the viewer fetch localhost/private assets. */
|
|
60
|
+
export function sanitizeReplayEvent(event: eventWithTime): eventWithTime {
|
|
61
|
+
try {
|
|
62
|
+
if (event.type === EventType.FullSnapshot) {
|
|
63
|
+
walkNode((event.data as { node?: unknown }).node);
|
|
64
|
+
} else if (event.type === EventType.IncrementalSnapshot) {
|
|
65
|
+
const data = event.data as {
|
|
66
|
+
source?: number;
|
|
67
|
+
adds?: Array<{ node?: unknown }>;
|
|
68
|
+
attributes?: Array<{ attributes?: unknown }>;
|
|
69
|
+
};
|
|
70
|
+
if (data.source === IncrementalSource.Mutation) {
|
|
71
|
+
data.adds?.forEach((item) => walkNode(item.node));
|
|
72
|
+
data.attributes?.forEach((item) => {
|
|
73
|
+
if (item.attributes && typeof item.attributes === 'object') {
|
|
74
|
+
sanitizeAttributes(item.attributes as Record<string, unknown>);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// A malformed asset must never take down the player.
|
|
81
|
+
}
|
|
82
|
+
return event;
|
|
83
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
.replay-preview {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
color: var(--replay-preview-fg, #e5e7eb);
|
|
5
|
+
background: var(--replay-preview-bg, #0f172a);
|
|
6
|
+
border: 1px solid var(--replay-preview-border, rgba(148, 163, 184, 0.28));
|
|
7
|
+
border-radius: 12px;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.replay-preview__frame {
|
|
12
|
+
display: block;
|
|
13
|
+
width: 100%;
|
|
14
|
+
aspect-ratio: 16 / 9;
|
|
15
|
+
border: 0;
|
|
16
|
+
background: #0f172a;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.replay-preview__invalid,
|
|
20
|
+
.replay-preview__empty,
|
|
21
|
+
.replay-preview__error {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
min-height: 140px;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
color: var(--replay-preview-muted, #94a3b8);
|
|
28
|
+
font: 13px/1.5 ui-sans-serif, system-ui, sans-serif;
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.replay-preview__invalid {
|
|
33
|
+
color: var(--replay-preview-link, #7dd3fc);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.replay-preview__error {
|
|
37
|
+
color: #fda4af;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.replay-preview__native {
|
|
41
|
+
width: 100%;
|
|
42
|
+
background: #0f172a;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.replay-preview__stage {
|
|
46
|
+
position: relative;
|
|
47
|
+
width: 100%;
|
|
48
|
+
min-height: 160px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
background: #020617;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.replay-preview__scaler {
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset: 0 auto auto 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.replay-preview__player {
|
|
59
|
+
position: relative;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.replay-preview__controls {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
padding: 8px 10px;
|
|
68
|
+
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
|
69
|
+
background: #111827;
|
|
70
|
+
font: 12px/1.2 ui-sans-serif, system-ui, sans-serif;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.replay-preview__controls button,
|
|
74
|
+
.replay-preview__controls select {
|
|
75
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
76
|
+
border-radius: 6px;
|
|
77
|
+
padding: 5px 8px;
|
|
78
|
+
color: inherit;
|
|
79
|
+
background: #1e293b;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.replay-preview__controls input[type='range'] {
|
|
84
|
+
min-width: 60px;
|
|
85
|
+
flex: 1;
|
|
86
|
+
accent-color: #38bdf8;
|
|
87
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a normal Loop QA project URL to its chrome-free public embed route.
|
|
3
|
+
* The application owns auth/public-access decisions; this helper only changes
|
|
4
|
+
* the route shape that Loop QA already exposes for cross-site iframes.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeReplayEmbedUrl(input: string): string | null {
|
|
7
|
+
let url: URL;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(input);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
14
|
+
|
|
15
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
16
|
+
if (parts[0] === 'projects' && parts.length >= 4) {
|
|
17
|
+
const projectId = decodeURIComponent(parts[1] ?? '');
|
|
18
|
+
const kind = parts[2] === 'explorations' ? 'tasks' : parts[2];
|
|
19
|
+
const resourceId = decodeURIComponent(parts[3] ?? '');
|
|
20
|
+
if (
|
|
21
|
+
projectId &&
|
|
22
|
+
resourceId &&
|
|
23
|
+
(kind === 'tasks' || kind === 'journeys' || kind === 'test-runs')
|
|
24
|
+
) {
|
|
25
|
+
url.pathname = `/p/${encodeURIComponent(projectId)}/${kind}/${encodeURIComponent(resourceId)}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return url.toString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isReplayQaUrl(input: string): boolean {
|
|
33
|
+
const normalized = normalizeReplayEmbedUrl(input);
|
|
34
|
+
if (!normalized) return false;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(normalized);
|
|
37
|
+
return url.pathname.startsWith('/p/') || url.hostname.endsWith('.replay.io');
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -11,12 +11,25 @@
|
|
|
11
11
|
--gb-accent-foreground: var(--accent-foreground);
|
|
12
12
|
--gb-primary: var(--primary);
|
|
13
13
|
--gb-primary-foreground: var(--primary-foreground);
|
|
14
|
-
|
|
14
|
+
/* Plain rgba/hex values (no color-mix()/oklch()) so the theme renders
|
|
15
|
+
correctly in older Chromium forks; hosts can override any token. */
|
|
16
|
+
--gb-code-bg: rgba(128, 128, 128, 0.13);
|
|
15
17
|
--gb-code-fg: var(--foreground);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
--gb-
|
|
18
|
+
|
|
19
|
+
/* Semantic hint palette — solid color plus precomputed tints. Matches the
|
|
20
|
+
editor palette in @brett_lamy/docstream-editor/styles.css. */
|
|
21
|
+
--gb-info: #3b82f6;
|
|
22
|
+
--gb-info-border: rgba(59, 130, 246, 0.45);
|
|
23
|
+
--gb-info-bg: rgba(59, 130, 246, 0.1);
|
|
24
|
+
--gb-success: #22c55e;
|
|
25
|
+
--gb-success-border: rgba(34, 197, 94, 0.45);
|
|
26
|
+
--gb-success-bg: rgba(34, 197, 94, 0.1);
|
|
27
|
+
--gb-warning: #f59e0b;
|
|
28
|
+
--gb-warning-border: rgba(245, 158, 11, 0.5);
|
|
29
|
+
--gb-warning-bg: rgba(245, 158, 11, 0.12);
|
|
30
|
+
--gb-danger: #ef4444;
|
|
31
|
+
--gb-danger-border: rgba(239, 68, 68, 0.45);
|
|
32
|
+
--gb-danger-bg: rgba(239, 68, 68, 0.1);
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
.docs-article {
|
|
@@ -130,8 +143,8 @@
|
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
.docs-hint-info {
|
|
133
|
-
border-color:
|
|
134
|
-
background:
|
|
146
|
+
border-color: var(--gb-info-border);
|
|
147
|
+
background: var(--gb-info-bg);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
.docs-hint-info .docs-hint-icon {
|
|
@@ -139,8 +152,8 @@
|
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
.docs-hint-success {
|
|
142
|
-
border-color:
|
|
143
|
-
background:
|
|
155
|
+
border-color: var(--gb-success-border);
|
|
156
|
+
background: var(--gb-success-bg);
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
.docs-hint-success .docs-hint-icon {
|
|
@@ -148,8 +161,8 @@
|
|
|
148
161
|
}
|
|
149
162
|
|
|
150
163
|
.docs-hint-warning {
|
|
151
|
-
border-color:
|
|
152
|
-
background:
|
|
164
|
+
border-color: var(--gb-warning-border);
|
|
165
|
+
background: var(--gb-warning-bg);
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
.docs-hint-warning .docs-hint-icon {
|
|
@@ -157,8 +170,8 @@
|
|
|
157
170
|
}
|
|
158
171
|
|
|
159
172
|
.docs-hint-danger {
|
|
160
|
-
border-color:
|
|
161
|
-
background:
|
|
173
|
+
border-color: var(--gb-danger-border);
|
|
174
|
+
background: var(--gb-danger-bg);
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
.docs-hint-danger .docs-hint-icon {
|
|
@@ -495,23 +508,23 @@
|
|
|
495
508
|
}
|
|
496
509
|
|
|
497
510
|
.oas-method-get {
|
|
498
|
-
background:
|
|
511
|
+
background: var(--gb-success-bg);
|
|
499
512
|
color: var(--gb-success);
|
|
500
513
|
}
|
|
501
514
|
|
|
502
515
|
.oas-method-post {
|
|
503
|
-
background:
|
|
516
|
+
background: var(--gb-info-bg);
|
|
504
517
|
color: var(--gb-info);
|
|
505
518
|
}
|
|
506
519
|
|
|
507
520
|
.oas-method-put,
|
|
508
521
|
.oas-method-patch {
|
|
509
|
-
background:
|
|
522
|
+
background: var(--gb-warning-bg);
|
|
510
523
|
color: var(--gb-warning);
|
|
511
524
|
}
|
|
512
525
|
|
|
513
526
|
.oas-method-delete {
|
|
514
|
-
background:
|
|
527
|
+
background: var(--gb-danger-bg);
|
|
515
528
|
color: var(--gb-danger);
|
|
516
529
|
}
|
|
517
530
|
|
|
@@ -602,18 +615,18 @@
|
|
|
602
615
|
}
|
|
603
616
|
|
|
604
617
|
.oas-code-2 {
|
|
605
|
-
background:
|
|
618
|
+
background: var(--gb-success-bg);
|
|
606
619
|
color: var(--gb-success);
|
|
607
620
|
}
|
|
608
621
|
|
|
609
622
|
.oas-code-3 {
|
|
610
|
-
background:
|
|
623
|
+
background: var(--gb-warning-bg);
|
|
611
624
|
color: var(--gb-warning);
|
|
612
625
|
}
|
|
613
626
|
|
|
614
627
|
.oas-code-4,
|
|
615
628
|
.oas-code-5 {
|
|
616
|
-
background:
|
|
629
|
+
background: var(--gb-danger-bg);
|
|
617
630
|
color: var(--gb-danger);
|
|
618
631
|
}
|
|
619
632
|
|
|
@@ -715,7 +728,7 @@
|
|
|
715
728
|
|
|
716
729
|
.hljs-operator,
|
|
717
730
|
.hljs-punctuation {
|
|
718
|
-
color:
|
|
731
|
+
color: var(--gb-code-fg);
|
|
719
732
|
}
|
|
720
733
|
|
|
721
734
|
.hljs-deletion {
|
|
@@ -725,3 +738,77 @@
|
|
|
725
738
|
.hljs-addition {
|
|
726
739
|
color: var(--gb-success);
|
|
727
740
|
}
|
|
741
|
+
|
|
742
|
+
.docs-react-demo {
|
|
743
|
+
overflow: hidden;
|
|
744
|
+
border: 1px solid var(--gb-border);
|
|
745
|
+
border-radius: 12px;
|
|
746
|
+
background: var(--gb-panel);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.docs-react-demo-header {
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
gap: 10px;
|
|
753
|
+
min-height: 42px;
|
|
754
|
+
padding: 8px 12px;
|
|
755
|
+
border-bottom: 1px solid var(--gb-border);
|
|
756
|
+
color: var(--gb-panel-foreground);
|
|
757
|
+
font-size: 13px;
|
|
758
|
+
font-weight: 550;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.docs-react-demo-status {
|
|
762
|
+
margin-left: auto;
|
|
763
|
+
color: var(--gb-muted-foreground);
|
|
764
|
+
font-size: 11px;
|
|
765
|
+
text-transform: uppercase;
|
|
766
|
+
letter-spacing: 0.08em;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.docs-react-demo-status-ready {
|
|
770
|
+
color: var(--gb-success);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.docs-react-demo-status-error {
|
|
774
|
+
color: var(--gb-danger);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.docs-react-demo-run {
|
|
778
|
+
border: 1px solid var(--gb-border);
|
|
779
|
+
border-radius: 6px;
|
|
780
|
+
padding: 4px 9px;
|
|
781
|
+
color: var(--gb-panel-foreground);
|
|
782
|
+
font-size: 12px;
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
.docs-react-demo-run:disabled {
|
|
787
|
+
cursor: wait;
|
|
788
|
+
opacity: 0.55;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.docs-react-demo-frame,
|
|
792
|
+
.docs-react-demo-placeholder {
|
|
793
|
+
display: block;
|
|
794
|
+
width: 100%;
|
|
795
|
+
border: 0;
|
|
796
|
+
background: #fff;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.docs-react-demo-placeholder {
|
|
800
|
+
display: grid;
|
|
801
|
+
place-items: center;
|
|
802
|
+
color: #64748b;
|
|
803
|
+
font-size: 13px;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.docs-react-demo-error {
|
|
807
|
+
margin: 0;
|
|
808
|
+
min-height: 100px;
|
|
809
|
+
overflow: auto;
|
|
810
|
+
padding: 14px;
|
|
811
|
+
color: var(--gb-danger);
|
|
812
|
+
background: var(--gb-code-bg);
|
|
813
|
+
white-space: pre-wrap;
|
|
814
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CSSProperties } from "react"
|
|
2
|
+
|
|
3
|
+
export interface VideoEmbedProps {
|
|
4
|
+
/** A direct media asset or a public player URL. */
|
|
5
|
+
src: string
|
|
6
|
+
title?: string
|
|
7
|
+
className?: string
|
|
8
|
+
/** CSS aspect-ratio for iframe embeds. Defaults to 16 / 9. */
|
|
9
|
+
aspectRatio?: CSSProperties["aspectRatio"]
|
|
10
|
+
controls?: boolean
|
|
11
|
+
autoplay?: boolean
|
|
12
|
+
loop?: boolean
|
|
13
|
+
muted?: boolean
|
|
14
|
+
poster?: string
|
|
15
|
+
allowFullScreen?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isDirectVideo(src: string): boolean {
|
|
19
|
+
return /\.(?:mp4|webm|ogg)(?:[?#]|$)/i.test(src)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function VideoEmbed({
|
|
23
|
+
src,
|
|
24
|
+
title = "Video",
|
|
25
|
+
className,
|
|
26
|
+
aspectRatio = "16 / 9",
|
|
27
|
+
controls = true,
|
|
28
|
+
autoplay = false,
|
|
29
|
+
loop = false,
|
|
30
|
+
muted = false,
|
|
31
|
+
poster,
|
|
32
|
+
allowFullScreen = true,
|
|
33
|
+
}: VideoEmbedProps) {
|
|
34
|
+
const mediaClass = `video-embed__media ${className ?? ""}`.trim()
|
|
35
|
+
if (isDirectVideo(src)) {
|
|
36
|
+
return (
|
|
37
|
+
<video
|
|
38
|
+
className={mediaClass}
|
|
39
|
+
src={src}
|
|
40
|
+
title={title}
|
|
41
|
+
controls={controls}
|
|
42
|
+
autoPlay={autoplay}
|
|
43
|
+
loop={loop}
|
|
44
|
+
muted={muted}
|
|
45
|
+
poster={poster}
|
|
46
|
+
playsInline
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
<iframe
|
|
52
|
+
className={mediaClass}
|
|
53
|
+
src={src}
|
|
54
|
+
title={title}
|
|
55
|
+
loading="lazy"
|
|
56
|
+
allow="autoplay; fullscreen"
|
|
57
|
+
allowFullScreen={allowFullScreen}
|
|
58
|
+
style={{ aspectRatio }}
|
|
59
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|