@docsector/docsector-reader 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/bin/docsector.js +1 -1
- package/jsconfig.json +1 -1
- package/package.json +1 -1
- package/src/components/DPageEmbeddedUrl.vue +375 -0
- package/src/components/DPageTokens.vue +8 -0
- package/src/components/page-section-tokens.js +60 -1
- package/src/composables/useEmbeddedUrl.js +365 -0
- package/src/pages/manual/content/blocks/embedded-urls.overview.en-US.md +33 -0
- package/src/pages/manual/content/blocks/embedded-urls.overview.pt-BR.md +33 -0
- package/src/pages/manual/content/blocks/embedded-urls.showcase.en-US.md +25 -0
- package/src/pages/manual/content/blocks/embedded-urls.showcase.pt-BR.md +25 -0
- package/src/pages/manual.index.js +28 -0
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
73
73
|
- 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
74
74
|
- 📖 **Expandable Markdown Sections** — Use `<d-expandable title="...">...</d-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
|
|
75
75
|
- 📎 **File Attachment Blocks** — Use `<d-file src="/files/...">...</d-file>` in Markdown to render downloadable file cards with automatic local size detection and support for external URLs
|
|
76
|
+
- 🌐 **Embedded URL Blocks** — Use `<d-embedded-url url="https://...">...</d-embedded-url>` to render curated embeds for YouTube, Vimeo, Spotify, and CodePen with a safe link-card fallback for unsupported URLs
|
|
76
77
|
- 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
|
|
77
78
|
- 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
|
|
78
79
|
- 🗃️ **Multi-Version History** — Archive older major versions under `src/pages/.old/<version>/` and expose them at prefixed routes (e.g. `/v0.x/guide/...`) while keeping the current docs at unprefixed routes
|
|
@@ -1036,6 +1037,21 @@ Notes:
|
|
|
1036
1037
|
- The block body is rendered as an inline Markdown caption.
|
|
1037
1038
|
- External URLs also work, so the same syntax can later point to R2 or another CDN without changing the page structure.
|
|
1038
1039
|
|
|
1040
|
+
### Embedded URL Blocks
|
|
1041
|
+
|
|
1042
|
+
```html
|
|
1043
|
+
<d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE" title="YouTube player demo">
|
|
1044
|
+
Optional caption rendered as inline Markdown.
|
|
1045
|
+
</d-embedded-url>
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
Notes:
|
|
1049
|
+
|
|
1050
|
+
- Supported providers currently include YouTube, Vimeo, Spotify, and CodePen.
|
|
1051
|
+
- The block preserves the original query string, so provider options such as `autoplay=1&loop=1` keep working when supported by the destination service.
|
|
1052
|
+
- Unsupported or private URLs fall back to a safe external-link card instead of attempting a generic iframe.
|
|
1053
|
+
- Raw HTML remains the escape hatch when you need a provider outside the curated list or full manual iframe control.
|
|
1054
|
+
|
|
1039
1055
|
---
|
|
1040
1056
|
|
|
1041
1057
|
|
package/bin/docsector.js
CHANGED
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useI18n } from 'vue-i18n'
|
|
4
|
+
|
|
5
|
+
import { resolveEmbeddedUrl } from '../composables/useEmbeddedUrl'
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
name: 'DPageEmbeddedUrl'
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const props = defineProps({
|
|
12
|
+
url: {
|
|
13
|
+
type: String,
|
|
14
|
+
default: ''
|
|
15
|
+
},
|
|
16
|
+
title: {
|
|
17
|
+
type: String,
|
|
18
|
+
default: ''
|
|
19
|
+
},
|
|
20
|
+
caption: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: ''
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
|
|
28
|
+
const resolved = computed(() => {
|
|
29
|
+
return resolveEmbeddedUrl(props.url, {
|
|
30
|
+
title: props.title
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const isEmbedded = computed(() => {
|
|
35
|
+
return resolved.value.mode === 'embed' && resolved.value.embedSrc !== ''
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const isCompactEmbed = computed(() => {
|
|
39
|
+
return isEmbedded.value && resolved.value.provider === 'spotify'
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const displayTitle = computed(() => {
|
|
43
|
+
return resolved.value.title || props.title || resolved.value.providerLabel || props.url
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const displayUrl = computed(() => {
|
|
47
|
+
return resolved.value.displayUrl || props.url
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const compactUrl = computed(() => {
|
|
51
|
+
return String(displayUrl.value || '').replace(/^https?:\/\//i, '')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const frameStyle = computed(() => {
|
|
55
|
+
const style = {}
|
|
56
|
+
|
|
57
|
+
if (resolved.value.aspectRatio) {
|
|
58
|
+
style.aspectRatio = resolved.value.aspectRatio
|
|
59
|
+
} else {
|
|
60
|
+
style.aspectRatio = 'auto'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (resolved.value.frameHeight > 0) {
|
|
64
|
+
style.height = `${resolved.value.frameHeight}px`
|
|
65
|
+
style.minHeight = `${resolved.value.frameHeight}px`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return style
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const openHref = computed(() => {
|
|
72
|
+
return resolved.value.canonicalUrl || props.url
|
|
73
|
+
})
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<div
|
|
78
|
+
class="d-page-embedded-url"
|
|
79
|
+
:class="{
|
|
80
|
+
'd-page-embedded-url--compact': isCompactEmbed,
|
|
81
|
+
'd-page-embedded-url--embedded': isEmbedded,
|
|
82
|
+
'd-page-embedded-url--fallback': !isEmbedded
|
|
83
|
+
}"
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
v-if="isEmbedded"
|
|
87
|
+
class="d-page-embedded-url__frame-shell"
|
|
88
|
+
:style="frameStyle"
|
|
89
|
+
>
|
|
90
|
+
<iframe
|
|
91
|
+
class="d-page-embedded-url__frame"
|
|
92
|
+
:src="resolved.embedSrc"
|
|
93
|
+
:title="displayTitle"
|
|
94
|
+
:allow="resolved.allow || undefined"
|
|
95
|
+
:allowfullscreen="resolved.allowFullscreen"
|
|
96
|
+
loading="lazy"
|
|
97
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
98
|
+
></iframe>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div
|
|
102
|
+
v-if="!isCompactEmbed || caption"
|
|
103
|
+
class="d-page-embedded-url__body"
|
|
104
|
+
>
|
|
105
|
+
<template v-if="!isCompactEmbed">
|
|
106
|
+
<div
|
|
107
|
+
v-if="!isEmbedded"
|
|
108
|
+
class="d-page-embedded-url__media"
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
>
|
|
111
|
+
<q-icon
|
|
112
|
+
:name="resolved.icon || 'link'"
|
|
113
|
+
size="28px"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="d-page-embedded-url__content">
|
|
118
|
+
<div
|
|
119
|
+
v-if="resolved.providerLabel"
|
|
120
|
+
class="d-page-embedded-url__provider"
|
|
121
|
+
>
|
|
122
|
+
<q-icon
|
|
123
|
+
:name="resolved.icon || 'link'"
|
|
124
|
+
size="16px"
|
|
125
|
+
/>
|
|
126
|
+
<span>{{ resolved.providerLabel }}</span>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="d-page-embedded-url__title">{{ displayTitle }}</div>
|
|
130
|
+
|
|
131
|
+
<div
|
|
132
|
+
v-if="compactUrl"
|
|
133
|
+
class="d-page-embedded-url__url"
|
|
134
|
+
>{{ compactUrl }}</div>
|
|
135
|
+
|
|
136
|
+
<div
|
|
137
|
+
v-if="caption"
|
|
138
|
+
class="d-page-embedded-url__caption"
|
|
139
|
+
v-html="caption"
|
|
140
|
+
></div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="d-page-embedded-url__actions">
|
|
144
|
+
<q-btn
|
|
145
|
+
no-caps
|
|
146
|
+
unelevated
|
|
147
|
+
padding="8px 12px"
|
|
148
|
+
class="d-page-embedded-url__action-button"
|
|
149
|
+
icon="open_in_new"
|
|
150
|
+
:label="t('page.file.open')"
|
|
151
|
+
:href="openHref"
|
|
152
|
+
target="_blank"
|
|
153
|
+
rel="noopener noreferrer"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<div
|
|
159
|
+
v-else-if="caption"
|
|
160
|
+
class="d-page-embedded-url__compact-caption"
|
|
161
|
+
v-html="caption"
|
|
162
|
+
></div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|
|
166
|
+
|
|
167
|
+
<style lang="sass">
|
|
168
|
+
body.body--light
|
|
169
|
+
--d-page-embedded-url-bg: linear-gradient(180deg, #f7faf5 0%, #ffffff 100%)
|
|
170
|
+
--d-page-embedded-url-border: rgba(52, 85, 54, 0.14)
|
|
171
|
+
--d-page-embedded-url-shadow: rgba(52, 85, 54, 0.08)
|
|
172
|
+
--d-page-embedded-url-frame-bg: rgba(44, 60, 45, 0.04)
|
|
173
|
+
--d-page-embedded-url-media-bg: rgba(255, 255, 255, 0.92)
|
|
174
|
+
--d-page-embedded-url-media-border: rgba(52, 85, 54, 0.08)
|
|
175
|
+
--d-page-embedded-url-accent: #30583a
|
|
176
|
+
--d-page-embedded-url-meta: #56715c
|
|
177
|
+
--d-page-embedded-url-caption: #405148
|
|
178
|
+
--d-page-embedded-url-action-border: rgba(52, 85, 54, 0.18)
|
|
179
|
+
--d-page-embedded-url-action-hover: rgba(52, 85, 54, 0.06)
|
|
180
|
+
|
|
181
|
+
body.body--dark
|
|
182
|
+
--d-page-embedded-url-bg: linear-gradient(180deg, rgba(226, 255, 234, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)
|
|
183
|
+
--d-page-embedded-url-border: rgba(214, 245, 224, 0.14)
|
|
184
|
+
--d-page-embedded-url-shadow: rgba(0, 0, 0, 0.3)
|
|
185
|
+
--d-page-embedded-url-frame-bg: rgba(255, 255, 255, 0.03)
|
|
186
|
+
--d-page-embedded-url-media-bg: rgba(255, 255, 255, 0.06)
|
|
187
|
+
--d-page-embedded-url-media-border: rgba(255, 255, 255, 0.1)
|
|
188
|
+
--d-page-embedded-url-accent: #b9e2c5
|
|
189
|
+
--d-page-embedded-url-meta: rgba(255, 255, 255, 0.72)
|
|
190
|
+
--d-page-embedded-url-caption: rgba(255, 255, 255, 0.9)
|
|
191
|
+
--d-page-embedded-url-action-border: rgba(214, 245, 224, 0.16)
|
|
192
|
+
--d-page-embedded-url-action-hover: rgba(214, 245, 224, 0.08)
|
|
193
|
+
|
|
194
|
+
.d-page-embedded-url
|
|
195
|
+
margin: 1.5rem 0
|
|
196
|
+
border: 1px solid var(--d-page-embedded-url-border)
|
|
197
|
+
border-radius: 18px
|
|
198
|
+
background: var(--d-page-embedded-url-bg)
|
|
199
|
+
box-shadow: 0 16px 36px var(--d-page-embedded-url-shadow)
|
|
200
|
+
overflow: hidden
|
|
201
|
+
|
|
202
|
+
.d-page-embedded-url__frame-shell
|
|
203
|
+
position: relative
|
|
204
|
+
width: 100%
|
|
205
|
+
min-height: 240px
|
|
206
|
+
aspect-ratio: 16 / 9
|
|
207
|
+
background: var(--d-page-embedded-url-frame-bg)
|
|
208
|
+
border-bottom: 1px solid var(--d-page-embedded-url-border)
|
|
209
|
+
|
|
210
|
+
.d-page-embedded-url__frame
|
|
211
|
+
position: absolute
|
|
212
|
+
inset: 0
|
|
213
|
+
width: 100%
|
|
214
|
+
height: 100%
|
|
215
|
+
border: 0
|
|
216
|
+
background: transparent
|
|
217
|
+
|
|
218
|
+
.d-page-embedded-url__body
|
|
219
|
+
display: flex
|
|
220
|
+
align-items: center
|
|
221
|
+
gap: 1rem
|
|
222
|
+
padding: 0.95rem 1rem
|
|
223
|
+
|
|
224
|
+
.d-page-embedded-url--embedded
|
|
225
|
+
.d-page-embedded-url__body
|
|
226
|
+
padding-top: 1.25rem
|
|
227
|
+
|
|
228
|
+
.d-page-embedded-url__content
|
|
229
|
+
padding-top: 0.1rem
|
|
230
|
+
|
|
231
|
+
.d-page-embedded-url--compact
|
|
232
|
+
border: 0
|
|
233
|
+
border-radius: 0
|
|
234
|
+
background: transparent
|
|
235
|
+
box-shadow: none
|
|
236
|
+
overflow: visible
|
|
237
|
+
|
|
238
|
+
.d-page-embedded-url__frame-shell
|
|
239
|
+
border: 0
|
|
240
|
+
background: transparent
|
|
241
|
+
|
|
242
|
+
.d-page-embedded-url__body
|
|
243
|
+
display: block
|
|
244
|
+
padding: 0.6rem 0 0
|
|
245
|
+
|
|
246
|
+
.d-page-embedded-url__compact-caption
|
|
247
|
+
margin-top: 0.65rem
|
|
248
|
+
color: var(--d-page-embedded-url-caption)
|
|
249
|
+
|
|
250
|
+
> :first-child
|
|
251
|
+
margin-top: 0
|
|
252
|
+
|
|
253
|
+
> :last-child
|
|
254
|
+
margin-bottom: 0
|
|
255
|
+
|
|
256
|
+
.d-page-embedded-url__media
|
|
257
|
+
width: 56px
|
|
258
|
+
height: 56px
|
|
259
|
+
flex: 0 0 56px
|
|
260
|
+
display: flex
|
|
261
|
+
align-items: center
|
|
262
|
+
justify-content: center
|
|
263
|
+
border-radius: 16px
|
|
264
|
+
background: var(--d-page-embedded-url-media-bg)
|
|
265
|
+
box-shadow: inset 0 0 0 1px var(--d-page-embedded-url-media-border)
|
|
266
|
+
color: var(--d-page-embedded-url-accent)
|
|
267
|
+
|
|
268
|
+
.d-page-embedded-url__content
|
|
269
|
+
min-width: 0
|
|
270
|
+
flex: 1 1 auto
|
|
271
|
+
|
|
272
|
+
.d-page-embedded-url__provider
|
|
273
|
+
display: inline-flex
|
|
274
|
+
align-items: center
|
|
275
|
+
gap: 0.35rem
|
|
276
|
+
margin-bottom: 0.35rem
|
|
277
|
+
color: var(--d-page-embedded-url-meta)
|
|
278
|
+
font-size: 0.82rem
|
|
279
|
+
font-weight: 700
|
|
280
|
+
line-height: 1
|
|
281
|
+
text-transform: uppercase
|
|
282
|
+
letter-spacing: 0.06em
|
|
283
|
+
|
|
284
|
+
.d-page-embedded-url__title
|
|
285
|
+
font-size: 1rem
|
|
286
|
+
font-weight: 700
|
|
287
|
+
line-height: 1.4
|
|
288
|
+
word-break: break-word
|
|
289
|
+
|
|
290
|
+
.d-page-embedded-url__url
|
|
291
|
+
margin-top: 0.2rem
|
|
292
|
+
color: var(--d-page-embedded-url-meta)
|
|
293
|
+
font-size: 0.84rem
|
|
294
|
+
line-height: 1.35
|
|
295
|
+
word-break: break-all
|
|
296
|
+
|
|
297
|
+
.d-page-embedded-url__caption
|
|
298
|
+
margin-top: 0.45rem
|
|
299
|
+
color: var(--d-page-embedded-url-caption)
|
|
300
|
+
|
|
301
|
+
> :first-child
|
|
302
|
+
margin-top: 0
|
|
303
|
+
|
|
304
|
+
> :last-child
|
|
305
|
+
margin-bottom: 0
|
|
306
|
+
|
|
307
|
+
.d-page-embedded-url__actions
|
|
308
|
+
flex: 0 0 auto
|
|
309
|
+
display: flex
|
|
310
|
+
align-items: center
|
|
311
|
+
align-self: stretch
|
|
312
|
+
|
|
313
|
+
.d-page-embedded-url__action-button
|
|
314
|
+
min-height: 40px
|
|
315
|
+
border-radius: 10px
|
|
316
|
+
border: 1px solid var(--d-page-embedded-url-action-border)
|
|
317
|
+
background: transparent !important
|
|
318
|
+
color: var(--d-page-embedded-url-accent) !important
|
|
319
|
+
transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease
|
|
320
|
+
|
|
321
|
+
.q-btn__content
|
|
322
|
+
gap: 0.45rem
|
|
323
|
+
font-size: 0.9rem
|
|
324
|
+
font-weight: 600
|
|
325
|
+
line-height: 1
|
|
326
|
+
|
|
327
|
+
.q-focus-helper,
|
|
328
|
+
&:before,
|
|
329
|
+
&:after
|
|
330
|
+
display: none
|
|
331
|
+
|
|
332
|
+
&:hover
|
|
333
|
+
transform: translateY(-1px)
|
|
334
|
+
background: var(--d-page-embedded-url-action-hover) !important
|
|
335
|
+
|
|
336
|
+
&:focus-visible
|
|
337
|
+
outline: 2px solid var(--d-page-embedded-url-accent)
|
|
338
|
+
outline-offset: 2px
|
|
339
|
+
|
|
340
|
+
.d-page-embedded-url--fallback
|
|
341
|
+
.d-page-embedded-url__body
|
|
342
|
+
padding-block: 1.05rem
|
|
343
|
+
|
|
344
|
+
@media (max-width: 720px)
|
|
345
|
+
.d-page-embedded-url__frame-shell
|
|
346
|
+
min-height: 200px
|
|
347
|
+
|
|
348
|
+
.d-page-embedded-url__body
|
|
349
|
+
flex-wrap: wrap
|
|
350
|
+
align-items: flex-start
|
|
351
|
+
|
|
352
|
+
.d-page-embedded-url__actions
|
|
353
|
+
width: 100%
|
|
354
|
+
justify-content: flex-start
|
|
355
|
+
|
|
356
|
+
.d-page-embedded-url__action-button
|
|
357
|
+
width: 100%
|
|
358
|
+
justify-content: center
|
|
359
|
+
|
|
360
|
+
@media (max-width: 520px)
|
|
361
|
+
.d-page-embedded-url__body
|
|
362
|
+
gap: 0.85rem
|
|
363
|
+
padding: 0.9rem
|
|
364
|
+
|
|
365
|
+
.d-page-embedded-url__media
|
|
366
|
+
width: 48px
|
|
367
|
+
height: 48px
|
|
368
|
+
flex-basis: 48px
|
|
369
|
+
|
|
370
|
+
.d-page-embedded-url__provider
|
|
371
|
+
font-size: 0.76rem
|
|
372
|
+
|
|
373
|
+
.d-page-embedded-url__title
|
|
374
|
+
font-size: 0.95rem
|
|
375
|
+
</style>
|
|
@@ -24,6 +24,7 @@ import DMermaidDiagram from './DMermaidDiagram.vue'
|
|
|
24
24
|
import DPageBlockquote from './DPageBlockquote.vue'
|
|
25
25
|
import DPageImage from './DPageImage.vue'
|
|
26
26
|
import DPageFile from './DPageFile.vue'
|
|
27
|
+
import DPageEmbeddedUrl from './DPageEmbeddedUrl.vue'
|
|
27
28
|
import DQuickLinks from './DQuickLinks.vue'
|
|
28
29
|
import DPageExpandable from './DPageExpandable.vue'
|
|
29
30
|
</script>
|
|
@@ -105,6 +106,13 @@ import DPageExpandable from './DPageExpandable.vue'
|
|
|
105
106
|
:caption="token.caption"
|
|
106
107
|
/>
|
|
107
108
|
|
|
109
|
+
<d-page-embedded-url
|
|
110
|
+
v-else-if="token.tag === 'embedded-url'"
|
|
111
|
+
:url="token.url"
|
|
112
|
+
:title="token.title"
|
|
113
|
+
:caption="token.caption"
|
|
114
|
+
/>
|
|
115
|
+
|
|
108
116
|
<d-page-source-code
|
|
109
117
|
v-else-if="token.tag === 'code'"
|
|
110
118
|
:index="id + token.codeIndex"
|
|
@@ -16,6 +16,7 @@ const ALERT_MESSAGE_TYPES = new Set([
|
|
|
16
16
|
const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
|
|
17
17
|
const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
|
|
18
18
|
const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
|
|
19
|
+
const EMBEDDED_URL_MARKER_PREFIX = '@@DOCSECTOR_EMBEDDED_URL_'
|
|
19
20
|
const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
|
|
20
21
|
const MATH_KATEX_OPTIONS = {
|
|
21
22
|
throwOnError: false,
|
|
@@ -274,6 +275,41 @@ const extractFileBlocks = (source = '') => {
|
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
|
|
278
|
+
const extractEmbeddedUrlBlocks = (source = '') => {
|
|
279
|
+
const map = new Map()
|
|
280
|
+
let index = 0
|
|
281
|
+
|
|
282
|
+
const replaceBlock = (match, rawAttrs, rawCaption = '') => {
|
|
283
|
+
const attrs = parseCustomTagAttributes(rawAttrs)
|
|
284
|
+
const url = decodeHtmlEntities(attrs.url || attrs.href || attrs.src || '').trim()
|
|
285
|
+
|
|
286
|
+
if (!url) {
|
|
287
|
+
return match
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const marker = `${EMBEDDED_URL_MARKER_PREFIX}${index}@@`
|
|
291
|
+
index++
|
|
292
|
+
|
|
293
|
+
map.set(marker, {
|
|
294
|
+
url,
|
|
295
|
+
title: decodeHtmlEntities(attrs.title || '').trim(),
|
|
296
|
+
caption: String(rawCaption).trim()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return `\n${marker}\n`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const replacedSelfClosing = String(source).replace(/<d-embedded-url\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
|
|
303
|
+
return replaceBlock(match, rawAttrs)
|
|
304
|
+
})
|
|
305
|
+
const replaced = replacedSelfClosing.replace(/<d-embedded-url\b([^>]*)>([\s\S]*?)<\/d-embedded-url>/gi, replaceBlock)
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
source: replaced,
|
|
309
|
+
embeddedUrlMap: map
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
const parseFenceAttributes = (raw = '') => {
|
|
278
314
|
const parsed = {}
|
|
279
315
|
const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
|
|
@@ -567,6 +603,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
|
|
|
567
603
|
|
|
568
604
|
const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithExpandables)
|
|
569
605
|
const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
|
|
606
|
+
const { source: sourceWithEmbeddedUrls, embeddedUrlMap } = extractEmbeddedUrlBlocks(sourceWithFiles)
|
|
570
607
|
|
|
571
608
|
fileMap.forEach((data, marker) => {
|
|
572
609
|
fileMap.set(marker, {
|
|
@@ -575,10 +612,17 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
|
|
|
575
612
|
})
|
|
576
613
|
})
|
|
577
614
|
|
|
615
|
+
embeddedUrlMap.forEach((data, marker) => {
|
|
616
|
+
embeddedUrlMap.set(marker, {
|
|
617
|
+
...data,
|
|
618
|
+
caption: restoreShieldedCodeSegments(data.caption, codeSegmentsMap)
|
|
619
|
+
})
|
|
620
|
+
})
|
|
621
|
+
|
|
578
622
|
const markdown = createMarkdownBlockParser()
|
|
579
623
|
const markdownInline = createMarkdownInlineParser()
|
|
580
624
|
const markdownEnv = {}
|
|
581
|
-
const parsed = markdown.parse(restoreShieldedCodeSegments(
|
|
625
|
+
const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithEmbeddedUrls, codeSegmentsMap), markdownEnv)
|
|
582
626
|
const tokens = []
|
|
583
627
|
|
|
584
628
|
let level = 0
|
|
@@ -762,6 +806,21 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
|
|
|
762
806
|
break
|
|
763
807
|
}
|
|
764
808
|
|
|
809
|
+
if (embeddedUrlMap.has(element.content.trim())) {
|
|
810
|
+
const data = embeddedUrlMap.get(element.content.trim())
|
|
811
|
+
|
|
812
|
+
tokens.push({
|
|
813
|
+
tag: 'embedded-url',
|
|
814
|
+
map: element.map,
|
|
815
|
+
url: data.url,
|
|
816
|
+
title: data.title,
|
|
817
|
+
caption: data.caption !== ''
|
|
818
|
+
? markdownInline.renderInline(data.caption, markdownEnv)
|
|
819
|
+
: ''
|
|
820
|
+
})
|
|
821
|
+
break
|
|
822
|
+
}
|
|
823
|
+
|
|
765
824
|
if (tag === 'p') {
|
|
766
825
|
const imageToken = parseStandaloneImageToken(element.content)
|
|
767
826
|
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
const DEFAULT_EMBED = {
|
|
2
|
+
mode: 'link',
|
|
3
|
+
provider: 'link',
|
|
4
|
+
kind: 'link',
|
|
5
|
+
icon: 'link',
|
|
6
|
+
title: '',
|
|
7
|
+
providerLabel: '',
|
|
8
|
+
canonicalUrl: '',
|
|
9
|
+
displayUrl: '',
|
|
10
|
+
embedSrc: '',
|
|
11
|
+
aspectRatio: '',
|
|
12
|
+
frameHeight: 0,
|
|
13
|
+
allow: '',
|
|
14
|
+
allowFullscreen: false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const YOUTUBE_HOSTS = new Set([
|
|
18
|
+
'youtu.be',
|
|
19
|
+
'youtube.com',
|
|
20
|
+
'www.youtube.com',
|
|
21
|
+
'm.youtube.com',
|
|
22
|
+
'music.youtube.com',
|
|
23
|
+
'youtube-nocookie.com',
|
|
24
|
+
'www.youtube-nocookie.com'
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
const VIMEO_HOSTS = new Set([
|
|
28
|
+
'vimeo.com',
|
|
29
|
+
'www.vimeo.com',
|
|
30
|
+
'player.vimeo.com'
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
const SPOTIFY_HOSTS = new Set([
|
|
34
|
+
'spotify.com',
|
|
35
|
+
'www.spotify.com',
|
|
36
|
+
'open.spotify.com'
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const CODEPEN_HOSTS = new Set([
|
|
40
|
+
'codepen.io',
|
|
41
|
+
'www.codepen.io'
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const SPOTIFY_PROVIDER_BY_KIND = {
|
|
45
|
+
track: {
|
|
46
|
+
kind: 'audio',
|
|
47
|
+
icon: 'music_note',
|
|
48
|
+
title: 'Spotify track',
|
|
49
|
+
frameHeight: 152
|
|
50
|
+
},
|
|
51
|
+
episode: {
|
|
52
|
+
kind: 'audio',
|
|
53
|
+
icon: 'podcasts',
|
|
54
|
+
title: 'Spotify episode',
|
|
55
|
+
frameHeight: 232
|
|
56
|
+
},
|
|
57
|
+
artist: {
|
|
58
|
+
kind: 'music',
|
|
59
|
+
icon: 'person',
|
|
60
|
+
title: 'Spotify artist',
|
|
61
|
+
frameHeight: 352
|
|
62
|
+
},
|
|
63
|
+
album: {
|
|
64
|
+
kind: 'music',
|
|
65
|
+
icon: 'album',
|
|
66
|
+
title: 'Spotify album',
|
|
67
|
+
frameHeight: 352
|
|
68
|
+
},
|
|
69
|
+
playlist: {
|
|
70
|
+
kind: 'music',
|
|
71
|
+
icon: 'queue_music',
|
|
72
|
+
title: 'Spotify playlist',
|
|
73
|
+
frameHeight: 352
|
|
74
|
+
},
|
|
75
|
+
show: {
|
|
76
|
+
kind: 'podcast',
|
|
77
|
+
icon: 'radio',
|
|
78
|
+
title: 'Spotify show',
|
|
79
|
+
frameHeight: 352
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const createFallbackResult = (rawUrl = '', title = '') => {
|
|
84
|
+
const trimmed = String(rawUrl || '').trim()
|
|
85
|
+
const normalized = parseHttpUrl(trimmed)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...DEFAULT_EMBED,
|
|
89
|
+
title: title || normalized?.hostname || trimmed,
|
|
90
|
+
providerLabel: normalized?.hostname || '',
|
|
91
|
+
canonicalUrl: normalized?.toString() || trimmed,
|
|
92
|
+
displayUrl: normalized?.toString() || trimmed
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parseHttpUrl = (rawUrl = '') => {
|
|
97
|
+
const trimmed = String(rawUrl || '').trim()
|
|
98
|
+
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(trimmed)
|
|
105
|
+
|
|
106
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return url
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mergeSearchParams = (url, options = {}) => {
|
|
117
|
+
const params = new URLSearchParams(url.search)
|
|
118
|
+
|
|
119
|
+
;(options.remove || []).forEach((key) => {
|
|
120
|
+
params.delete(key)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
Object.entries(options.set || {}).forEach(([key, value]) => {
|
|
124
|
+
if (value === '' || value === null || value === undefined) {
|
|
125
|
+
params.delete(key)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
params.set(key, String(value))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const query = params.toString()
|
|
133
|
+
return query ? `?${query}` : ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parseTimeToSeconds = (value = '') => {
|
|
137
|
+
const trimmed = String(value || '').trim()
|
|
138
|
+
|
|
139
|
+
if (!trimmed) {
|
|
140
|
+
return ''
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (/^\d+$/.test(trimmed)) {
|
|
144
|
+
return trimmed
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const match = trimmed.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i)
|
|
148
|
+
if (!match) {
|
|
149
|
+
return ''
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const hours = Number(match[1] || 0)
|
|
153
|
+
const minutes = Number(match[2] || 0)
|
|
154
|
+
const seconds = Number(match[3] || 0)
|
|
155
|
+
const total = (hours * 3600) + (minutes * 60) + seconds
|
|
156
|
+
|
|
157
|
+
return total > 0 ? String(total) : ''
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const resolveYouTubeVideoId = (url) => {
|
|
161
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
162
|
+
|
|
163
|
+
if (url.hostname === 'youtu.be') {
|
|
164
|
+
return segments[0] || ''
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (segments[0] === 'watch') {
|
|
168
|
+
return url.searchParams.get('v') || ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (['embed', 'shorts', 'live', 'v'].includes(segments[0])) {
|
|
172
|
+
return segments[1] || ''
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return url.searchParams.get('v') || ''
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const resolveYouTubeEmbed = (url, title = '') => {
|
|
179
|
+
const videoId = resolveYouTubeVideoId(url)
|
|
180
|
+
|
|
181
|
+
if (!videoId) {
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const start = parseTimeToSeconds(url.searchParams.get('t'))
|
|
186
|
+
const query = mergeSearchParams(url, {
|
|
187
|
+
remove: ['v', 't'],
|
|
188
|
+
set: {
|
|
189
|
+
...(start ? { start } : {}),
|
|
190
|
+
...((url.searchParams.get('loop') === '1' || url.searchParams.get('loop') === 'true') && !url.searchParams.get('playlist')
|
|
191
|
+
? { playlist: videoId }
|
|
192
|
+
: {})
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...DEFAULT_EMBED,
|
|
198
|
+
mode: 'embed',
|
|
199
|
+
provider: 'youtube',
|
|
200
|
+
kind: 'video',
|
|
201
|
+
icon: 'smart_display',
|
|
202
|
+
title: title || 'YouTube video',
|
|
203
|
+
providerLabel: 'YouTube',
|
|
204
|
+
canonicalUrl: url.toString(),
|
|
205
|
+
displayUrl: url.toString(),
|
|
206
|
+
embedSrc: `https://www.youtube.com/embed/${videoId}${query}`,
|
|
207
|
+
aspectRatio: '16 / 9',
|
|
208
|
+
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
|
|
209
|
+
allowFullscreen: true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const resolveVimeoVideoId = (url) => {
|
|
214
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
215
|
+
|
|
216
|
+
if (segments[0] === 'video') {
|
|
217
|
+
return segments[1] || ''
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return segments.find((segment) => /^\d+$/.test(segment)) || ''
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const resolveVimeoEmbed = (url, title = '') => {
|
|
224
|
+
const videoId = resolveVimeoVideoId(url)
|
|
225
|
+
|
|
226
|
+
if (!videoId) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...DEFAULT_EMBED,
|
|
232
|
+
mode: 'embed',
|
|
233
|
+
provider: 'vimeo',
|
|
234
|
+
kind: 'video',
|
|
235
|
+
icon: 'movie',
|
|
236
|
+
title: title || 'Vimeo video',
|
|
237
|
+
providerLabel: 'Vimeo',
|
|
238
|
+
canonicalUrl: url.toString(),
|
|
239
|
+
displayUrl: url.toString(),
|
|
240
|
+
embedSrc: `https://player.vimeo.com/video/${videoId}${mergeSearchParams(url)}`,
|
|
241
|
+
aspectRatio: '16 / 9',
|
|
242
|
+
allow: 'autoplay; fullscreen; picture-in-picture',
|
|
243
|
+
allowFullscreen: true
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const resolveSpotifyParts = (url) => {
|
|
248
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
249
|
+
const embedIndex = segments[0] === 'embed' ? 1 : 0
|
|
250
|
+
const type = segments[embedIndex] || ''
|
|
251
|
+
const id = segments[embedIndex + 1] || ''
|
|
252
|
+
|
|
253
|
+
if (!SPOTIFY_PROVIDER_BY_KIND[type] || !id) {
|
|
254
|
+
return null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { type, id }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const resolveSpotifyEmbed = (url, title = '') => {
|
|
261
|
+
const parts = resolveSpotifyParts(url)
|
|
262
|
+
|
|
263
|
+
if (parts === null) {
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const definition = SPOTIFY_PROVIDER_BY_KIND[parts.type]
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
...DEFAULT_EMBED,
|
|
271
|
+
mode: 'embed',
|
|
272
|
+
provider: 'spotify',
|
|
273
|
+
kind: definition.kind,
|
|
274
|
+
icon: definition.icon,
|
|
275
|
+
title: title || definition.title,
|
|
276
|
+
providerLabel: 'Spotify',
|
|
277
|
+
canonicalUrl: url.toString(),
|
|
278
|
+
displayUrl: url.toString(),
|
|
279
|
+
embedSrc: `https://open.spotify.com/embed/${parts.type}/${parts.id}${mergeSearchParams(url)}`,
|
|
280
|
+
aspectRatio: '',
|
|
281
|
+
frameHeight: definition.frameHeight,
|
|
282
|
+
allow: 'autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture',
|
|
283
|
+
allowFullscreen: false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const resolveCodePenParts = (url) => {
|
|
288
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
289
|
+
|
|
290
|
+
if (segments[0] === 'team') {
|
|
291
|
+
const team = segments[1] || ''
|
|
292
|
+
const penType = segments[2] || ''
|
|
293
|
+
const penId = segments[3] || ''
|
|
294
|
+
|
|
295
|
+
if (!team || !penId || !['pen', 'full', 'details', 'embed', 'debug'].includes(penType)) {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
user: `team/${team}`,
|
|
301
|
+
penId
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const user = segments[0] || ''
|
|
306
|
+
const penType = segments[1] || ''
|
|
307
|
+
const penId = segments[2] || ''
|
|
308
|
+
|
|
309
|
+
if (!user || !penId || !['pen', 'full', 'details', 'embed', 'debug'].includes(penType)) {
|
|
310
|
+
return null
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { user, penId }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const resolveCodePenEmbed = (url, title = '') => {
|
|
317
|
+
const parts = resolveCodePenParts(url)
|
|
318
|
+
|
|
319
|
+
if (parts === null) {
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
...DEFAULT_EMBED,
|
|
325
|
+
mode: 'embed',
|
|
326
|
+
provider: 'codepen',
|
|
327
|
+
kind: 'code',
|
|
328
|
+
icon: 'code',
|
|
329
|
+
title: title || 'CodePen embed',
|
|
330
|
+
providerLabel: 'CodePen',
|
|
331
|
+
canonicalUrl: url.toString(),
|
|
332
|
+
displayUrl: url.toString(),
|
|
333
|
+
embedSrc: `https://codepen.io/${parts.user}/embed/${parts.penId}${mergeSearchParams(url)}`,
|
|
334
|
+
aspectRatio: '16 / 9',
|
|
335
|
+
allow: 'accelerometer; camera; clipboard-write; display-capture; encrypted-media; geolocation; gyroscope; microphone; midi; web-share',
|
|
336
|
+
allowFullscreen: true
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export const resolveEmbeddedUrl = (rawUrl = '', options = {}) => {
|
|
341
|
+
const title = String(options.title || '').trim()
|
|
342
|
+
const normalized = parseHttpUrl(rawUrl)
|
|
343
|
+
|
|
344
|
+
if (normalized === null) {
|
|
345
|
+
return createFallbackResult(rawUrl, title)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (YOUTUBE_HOSTS.has(normalized.hostname)) {
|
|
349
|
+
return resolveYouTubeEmbed(normalized, title) || createFallbackResult(rawUrl, title)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (VIMEO_HOSTS.has(normalized.hostname)) {
|
|
353
|
+
return resolveVimeoEmbed(normalized, title) || createFallbackResult(rawUrl, title)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (SPOTIFY_HOSTS.has(normalized.hostname)) {
|
|
357
|
+
return resolveSpotifyEmbed(normalized, title) || createFallbackResult(rawUrl, title)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (CODEPEN_HOSTS.has(normalized.hostname)) {
|
|
361
|
+
return resolveCodePenEmbed(normalized, title) || createFallbackResult(rawUrl, title)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return createFallbackResult(rawUrl, title)
|
|
365
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
Embedded URLs turn supported public links into responsive embeds directly inside Markdown pages.
|
|
4
|
+
|
|
5
|
+
This block is a higher-level alternative to raw iframe markup when the source is a supported provider and the page should keep a consistent card + preview treatment.
|
|
6
|
+
|
|
7
|
+
## Markdown Syntax
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE" title="YouTube player demo">
|
|
11
|
+
Optional caption rendered as inline Markdown.
|
|
12
|
+
</d-embedded-url>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You can also omit the caption body when the provider preview already gives enough context:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Supported Providers
|
|
22
|
+
|
|
23
|
+
- YouTube
|
|
24
|
+
- Vimeo
|
|
25
|
+
- Spotify
|
|
26
|
+
- CodePen
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- `url` is required. `title` is optional.
|
|
31
|
+
- The block preserves the original query string, so provider options such as `autoplay=1&loop=1` continue to work when the provider supports them.
|
|
32
|
+
- Unsupported or private URLs fall back to a safe external-link card instead of attempting a generic iframe.
|
|
33
|
+
- Use Raw HTML when you need a provider outside the curated list or when you need full manual iframe control.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Apresentação
|
|
2
|
+
|
|
3
|
+
URLs embutidas transformam links públicos suportados em embeds responsivos diretamente dentro das páginas Markdown.
|
|
4
|
+
|
|
5
|
+
Este block é uma alternativa de nível mais alto ao uso manual de iframe quando a origem pertence a um provider suportado e a página deve manter um tratamento consistente de preview + card.
|
|
6
|
+
|
|
7
|
+
## Sintaxe Markdown
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE" title="Demo do player do YouTube">
|
|
11
|
+
Legenda opcional renderizada como Markdown inline.
|
|
12
|
+
</d-embedded-url>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Você também pode omitir o corpo quando o preview do provider já entrega contexto suficiente:
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Providers suportados
|
|
22
|
+
|
|
23
|
+
- YouTube
|
|
24
|
+
- Vimeo
|
|
25
|
+
- Spotify
|
|
26
|
+
- CodePen
|
|
27
|
+
|
|
28
|
+
## Observações
|
|
29
|
+
|
|
30
|
+
- `url` é obrigatório. `title` é opcional.
|
|
31
|
+
- O block preserva a query string original, então opções do provider como `autoplay=1&loop=1` continuam funcionando quando houver suporte.
|
|
32
|
+
- URLs não suportadas ou privadas fazem fallback para um card seguro com link externo, sem tentar um iframe genérico.
|
|
33
|
+
- Use HTML bruto quando você precisar de um provider fora da lista curada ou de controle manual total sobre o iframe.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Showcase
|
|
2
|
+
|
|
3
|
+
### YouTube Video
|
|
4
|
+
|
|
5
|
+
<d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE&autoplay=1&loop=1" title="YouTube player demo">
|
|
6
|
+
The original playback query parameters stay attached to the provider URL.
|
|
7
|
+
</d-embedded-url>
|
|
8
|
+
|
|
9
|
+
### Spotify Track
|
|
10
|
+
|
|
11
|
+
<d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P?si=1234">
|
|
12
|
+
Spotify links render with the provider-specific compact player height.
|
|
13
|
+
</d-embedded-url>
|
|
14
|
+
|
|
15
|
+
### CodePen Demo
|
|
16
|
+
|
|
17
|
+
<d-embedded-url url="https://codepen.io/team/codepen/pen/PNaGbb?default-tab=result" title="Interactive demo">
|
|
18
|
+
Use the same block for live front-end demos without dropping to raw HTML.
|
|
19
|
+
</d-embedded-url>
|
|
20
|
+
|
|
21
|
+
### Unsupported URL Fallback
|
|
22
|
+
|
|
23
|
+
<d-embedded-url url="https://example.com/docs/embed-me" title="External API docs">
|
|
24
|
+
Unsupported providers render a safe link card so the reading flow still keeps the URL visible and actionable.
|
|
25
|
+
</d-embedded-url>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Demonstração
|
|
2
|
+
|
|
3
|
+
### Vídeo do YouTube
|
|
4
|
+
|
|
5
|
+
<d-embedded-url url="https://www.youtube.com/watch?v=M7lc1UVf-VE&autoplay=1&loop=1" title="Demo do player do YouTube">
|
|
6
|
+
Os parâmetros originais de reprodução continuam anexados à URL do provider.
|
|
7
|
+
</d-embedded-url>
|
|
8
|
+
|
|
9
|
+
### Faixa do Spotify
|
|
10
|
+
|
|
11
|
+
<d-embedded-url url="https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P?si=1234">
|
|
12
|
+
Links do Spotify renderizam com a altura compacta específica do provider.
|
|
13
|
+
</d-embedded-url>
|
|
14
|
+
|
|
15
|
+
### Demo no CodePen
|
|
16
|
+
|
|
17
|
+
<d-embedded-url url="https://codepen.io/team/codepen/pen/PNaGbb?default-tab=result" title="Demo interativa">
|
|
18
|
+
Use o mesmo block para demos front-end ao vivo sem cair para HTML bruto.
|
|
19
|
+
</d-embedded-url>
|
|
20
|
+
|
|
21
|
+
### Fallback para URL não suportada
|
|
22
|
+
|
|
23
|
+
<d-embedded-url url="https://example.com/docs/embed-me" title="Documentação externa da API">
|
|
24
|
+
Providers não suportados renderizam um card seguro com link externo, mantendo a URL visível e acionável no fluxo de leitura.
|
|
25
|
+
</d-embedded-url>
|
|
@@ -599,6 +599,34 @@ export default {
|
|
|
599
599
|
}
|
|
600
600
|
},
|
|
601
601
|
|
|
602
|
+
'/content/blocks/embedded-urls': {
|
|
603
|
+
config: {
|
|
604
|
+
icon: 'link',
|
|
605
|
+
status: 'new',
|
|
606
|
+
meta: {
|
|
607
|
+
description: {
|
|
608
|
+
'en-US': 'Embedded URLs — Documentation of Docsector Reader',
|
|
609
|
+
'pt-BR': 'URLs embutidas — Documentação do Docsector Reader'
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
book: 'manual',
|
|
613
|
+
menu: {},
|
|
614
|
+
subpages: {
|
|
615
|
+
showcase: true
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
data: {
|
|
619
|
+
'en-US': { title: 'Embedded URLs' },
|
|
620
|
+
'pt-BR': { title: 'URLs embutidas' }
|
|
621
|
+
},
|
|
622
|
+
metadata: {
|
|
623
|
+
tags: {
|
|
624
|
+
'en-US': 'embed embedded url youtube vimeo spotify codepen iframe preview external link gitbook',
|
|
625
|
+
'pt-BR': 'embed url embutida youtube vimeo spotify codepen iframe preview link externo gitbook'
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
|
|
602
630
|
'/content/blocks/math-and-tex': {
|
|
603
631
|
config: {
|
|
604
632
|
icon: 'functions',
|