@docsector/docsector-reader 3.2.2 → 3.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 CHANGED
@@ -71,6 +71,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
71
71
  - 🔗 **GitHub-Compatible Heading Anchors** — Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
72
72
  - 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
73
73
  - 📖 **Expandable Markdown Sections** — Use `<d-expandable title="...">...</d-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
74
+ - 📎 **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
74
75
  - 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
75
76
  - 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
76
77
  - 🗃️ **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
@@ -617,7 +618,7 @@ Docsector Reader works as a **rendering engine**: it provides the layout, compon
617
618
  │ ├── quasar.config.js ← thin wrapper │
618
619
  │ ├── src/pages/ ← Markdown + route defs │
619
620
  │ ├── src/i18n/ ← language files + tags │
620
- │ └── public/ ← logo, images, icons
621
+ │ └── public/ ← logo, images, icons, files
621
622
  │ │
622
623
  │ ┌───────────────────────────────────────────────┐ │
623
624
  │ │ @docsector/docsector-reader (engine) │ │
@@ -875,7 +876,8 @@ my-docs/
875
876
  └── public/
876
877
  ├── images/logo.png # Project logo
877
878
  ├── flags/ # Locale flag images
878
- └── icons/ # PWA icons
879
+ ├── icons/ # PWA icons
880
+ └── files/ # Downloadable attachments served as /files/...
879
881
  ```
880
882
 
881
883
  A common manual pattern is to keep core UI references under `src/pages/manual/basic/` with user-friendly page titles and focused entry pages such as Search, Branding, Version Switcher, Edit on GitHub, Translation Progress, and Previous & Next, end-user content references under `src/pages/manual/content/blocks/`, structural docs under `src/pages/manual/content/structures/`, and legacy/internal engine-specific references under `src/pages/manual/components/`.
@@ -1018,8 +1020,24 @@ Notes:
1018
1020
  Supported alert types: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`.
1019
1021
  Regular blockquotes without `[!TYPE]` continue to work normally.
1020
1022
 
1023
+ ### File Attachment Blocks
1024
+
1025
+ ```html
1026
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
1027
+ Download the example file bundled with the docs.
1028
+ </d-file>
1029
+ ```
1030
+
1031
+ Notes:
1032
+
1033
+ - Store small repo-tracked attachments in `public/files/` and link them with absolute paths such as `/files/manual/release-checklist.txt`.
1034
+ - `title` and `size` are optional. If `title` is omitted, the rendered card falls back to the filename from `src`.
1035
+ - The block body is rendered as an inline Markdown caption.
1036
+ - External URLs also work, so the same syntax can later point to R2 or another CDN without changing the page structure.
1037
+
1021
1038
  ---
1022
1039
 
1040
+
1023
1041
  ## 🖥️ CLI Commands
1024
1042
 
1025
1043
  ```bash
package/bin/docsector.js CHANGED
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
23
23
  const args = process.argv.slice(2)
24
24
  const command = args[0]
25
25
 
