@docsector/docsector-reader 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,8 @@ 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'
28
+ import DBlockCard from './DBlockCard.vue'
27
29
  import DQuickLinks from './DQuickLinks.vue'
28
30
  import DPageExpandable from './DPageExpandable.vue'
29
31
  </script>
@@ -105,6 +107,13 @@ import DPageExpandable from './DPageExpandable.vue'
105
107
  :caption="token.caption"
106
108
  />
107
109
 
110
+ <d-page-embedded-url
111
+ v-else-if="token.tag === 'embedded-url'"
112
+ :url="token.url"
113
+ :title="token.title"
114
+ :caption="token.caption"
115
+ />
116
+
108
117
  <d-page-source-code
109
118
  v-else-if="token.tag === 'code'"
110
119
  :index="id + token.codeIndex"
@@ -120,6 +129,12 @@ import DPageExpandable from './DPageExpandable.vue'
120
129
  :content="token.content"
121
130
  />
122
131
 
132
+ <d-block-card
133
+ v-else-if="token.tag === 'cards'"
134
+ :title="token.title"
135
+ :items="token.items"
136
+ />
137
+
123
138
  <d-quick-links
124
139
  v-else-if="token.tag === 'quick-links'"
125
140
  :title="token.title"
@@ -13,9 +13,11 @@ const ALERT_MESSAGE_TYPES = new Set([
13
13
  'caution'
14
14
  ])
15
15
 
16
+ const CARDS_MARKER_PREFIX = '@@DOCSECTOR_CARDS_'
16
17
  const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
17
18
  const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
18
19
  const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
20
+ const EMBEDDED_URL_MARKER_PREFIX = '@@DOCSECTOR_EMBEDDED_URL_'
19
21
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
20
22
  const MATH_KATEX_OPTIONS = {
21
23
  throwOnError: false,
@@ -189,6 +191,55 @@ const extractQuickLinksBlocks = (source = '') => {
189
191
  }
190
192
  }
191
193
 
194
+ const extractCardsBlocks = (source = '') => {
195
+ const map = new Map()
196
+ let index = 0
197
+
198
+ const blockPattern = /<d-(?:block-)?cards\b([^>]*)>([\s\S]*?)<\/d-(?:block-)?cards>/gi
199
+ const replaced = String(source).replace(blockPattern, (_, blockAttrsRaw, inner) => {
200
+ const blockAttrs = parseCustomTagAttributes(blockAttrsRaw)
201
+ const items = []
202
+ const itemPattern = /<d-(?:block-)?card\b([^>]*)\/?\s*>/gi
203
+
204
+ let itemMatch = itemPattern.exec(inner)
205
+ while (itemMatch !== null) {
206
+ const itemAttrs = parseCustomTagAttributes(itemMatch[1])
207
+ const title = itemAttrs.title || ''
208
+ const description = itemAttrs.description || ''
209
+ const to = itemAttrs.to || ''
210
+ const href = itemAttrs.href || ''
211
+
212
+ if (title && description && (to || href)) {
213
+ items.push({
214
+ title,
215
+ description,
216
+ to,
217
+ href,
218
+ image: itemAttrs.image || '',
219
+ icon: itemAttrs.icon || ''
220
+ })
221
+ }
222
+
223
+ itemMatch = itemPattern.exec(inner)
224
+ }
225
+
226
+ const marker = `${CARDS_MARKER_PREFIX}${index}@@`
227
+ index++
228
+
229
+ map.set(marker, {
230
+ title: blockAttrs.title || '',
231
+ items
232
+ })
233
+
234
+ return `\n${marker}\n`
235
+ })
236
+
237
+ return {
238
+ source: replaced,
239
+ cardsMap: map
240
+ }
241
+ }
242
+
192
243
  const parseExpandableOpenState = (raw = '') => {
193
244
  return ['1', 'true', 'yes', 'on'].includes(String(raw).trim().toLowerCase())
194
245
  }
@@ -274,6 +325,41 @@ const extractFileBlocks = (source = '') => {
274
325
  }