26
- const VERSION = '3.2.2'
26
+ const VERSION = '3.3.1'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "3.2.2",
3
+ "version": "3.3.1",
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,7 @@
1
+ Docsector Reader Release Checklist
2
+ =================================
3
+
4
+ 1. Confirm the target route and page registry entry.
5
+ 2. Update the overview and showcase markdown pages.
6
+ 3. Run the focused test suite for the touched parser or helper.
7
+ 4. Run a production build and inspect the published artifact.
@@ -0,0 +1,430 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useQuasar } from 'quasar'
4
+ import { useI18n } from 'vue-i18n'
5
+
6
+ import { resolveFileIconUrl } from '../composables/useFileIcon'
7
+
8
+ const BASE_URL = import.meta.env.BASE_URL || '/'
9
+
10
+ defineOptions({
11
+ name: 'DPageFile'
12
+ })
13
+
14
+ const props = defineProps({
15
+ src: {
16
+ type: String,
17
+ default: ''
18
+ },
19
+ title: {
20
+ type: String,
21
+ default: ''
22
+ },
23
+ size: {
24
+ type: String,
25
+ default: ''
26
+ },
27
+ caption: {
28
+ type: String,
29
+ default: ''
30
+ }
31
+ })
32
+
33
+ const $q = useQuasar()
34
+ const { t } = useI18n()
35
+ const resolvedSize = ref(String(props.size || '').trim())
36
+
37
+ const isExternal = computed(() => /^https?:\/\//i.test(props.src || ''))
38
+ const resolvedHref = computed(() => {
39
+ const raw = String(props.src || '').trim()
40
+
41
+ if (!raw) {
42
+ return ''
43
+ }
44
+
45
+ if (/^(?:[a-z]+:)?\/\//i.test(raw) || /^(?:mailto:|tel:|data:)/i.test(raw)) {
46
+ return raw
47
+ }
48
+
49
+ const trimmedBase = String(BASE_URL).replace(/\/$/, '')
50
+
51
+ if (raw.startsWith('/')) {
52
+ return `${trimmedBase}${raw}` || raw
53
+ }
54
+
55
+ const normalized = raw.replace(/^\.\//, '')
56
+ return `${trimmedBase}/${normalized}`.replace(/\/+/g, '/')
57
+ })
58
+ const absoluteHref = computed(() => {
59
+ const href = resolvedHref.value
60
+
61
+ if (!href || /^(?:[a-z]+:)?\/\//i.test(href)) {
62
+ return href
63
+ }
64
+
65
+ if (typeof window === 'undefined') {
66
+ return href
67
+ }
68
+
69
+ return new URL(href, window.location.origin).toString()
70
+ })
71
+ const fileName = computed(() => {
72
+ const normalized = String(props.src || '')
73
+ .split('#')[0]
74
+ .split('?')[0]
75
+ const rawSegment = normalized
76
+ .split('/')
77
+ .filter(Boolean)
78
+ .pop() || ''
79
+
80
+ if (!rawSegment) {
81
+ return ''
82
+ }
83
+
84
+ try {
85
+ return decodeURIComponent(rawSegment)
86
+ } catch {
87
+ return rawSegment
88
+ }
89
+ })
90
+ const displayTitle = computed(() => {
91
+ return props.title || fileName.value || t('page.file.defaultTitle')
92
+ })
93
+ const displayHeading = computed(() => {
94
+ if (props.title && fileName.value && fileName.value !== props.title) {
95
+ return `${props.title} (${fileName.value})`
96
+ }
97
+
98
+ return displayTitle.value
99
+ })
100
+ const iconUrl = computed(() => {
101
+ return resolveFileIconUrl(fileName.value || displayTitle.value || props.src, {
102
+ preferLight: !$q.dark.isActive
103
+ })
104
+ })
105
+
106
+ const formatFileSize = (bytes) => {
107
+ const value = Number(bytes)
108
+
109
+ if (!Number.isFinite(value) || value <= 0) {
110
+ return ''
111
+ }
112
+
113
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
114
+ let size = value
115
+ let unitIndex = 0
116
+
117
+ while (size >= 1024 && unitIndex < units.length - 1) {
118
+ size /= 1024
119
+ unitIndex++
120
+ }
121
+
122
+ const maximumFractionDigits = size >= 100 || unitIndex === 0 ? 0 : (size >= 10 ? 1 : 2)
123
+ return `${new Intl.NumberFormat(undefined, {
124
+ maximumFractionDigits,
125
+ minimumFractionDigits: 0
126
+ }).format(size)} ${units[unitIndex]}`
127
+ }
128
+
129
+ const readContentLength = async (href, options = {}) => {
130
+ const response = await fetch(href, options)
131
+
132
+ if (!response.ok) {
133
+ throw new Error(`HTTP ${response.status}`)
134
+ }
135
+
136
+ const contentLength = response.headers.get('content-length') || response.headers.get('Content-Length')
137
+ return formatFileSize(contentLength)
138
+ }
139
+
140
+ const hydrateSize = async () => {
141
+ const manualSize = String(props.size || '').trim()
142
+ resolvedSize.value = manualSize
143
+
144
+ if (manualSize || !absoluteHref.value || typeof window === 'undefined') {
145
+ return
146
+ }
147
+
148
+ try {
149
+ const automaticSize = await readContentLength(absoluteHref.value, {
150
+ method: 'HEAD'
151
+ })
152
+
153
+ if (automaticSize) {
154
+ resolvedSize.value = automaticSize
155
+ return
156
+ }
157
+
158
+ if (!isExternal.value) {
159
+ const fallbackSize = await readContentLength(absoluteHref.value)
160
+ if (fallbackSize) {
161
+ resolvedSize.value = fallbackSize
162
+ }
163
+ }
164
+ } catch {
165
+ // External URLs may block access to Content-Length via CORS.
166
+ }
167
+ }
168
+
169
+ watch([
170
+ () => props.size,
171
+ absoluteHref
172
+ ], () => {
173
+ hydrateSize()
174
+ }, {
175
+ immediate: true
176
+ })
177
+
178
+ const openFile = () => {
179
+ const href = absoluteHref.value
180
+
181
+ if (!href || typeof window === 'undefined') {
182
+ return
183
+ }
184
+
185
+ const link = document.createElement('a')
186
+
187
+ link.href = href
188
+ link.target = '_blank'
189
+ link.rel = 'noopener noreferrer'
190
+ document.body.appendChild(link)
191
+ link.click()
192
+ link.remove()
193
+ }
194
+
195
+ const downloadFile = async () => {
196
+ const href = absoluteHref.value
197
+
198
+ if (!href || typeof window === 'undefined') {
199
+ return
200
+ }
201
+
202
+ if (isExternal.value) {
203
+ openFile()
204
+ return
205
+ }
206
+
207
+ try {
208
+ const response = await fetch(href)
209
+
210
+ if (!response.ok) {
211
+ throw new Error(`HTTP ${response.status}`)
212
+ }
213
+
214
+ const blob = await response.blob()
215
+ const objectUrl = window.URL.createObjectURL(blob)
216
+ const link = document.createElement('a')
217
+
218
+ link.href = objectUrl
219
+ link.download = fileName.value || displayTitle.value || 'download'
220
+ link.rel = 'noopener noreferrer'
221
+ document.body.appendChild(link)
222
+ link.click()
223
+ link.remove()
224
+
225
+ window.setTimeout(() => {
226
+ window.URL.revokeObjectURL(objectUrl)
227
+ }, 1000)
228
+ } catch {
229
+ window.location.assign(href)
230
+ }
231
+ }
232
+ </script>
233
+
234
+ <template>
235
+ <div class="d-page-file">
236
+ <div class="d-page-file__body">
237
+ <div class="d-page-file__media-column">
238
+ <div class="d-page-file__media" aria-hidden="true">
239
+ <img
240
+ v-if="iconUrl"
241
+ class="d-page-file__icon"
242
+ :src="iconUrl"
243
+ alt=""
244
+ loading="lazy"
245
+ />
246
+ <q-icon
247
+ v-else
248
+ name="attach_file"
249
+ size="30px"
250
+ />
251
+ </div>
252
+
253
+ <div v-if="resolvedSize" class="d-page-file__size">{{ resolvedSize }}</div>
254
+ </div>
255
+
256
+ <div class="d-page-file__content">
257
+ <div class="d-page-file__title">{{ displayHeading }}</div>
258
+
259
+ <div
260
+ v-if="caption"
261
+ class="d-page-file__caption"
262
+ v-html="caption"
263
+ ></div>
264
+ </div>
265
+
266
+ <div class="d-page-file__actions">
267
+ <q-btn
268
+ no-caps
269
+ unelevated
270
+ padding="8px 12px"
271
+ class="d-page-file__action-button d-page-file__action-button--primary"
272
+ icon="download"
273
+ :label="t('page.file.download')"
274
+ @click="downloadFile"
275
+ />
276
+
277
+ <q-btn
278
+ no-caps
279
+ unelevated
280
+ padding="8px 12px"
281
+ class="d-page-file__action-button d-page-file__action-button--secondary"
282
+ icon="open_in_new"
283
+ :label="t('page.file.open')"
284
+ @click="openFile"
285
+ />
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </template>
290
+
291
+ <style lang="sass">
292
+ body.body--light
293
+ --d-page-file-bg: linear-gradient(180deg, #faf8f2 0%, #ffffff 100%)
294
+ --d-page-file-border: rgba(101, 85, 41, 0.18)
295
+ --d-page-file-shadow: rgba(101, 85, 41, 0.08)
296
+ --d-page-file-media-bg: rgba(255, 255, 255, 0.82)
297
+ --d-page-file-media-border: rgba(101, 85, 41, 0.1)
298
+ --d-page-file-meta: #665f4f
299
+ --d-page-file-caption: #4d5563
300
+ --d-page-file-accent: #655529
301
+ --d-page-file-action-text: #4d4020
302
+ --d-page-file-action-border: rgba(101, 85, 41, 0.22)
303
+ --d-page-file-action-bg-hover: rgba(101, 85, 41, 0.06)
304
+
305
+ body.body--dark
306
+ --d-page-file-bg: linear-gradient(180deg, rgba(255, 248, 235, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)
307
+ --d-page-file-border: rgba(255, 235, 194, 0.14)
308
+ --d-page-file-shadow: rgba(0, 0, 0, 0.28)
309
+ --d-page-file-media-bg: rgba(255, 255, 255, 0.94)
310
+ --d-page-file-media-border: rgba(255, 255, 255, 0.18)
311
+ --d-page-file-meta: rgba(255, 255, 255, 0.7)
312
+ --d-page-file-caption: rgba(255, 255, 255, 0.9)
313
+ --d-page-file-accent: #d7bc7e
314
+ --d-page-file-action-text: rgba(255, 255, 255, 0.92)
315
+ --d-page-file-action-border: rgba(255, 235, 194, 0.18)
316
+ --d-page-file-action-bg-hover: rgba(255, 235, 194, 0.08)
317
+
318
+ .d-page-file
319
+ margin: 1.5rem 0
320
+ border: 1px solid var(--d-page-file-border)
321
+ border-radius: 18px
322
+ background: var(--d-page-file-bg)
323
+ box-shadow: 0 16px 36px var(--d-page-file-shadow)
324
+ overflow: hidden
325
+
326
+ .d-page-file__body
327
+ display: flex
328
+ align-items: center
329
+ gap: 1rem
330
+ padding: 0.82rem 0.92rem
331
+
332
+ .d-page-file__media-column
333
+ display: flex
334
+ flex: 0 0 68px
335
+ flex-direction: column
336
+ align-items: center
337
+ gap: 0.16rem
338
+
339
+ .d-page-file__media
340
+ width: 56px
341
+ height: 56px
342
+ display: flex
343
+ align-items: center
344
+ justify-content: center
345
+ border-radius: 16px
346
+ background: var(--d-page-file-media-bg)
347
+ box-shadow: inset 0 0 0 1px var(--d-page-file-media-border)
348
+ color: var(--d-page-file-accent)
349
+
350
+ .d-page-file__size
351
+ width: 100%
352
+ text-align: center
353
+ color: var(--d-page-file-meta)
354
+ font-size: 0.8rem
355
+ font-weight: 700
356
+ line-height: 1.05
357
+
358
+ .d-page-file__icon
359
+ display: block
360
+ width: 32px
361
+ height: 32px
362
+
363
+ .d-page-file__content
364
+ min-width: 0
365
+ flex: 1 1 auto
366
+
367
+ .d-page-file__title
368
+ font-size: 1rem
369
+ font-weight: 700
370
+ line-height: 1.4
371
+ word-break: break-word
372
+
373
+ .d-page-file__caption
374
+ margin-top: 0.35rem
375
+ color: var(--d-page-file-caption)
376
+
377
+ > :first-child
378
+ margin-top: 0
379
+
380
+ > :last-child
381
+ margin-bottom: 0
382
+
383
+ .d-page-file__actions
384
+ display: flex
385
+ flex: 0 0 auto
386
+ align-items: center
387
+ gap: 0.6rem
388
+ align-self: center
389
+
390
+ .d-page-file__action-button
391
+ min-height: 40px
392
+ border-radius: 10px
393
+ border: 1px solid var(--d-page-file-action-border)
394
+ background: transparent !important
395
+ color: var(--d-page-file-action-text) !important
396
+ transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease
397
+
398
+ .q-btn__content
399
+ gap: 0.45rem
400
+ font-size: 0.9rem
401
+ font-weight: 600
402
+ line-height: 1
403
+
404
+ .q-focus-helper,
405
+ &:before,
406
+ &:after
407
+ display: none
408
+
409
+ &:hover
410
+ transform: translateY(-1px)
411
+ background: var(--d-page-file-action-bg-hover) !important
412
+
413
+ &:focus-visible
414
+ outline: 2px solid var(--d-page-file-accent)
415
+ outline-offset: 2px
416
+
417
+ .d-page-file__action-button--primary
418
+ border-color: var(--d-page-file-action-border)
419
+
420
+ @media (max-width: 720px)
421
+ .d-page-file__body
422
+ flex-wrap: wrap
423
+ align-items: flex-start
424
+
425
+ .d-page-file__actions
426
+ width: 100%
427
+
428
+ .d-page-file__action-button
429
+ flex: 1 1 0
430
+ </style>
@@ -23,6 +23,7 @@ import DPageSourceCode from './DPageSourceCode.vue'
23
23
  import DMermaidDiagram from './DMermaidDiagram.vue'
24
24
  import DPageBlockquote from './DPageBlockquote.vue'
25
25
  import DPageImage from './DPageImage.vue'
26
+ import DPageFile from './DPageFile.vue'
26
27
  import DQuickLinks from './DQuickLinks.vue'
27
28
  import DPageExpandable from './DPageExpandable.vue'
28
29
  </script>
@@ -94,6 +95,14 @@ import DPageExpandable from './DPageExpandable.vue'
94
95
  <div v-html="token.content"></div>
95
96
  </d-page-blockquote>
96
97
 
98
+ <d-page-file
99
+ v-else-if="token.tag === 'file'"
100
+ :src="token.src"
101
+ :title="token.title"
102
+ :size="token.size"
103
+ :caption="token.caption"
104
+ />
105
+
97
106
  <d-page-source-code
98
107
  v-else-if="token.tag === 'code'"
99
108
  :index="id + token.codeIndex"
@@ -14,6 +14,7 @@ const ALERT_MESSAGE_TYPES = new Set([
14
14
 
15
15
  const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
16
16
  const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
17
+ const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
17
18
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
18
19
  const MATH_KATEX_OPTIONS = {
19
20
  throwOnError: false,
@@ -190,6 +191,62 @@ const extractExpandableBlocks = (source = '') => {
190
191
  }
191
192
  }
192
193
 
194
+ const getFileTitleFromSrc = (src = '') => {
195
+ const normalized = String(src)
196
+ .split('#')[0]
197
+ .split('?')[0]
198
+ const rawSegment = normalized
199
+ .split('/')
200
+ .filter(Boolean)
201
+ .pop() || ''
202
+
203
+ if (!rawSegment) {
204
+ return 'Download file'
205
+ }
206
+
207
+ try {
208
+ return decodeURIComponent(rawSegment)
209
+ } catch {
210
+ return rawSegment
211
+ }
212
+ }
213
+
214
+ const extractFileBlocks = (source = '') => {
215
+ const map = new Map()
216
+ let index = 0
217
+
218
+ const replaceBlock = (match, rawAttrs, rawCaption = '') => {
219
+ const attrs = parseCustomTagAttributes(rawAttrs)
220
+ const src = decodeHtmlEntities(attrs.src || attrs.href || '').trim()
221
+
222
+ if (!src) {
223
+ return match
224
+ }
225
+
226
+ const marker = `${FILE_MARKER_PREFIX}${index}@@`
227
+ index++
228
+
229
+ map.set(marker, {
230
+ src,
231
+ title: decodeHtmlEntities(attrs.title || attrs.name || '').trim(),
232
+ size: decodeHtmlEntities(attrs.size || '').trim(),
233
+ caption: String(rawCaption).trim()
234
+ })
235
+
236
+ return `\n${marker}\n`
237
+ }
238
+
239
+ const replacedSelfClosing = String(source).replace(/<d-file\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
240
+ return replaceBlock(match, rawAttrs)
241
+ })
242
+ const replaced = replacedSelfClosing.replace(/<d-file\b([^>]*)>([\s\S]*?)<\/d-file>/gi, replaceBlock)
243
+
244
+ return {
245
+ source: replaced,
246
+ fileMap: map
247
+ }
248
+ }
249
+
193
250
  const parseFenceAttributes = (raw = '') => {
194
251
  const parsed = {}
195
252
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -469,10 +526,19 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
469
526
  })
470
527
 
471
528
  const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithExpandables)
529
+ const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
530
+
531
+ fileMap.forEach((data, marker) => {
532
+ fileMap.set(marker, {
533
+ ...data,
534
+ caption: restoreShieldedCodeSegments(data.caption, codeSegmentsMap)
535
+ })
536
+ })
537
+
472
538
  const markdown = createMarkdownBlockParser()
473
539
  const markdownInline = createMarkdownInlineParser()
474
540
  const markdownEnv = {}
475
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithQuickLinks, codeSegmentsMap), markdownEnv)
541
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithFiles, codeSegmentsMap), markdownEnv)
476
542
  const tokens = []
477
543
 
478
544
  let level = 0
@@ -640,6 +706,22 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
640
706
  break
641
707
  }
642
708
 
709
+ if (fileMap.has(element.content.trim())) {
710
+ const data = fileMap.get(element.content.trim())
711
+
712
+ tokens.push({
713
+ tag: 'file',
714
+ map: element.map,
715
+ src: data.src,
716
+ title: data.title || getFileTitleFromSrc(data.src),
717
+ size: data.size,
718
+ caption: data.caption !== ''
719
+ ? markdownInline.renderInline(data.caption, markdownEnv)
720
+ : ''
721
+ })
722
+ break
723
+ }
724
+
643
725
  if (tag === 'p') {
644
726
  const imageToken = parseStandaloneImageToken(element.content)
645
727
 
@@ -38,6 +38,13 @@
38
38
  copied: 'Copied!',
39
39
  viewAsMarkdown: 'View as Markdown',
40
40
  viewAsMarkdownCaption: 'View this page as plain text',
41
+ file: {
42
+ label: 'File',
43
+ defaultTitle: 'Download file',
44
+ external: 'External file',
45
+ download: 'Download',
46
+ open: 'Open'
47
+ },
41
48
  openInChatGPT: 'Open in ChatGPT',
42
49
  openInChatGPTCaption: 'Ask ChatGPT about this page',
43
50
  openInClaude: 'Open in Claude',
@@ -37,6 +37,13 @@
37
37
  copied: 'Copiado!',
38
38
  viewAsMarkdown: 'Ver como Markdown',
39
39
  viewAsMarkdownCaption: 'Ver esta página como texto simples',
40
+ file: {
41
+ label: 'Arquivo',
42
+ defaultTitle: 'Baixar arquivo',
43
+ external: 'Arquivo externo',
44
+ download: 'Baixar',
45
+ open: 'Abrir'
46
+ },
40
47
  openInChatGPT: 'Abrir no ChatGPT',
41
48
  openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta página',
42
49
  openInClaude: 'Abrir no Claude',
@@ -45,7 +45,7 @@ See the dedicated manual pages for block-by-block reference:
45
45
 
46
46
  - [Paragraphs](/manual/content/blocks/paragraphs/overview/), [Headings](/manual/content/blocks/headings/overview/), [Unordered lists](/manual/content/blocks/unordered-lists/overview/), [Ordered lists](/manual/content/blocks/ordered-lists/overview/)
47
47
  - [Hints](/manual/content/blocks/hints/overview/), [Quote](/manual/content/blocks/quotes/overview/), [Code blocks](/manual/content/blocks/code-blocks/overview/), [Mermaid diagrams](/manual/content/blocks/mermaid-diagrams/overview/)
48
- - [Images](/manual/content/blocks/images/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expandable](/manual/content/blocks/expandable/overview/), [Tables](/manual/content/blocks/tables/overview/), [Raw HTML](/manual/content/blocks/raw-html/overview/), and [Quick Links](/manual/content/blocks/quick-links/overview/)
48
+ - [Images](/manual/content/blocks/images/overview/), [Files](/manual/content/blocks/files/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expandable](/manual/content/blocks/expandable/overview/), [Tables](/manual/content/blocks/tables/overview/), [Raw HTML](/manual/content/blocks/raw-html/overview/), and [Quick Links](/manual/content/blocks/quick-links/overview/)
49
49
 
50
50
  ### Headings
51
51
 
@@ -132,6 +132,20 @@ Set `open="true"` when the block should start expanded.
132
132
 
133
133
  The expandable body supports paragraphs, lists, alerts, code blocks, Mermaid diagrams, tables, raw HTML, and quick links. Keep headings outside the expandable block in this first version, because headings inside the body are flattened to regular paragraphs to preserve the page ToC.
134
134
 
135
+ ### File Blocks
136
+
137
+ Use `<d-file>` to publish downloadable attachments from repo-tracked files or external storage without leaving Markdown:
138
+
139
+ ```html
140
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
141
+ Download the example attachment used in this manual.
142
+ </d-file>
143
+ ```
144
+
145
+ Store repo-tracked files under `public/files/` and prefer absolute site paths such as `/files/...`.
146
+
147
+ `title` and `size` are optional. When `title` is omitted, the UI falls back to the file name from `src`. The caption body supports inline Markdown, and the same syntax also works with external URLs if you later move storage to R2 or another CDN.
148
+
135
149
  ## Adding a New Language
136
150
 
137
151
  1. Create `src/i18n/languages/xx-XX.hjson` with all UI translations
@@ -45,7 +45,7 @@ Veja também as páginas dedicadas do manual para cada bloco:
45
45
 
46
46
  - [Parágrafos](/manual/content/blocks/paragraphs/overview/), [Títulos](/manual/content/blocks/headings/overview/), [Listas não ordenadas](/manual/content/blocks/unordered-lists/overview/), [Listas ordenadas](/manual/content/blocks/ordered-lists/overview/)
47
47
  - [Hints](/manual/content/blocks/hints/overview/), [Citação](/manual/content/blocks/quotes/overview/), [Blocos de código](/manual/content/blocks/code-blocks/overview/), [Diagramas Mermaid](/manual/content/blocks/mermaid-diagrams/overview/)
48
- - [Imagens](/manual/content/blocks/images/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expansível](/manual/content/blocks/expandable/overview/), [Tabelas](/manual/content/blocks/tables/overview/), [HTML bruto](/manual/content/blocks/raw-html/overview/) e [Quick Links](/manual/content/blocks/quick-links/overview/)
48
+ - [Imagens](/manual/content/blocks/images/overview/), [Arquivos](/manual/content/blocks/files/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expansível](/manual/content/blocks/expandable/overview/), [Tabelas](/manual/content/blocks/tables/overview/), [HTML bruto](/manual/content/blocks/raw-html/overview/) e [Quick Links](/manual/content/blocks/quick-links/overview/)
49
49
 
50
50
  ### Títulos
51
51
 
@@ -132,6 +132,20 @@ Defina `open="true"` quando o bloco precisar começar aberto.
132
132
 
133
133
  O corpo do expansível suporta parágrafos, listas, alertas, blocos de código, diagramas Mermaid, tabelas, HTML bruto e quick links. Nesta primeira versão, mantenha títulos fora do bloco expansível, porque títulos dentro do corpo viram parágrafos comuns para preservar o ToC da página.
134
134
 
135
+ ### Blocos de Arquivo
136
+
137
+ Use `<d-file>` para publicar anexos baixáveis a partir de arquivos versionados no repositório ou de storage externo sem sair do Markdown:
138
+
139
+ ```html
140
+ <d-file src="/files/manual/release-checklist.txt" title="Checklist de release" size="1 KB">
141
+ Baixe o anexo de exemplo usado neste manual.
142
+ </d-file>
143
+ ```
144
+
145
+ Guarde arquivos versionados no repositório em `public/files/` e prefira caminhos absolutos do site, como `/files/...`.
146
+
147
+ `title` e `size` são opcionais. Quando `title` não é informado, a UI usa o nome do arquivo presente em `src`. O corpo do bloco funciona como legenda com Markdown inline, e a mesma sintaxe também aceita URLs externas caso você mova o storage para R2 ou outro CDN no futuro.
148
+
135
149
  ## Adicionando um Novo Idioma
136
150
 
137
151
  1. Crie `src/i18n/languages/xx-XX.hjson` com todas as traduções de UI
@@ -0,0 +1,27 @@
1
+ ## Overview
2
+
3
+ Files render a download card directly inside Markdown so a page or subpage can publish attachments without leaving the normal authoring flow.
4
+
5
+ They are useful for checklists, sample bundles, PDFs, release notes, and any other file that should be linked from the reading flow.
6
+
7
+ ## Markdown Syntax
8
+
9
+ ```html
10
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
11
+ Download the example attachment used in this manual.
12
+ </d-file>
13
+ ```
14
+
15
+ You can also omit the caption body when the file name already provides enough context:
16
+
17
+ ```html
18
+ <d-file src="/files/manual/release-checklist.txt" size="1 KB" />
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ - Store small repo-tracked attachments under `public/files/` and prefer absolute paths such as `/files/...`.
24
+ - `src` is required. `title` and `size` are optional.
25
+ - When `title` is omitted, the rendered card falls back to the file name from `src`.
26
+ - The block body is rendered as an inline Markdown caption.
27
+ - External URLs also work, so the same syntax can point to a future R2 bucket or another CDN.
@@ -0,0 +1,27 @@
1
+ ## Visão Geral
2
+
3
+ Arquivos renderizam um card de download diretamente no Markdown para que uma page ou subpage publique anexos sem sair do fluxo normal de autoria.
4
+
5
+ Eles são úteis para checklists, bundles de exemplo, PDFs, notas de release e qualquer outro arquivo que precise ficar no fluxo de leitura.
6
+
7
+ ## Sintaxe em Markdown
8
+
9
+ ```html
10
+ <d-file src="/files/manual/release-checklist.txt" title="Checklist de release" size="1 KB">
11
+ Baixe o anexo de exemplo usado neste manual.
12
+ </d-file>
13
+ ```
14
+
15
+ Você também pode omitir o corpo da legenda quando o nome do arquivo já fornece contexto suficiente:
16
+
17
+ ```html
18
+ <d-file src="/files/manual/release-checklist.txt" size="1 KB" />
19
+ ```
20
+
21
+ ## Observações
22
+
23
+ - Guarde anexos pequenos versionados no repositório em `public/files/` e prefira caminhos absolutos como `/files/...`.
24
+ - `src` é obrigatório. `title` e `size` são opcionais.
25
+ - Quando `title` não é informado, o card renderizado usa o nome do arquivo presente em `src`.
26
+ - O corpo do bloco é renderizado como legenda com Markdown inline.
27
+ - URLs externas também funcionam, então a mesma sintaxe pode apontar no futuro para um bucket R2 ou outro CDN.
@@ -0,0 +1,17 @@
1
+ ## Showcase
2
+
3
+ ### Local File with Caption
4
+
5
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
6
+ Download the example checklist published from `public/files/manual`.
7
+ </d-file>
8
+
9
+ ### Local File with Automatic Size
10
+
11
+ <d-file src="/files/manual/release-checklist.txt" />
12
+
13
+ ### External File URL
14
+
15
+ <d-file src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" title="Reference PDF" size="13 KB">
16
+ The same block can point to a CDN or object storage URL without changing the page layout.
17
+ </d-file>
@@ -0,0 +1,17 @@
1
+ ## Demonstração
2
+
3
+ ### Arquivo Local com Legenda
4
+
5
+ <d-file src="/files/manual/release-checklist.txt" title="Checklist de release" size="1 KB">
6
+ Baixe o checklist de exemplo publicado a partir de `public/files/manual`.
7
+ </d-file>
8
+
9
+ ### Arquivo Local com Tamanho Automático
10
+
11
+ <d-file src="/files/manual/release-checklist.txt" />
12
+
13
+ ### URL Externa de Arquivo
14
+
15
+ <d-file src="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" title="PDF de referência" size="13 KB">
16
+ O mesmo bloco pode apontar para um CDN ou URL de object storage sem mudar o layout da página.
17
+ </d-file>
@@ -542,6 +542,35 @@ export default {
542
542
  }
543
543
  },
544
544
 
545
+ '/content/blocks/files': {
546
+ config: {
547
+ icon: 'attach_file',
548
+ status: 'new',
549
+ version: 'v3.3.0',
550
+ meta: {
551
+ description: {
552
+ 'en-US': 'Files — Documentation of Docsector Reader',
553
+ 'pt-BR': 'Arquivos — Documentacao do Docsector Reader'
554
+ }
555
+ },
556
+ book: 'manual',
557
+ menu: {},
558
+ subpages: {
559
+ showcase: true
560
+ }
561
+ },
562
+ data: {
563
+ 'en-US': { title: 'Files' },
564
+ 'pt-BR': { title: 'Arquivos' }
565
+ },
566
+ metadata: {
567
+ tags: {
568
+ 'en-US': 'files download attachments markdown assets public r2 cloudflare github',
569
+ 'pt-BR': 'arquivos download anexos markdown assets public r2 cloudflare github'
570
+ }
571
+ }
572
+ },
573
+
545
574
  '/content/blocks/math-and-tex': {
546
575
  config: {
547
576
  icon: 'functions',