275
326
  }
276
327
 
328
+ const extractEmbeddedUrlBlocks = (source = '') => {
329
+ const map = new Map()
330
+ let index = 0
331
+
332
+ const replaceBlock = (match, rawAttrs, rawCaption = '') => {
333
+ const attrs = parseCustomTagAttributes(rawAttrs)
334
+ const url = decodeHtmlEntities(attrs.url || attrs.href || attrs.src || '').trim()
335
+
336
+ if (!url) {
337
+ return match
338
+ }
339
+
340
+ const marker = `${EMBEDDED_URL_MARKER_PREFIX}${index}@@`
341
+ index++
342
+
343
+ map.set(marker, {
344
+ url,
345
+ title: decodeHtmlEntities(attrs.title || '').trim(),
346
+ caption: String(rawCaption).trim()
347
+ })
348
+
349
+ return `\n${marker}\n`
350
+ }
351
+
352
+ const replacedSelfClosing = String(source).replace(/<d-embedded-url\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
353
+ return replaceBlock(match, rawAttrs)
354
+ })
355
+ const replaced = replacedSelfClosing.replace(/<d-embedded-url\b([^>]*)>([\s\S]*?)<\/d-embedded-url>/gi, replaceBlock)
356
+
357
+ return {
358
+ source: replaced,
359
+ embeddedUrlMap: map
360
+ }
361
+ }
362
+
277
363
  const parseFenceAttributes = (raw = '') => {
278
364
  const parsed = {}
279
365
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -565,8 +651,10 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
565
651
  })
566
652
  })
567
653
 
568
- const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithExpandables)
654
+ const { source: sourceWithCards, cardsMap } = extractCardsBlocks(sourceWithExpandables)
655
+ const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithCards)
569
656
  const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
657
+ const { source: sourceWithEmbeddedUrls, embeddedUrlMap } = extractEmbeddedUrlBlocks(sourceWithFiles)
570
658
 
571
659
  fileMap.forEach((data, marker) => {
572
660
  fileMap.set(marker, {
@@ -575,10 +663,17 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
575
663
  })
576
664
  })
577
665
 
666
+ embeddedUrlMap.forEach((data, marker) => {
667
+ embeddedUrlMap.set(marker, {
668
+ ...data,
669
+ caption: restoreShieldedCodeSegments(data.caption, codeSegmentsMap)
670
+ })
671
+ })
672
+
578
673
  const markdown = createMarkdownBlockParser()
579
674
  const markdownInline = createMarkdownInlineParser()
580
675
  const markdownEnv = {}
581
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithFiles, codeSegmentsMap), markdownEnv)
676
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithEmbeddedUrls, codeSegmentsMap), markdownEnv)
582
677
  const tokens = []
583
678
 
584
679
  let level = 0
@@ -735,6 +830,17 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
735
830
  break
736
831
  }
737
832
 
833
+ if (cardsMap.has(element.content.trim())) {
834
+ const data = cardsMap.get(element.content.trim())
835
+
836
+ tokens.push({
837
+ tag: 'cards',
838
+ title: data.title,
839
+ items: data.items
840
+ })
841
+ break
842
+ }
843
+
738
844
  if (quickLinksMap.has(element.content.trim())) {
739
845
  const data = quickLinksMap.get(element.content.trim())
740
846
 
@@ -762,6 +868,21 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
762
868
  break
763
869
  }
764
870
 
871
+ if (embeddedUrlMap.has(element.content.trim())) {
872
+ const data = embeddedUrlMap.get(element.content.trim())
873
+
874
+ tokens.push({
875
+ tag: 'embedded-url',
876
+ map: element.map,
877
+ url: data.url,
878
+ title: data.title,
879
+ caption: data.caption !== ''
880
+ ? markdownInline.renderInline(data.caption, markdownEnv)
881
+ : ''
882
+ })
883
+ break
884
+ }
885
+
765
886
  if (tag === 'p') {
766
887
  const imageToken = parseStandaloneImageToken(element.content)
767
888