@docsector/docsector-reader 4.0.1 โ†’ 4.1.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 CHANGED
@@ -79,6 +79,7 @@ Transform Markdown content into beautiful, navigable documentation sites โ€” wit
79
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
80
80
  - ๐Ÿท๏ธ **Version Selector Badges** โ€” Every version in the sidebar selector displays a color-coded badge: green for released, orange for draft, red for deprecated; fully customizable via `badge: { label, color, textColor }`
81
81
  - ๐Ÿ“‚ **Tabbed Code Blocks** โ€” Group consecutive fenced code blocks into tabs using the `group` and `tab` attributes in the fence info line
82
+ - ๐Ÿงช **Live Code Example Blocks** โ€” Use `<d-block-code-example src="..." />` to render bundled Vue SFC examples with a live preview, GitHub source link, source toggle, and CodePen export for compatible examples
82
83
  - ๐Ÿž **Breadcrumb Path Display** โ€” Show a file path breadcrumb above code blocks with the `breadcrumb` attribute; renders as clickable path segments
83
84
  - ๐ŸŽจ **File Type Icons** โ€” Automatically resolves file extension or filename to a Material Icon Theme SVG icon, shown inline in tabs and beside the last breadcrumb segment
84
85
  - โš™๏ธ **Single Config File** โ€” Customize branding, links, and languages via `docsector.config.js`
@@ -1023,6 +1024,22 @@ Notes:
1023
1024
  Supported alert types: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`.
1024
1025
  Regular blockquotes without `[!TYPE]` continue to work normally.
1025
1026
 
1027
+ ### Code Example Blocks
1028
+
1029
+ ```html
1030
+ <d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter">
1031
+ Optional caption rendered as inline Markdown.
1032
+ </d-block-code-example>
1033
+ ```
1034
+
1035
+ Notes:
1036
+
1037
+ - Store live examples as Vue SFCs under `src/examples/**/*.vue`; for example, `src="manual/code-examples/basic-counter"` resolves `src/examples/manual/code-examples/BasicCounter.vue` after kebab-case normalization.
1038
+ - Readers get a live preview, a GitHub button for the example SFC, a source button with Template / Script / Style / All tabs, and a CodePen button when the example can be exported safely.
1039
+ - Use `expanded="true"` only when the source code should be visible by default.
1040
+ - CodePen export currently supports plain Vue SFCs with a template, optional style, and an Options API `export default` script. Named imports from `vue` and `quasar` are converted to browser globals.
1041
+ - Examples using `<script setup>`, TypeScript scripts, or local imports still render in Docsector, but the CodePen action is disabled. Use `codepen="false"` to hide it intentionally.
1042
+
1026
1043
  ### File Attachment Blocks
1027
1044
 
1028
1045
  ```html
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 = '4.0.1'
26
+ const VERSION = '4.1.0'
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": "4.0.1",
3
+ "version": "4.1.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,423 @@
1
+ <script setup>
2
+ import { computed, markRaw, ref, watch } from 'vue'
3
+ import { openURL, Quasar, useQuasar } from 'quasar'
4
+
5
+ import { resolveCodeExample } from 'virtual:docsector-code-examples'
6
+ import docsectorConfig from 'docsector.config.js'
7
+
8
+ import DBlockSourceCode from './DBlockSourceCode.vue'
9
+ import {
10
+ canCreateCodepenPayload,
11
+ createCodeExampleGitHubUrl,
12
+ createCodeExampleTabs,
13
+ createCodepenPayload,
14
+ getCodepenUnsupportedReason
15
+ } from './code-example-source'
16
+
17
+ defineOptions({
18
+ name: 'DBlockCodeExample'
19
+ })
20
+
21
+ const props = defineProps({
22
+ index: {
23
+ type: Number,
24
+ required: true
25
+ },
26
+ src: {
27
+ type: String,
28
+ default: ''
29
+ },
30
+ title: {
31
+ type: String,
32
+ default: ''
33
+ },
34
+ caption: {
35
+ type: String,
36
+ default: ''
37
+ },
38
+ expanded: {
39
+ type: Boolean,
40
+ default: false
41
+ },
42
+ codepen: {
43
+ type: Boolean,
44
+ default: true
45
+ },
46
+ scrollable: {
47
+ type: Boolean,
48
+ default: false
49
+ },
50
+ overflow: {
51
+ type: Boolean,
52
+ default: false
53
+ },
54
+ height: {
55
+ type: String,
56
+ default: ''
57
+ }
58
+ })
59
+
60
+ const $q = useQuasar()
61
+
62
+ const isBusy = ref(false)
63
+ const errorMessage = ref('')
64
+ const component = ref(null)
65
+ const sourceText = ref('')
66
+ const sourceTabs = ref([])
67
+ const sourceOpen = ref(props.expanded)
68
+ const exampleFilePath = ref('')
69
+ let requestIndex = 0
70
+
71
+ const displayTitle = computed(() => props.title || props.src || 'Code example')
72
+ const frameTone = computed(() => $q.dark.isActive ? 'dark' : 'light')
73
+ const hasSource = computed(() => sourceText.value.trim().length > 0)
74
+ const codepenUnsupportedReason = computed(() => {
75
+ if (!hasSource.value) {
76
+ return 'Source code is still loading.'
77
+ }
78
+
79
+ return getCodepenUnsupportedReason(sourceText.value)
80
+ })
81
+ const canOpenCodepen = computed(() => props.codepen && hasSource.value && canCreateCodepenPayload(sourceText.value))
82
+ const codepenTooltip = computed(() => canOpenCodepen.value ? 'Edit in CodePen' : codepenUnsupportedReason.value)
83
+ const githubUrl = computed(() => createCodeExampleGitHubUrl(exampleFilePath.value, docsectorConfig))
84
+ const canOpenGitHub = computed(() => githubUrl.value !== '')
85
+ const previewStyle = computed(() => {
86
+ const style = {}
87
+ const normalizedHeight = normalizeCssLength(props.height)
88
+
89
+ if (normalizedHeight) {
90
+ style.height = normalizedHeight
91
+ } else if (props.scrollable) {
92
+ style.height = '500px'
93
+ }
94
+
95
+ return style
96
+ })
97
+
98
+ watch(() => props.expanded, (value) => {
99
+ sourceOpen.value = value
100
+ })
101
+
102
+ watch(() => props.src, () => {
103
+ loadExample()
104
+ }, { immediate: true })
105
+
106
+ function normalizeCssLength (value = '') {
107
+ const normalized = String(value || '').trim()
108
+
109
+ if (!normalized) {
110
+ return ''
111
+ }
112
+
113
+ if (/^-?\d+(?:\.\d+)?$/.test(normalized)) {
114
+ return `${normalized}px`
115
+ }
116
+
117
+ return normalized
118
+ }
119
+
120
+ async function loadExample () {
121
+ const currentRequest = ++requestIndex
122
+ const resolved = resolveCodeExample(props.src)
123
+
124
+ isBusy.value = true
125
+ errorMessage.value = ''
126
+ component.value = null
127
+ sourceText.value = ''
128
+ sourceTabs.value = []
129
+ exampleFilePath.value = ''
130
+
131
+ if (!props.src) {
132
+ errorMessage.value = 'Code example source is missing.'
133
+ isBusy.value = false
134
+ return
135
+ }
136
+
137
+ if (!resolved.exists || typeof resolved.loadComponent !== 'function' || typeof resolved.loadSource !== 'function') {
138
+ errorMessage.value = `Code example not found: ${resolved.id || props.src}`
139
+ isBusy.value = false
140
+ return
141
+ }
142
+
143
+ try {
144
+ const [componentModule, rawSource] = await Promise.all([
145
+ resolved.loadComponent(),
146
+ resolved.loadSource()
147
+ ])
148
+
149
+ if (currentRequest !== requestIndex) {
150
+ return
151
+ }
152
+
153
+ component.value = markRaw(componentModule.default || componentModule)
154
+ sourceText.value = String(rawSource || '')
155
+ sourceTabs.value = createCodeExampleTabs(sourceText.value)
156
+ exampleFilePath.value = resolved.filePath || ''
157
+ } catch (err) {
158
+ if (currentRequest !== requestIndex) {
159
+ return
160
+ }
161
+
162
+ errorMessage.value = err?.message || `Unable to load code example: ${props.src}`
163
+ } finally {
164
+ if (currentRequest === requestIndex) {
165
+ isBusy.value = false
166
+ }
167
+ }
168
+ }
169
+
170
+ function toggleSource () {
171
+ sourceOpen.value = !sourceOpen.value
172
+ }
173
+
174
+ function submitCodepenPayload (payload) {
175
+ if (typeof document === 'undefined') {
176
+ return
177
+ }
178
+
179
+ const form = document.createElement('form')
180
+ const input = document.createElement('input')
181
+
182
+ form.method = 'post'
183
+ form.action = 'https://codepen.io/pen/define/'
184
+ form.target = '_blank'
185
+ form.rel = 'noopener'
186
+ form.style.display = 'none'
187
+
188
+ input.type = 'hidden'
189
+ input.name = 'data'
190
+ input.value = JSON.stringify(payload)
191
+
192
+ form.appendChild(input)
193
+ document.body.appendChild(form)
194
+ form.submit()
195
+ form.remove()
196
+ }
197
+
198
+ function openCodepen () {
199
+ if (!canOpenCodepen.value) {
200
+ return
201
+ }
202
+
203
+ const sourceUrl = typeof window === 'undefined'
204
+ ? ''
205
+ : `${window.location.origin}${window.location.pathname}${window.location.hash}`
206
+
207
+ submitCodepenPayload(createCodepenPayload(sourceText.value, {
208
+ title: displayTitle.value,
209
+ quasarVersion: Quasar.version,
210
+ sourceUrl
211
+ }))
212
+ }
213
+
214
+ function openGitHub () {
215
+ if (canOpenGitHub.value) {
216
+ openURL(githubUrl.value)
217
+ }
218
+ }
219
+ </script>
220
+
221
+ <template>
222
+ <div
223
+ class="d-block-code-example"
224
+ :class="`d-block-code-example--${frameTone}`"
225
+ >
226
+ <div class="d-block-code-example__toolbar">
227
+ <div class="d-block-code-example__title">{{ displayTitle }}</div>
228
+
229
+ <q-space />
230
+
231
+ <q-btn
232
+ class="d-block-code-example__button"
233
+ dense
234
+ flat
235
+ round
236
+ icon="fab fa-github"
237
+ :disable="!canOpenGitHub"
238
+ aria-label="View example on GitHub"
239
+ @click="openGitHub"
240
+ >
241
+ <q-tooltip>{{ canOpenGitHub ? 'View on GitHub' : 'GitHub source is unavailable' }}</q-tooltip>
242
+ </q-btn>
243
+
244
+ <q-btn
245
+ v-if="codepen"
246
+ class="d-block-code-example__button"
247
+ dense
248
+ flat
249
+ round
250
+ icon="fab fa-codepen"
251
+ :disable="!canOpenCodepen"
252
+ aria-label="Edit in CodePen"
253
+ @click="openCodepen"
254
+ >
255
+ <q-tooltip>{{ codepenTooltip }}</q-tooltip>
256
+ </q-btn>
257
+
258
+ <q-btn
259
+ class="d-block-code-example__button"
260
+ dense
261
+ flat
262
+ round
263
+ icon="code"
264
+ :disable="!hasSource"
265
+ :aria-label="sourceOpen ? 'Hide source' : 'View source'"
266
+ @click="toggleSource"
267
+ >
268
+ <q-tooltip>{{ sourceOpen ? 'Hide source' : 'View source' }}</q-tooltip>
269
+ </q-btn>
270
+ </div>
271
+
272
+ <q-slide-transition>
273
+ <div
274
+ v-show="sourceOpen && hasSource"
275
+ class="d-block-code-example__source"
276
+ >
277
+ <d-block-source-code
278
+ :index="index"
279
+ language="vue"
280
+ :text="sourceText"
281
+ :tabs="sourceTabs"
282
+ />
283
+ </div>
284
+ </q-slide-transition>
285
+
286
+ <q-linear-progress
287
+ v-if="isBusy"
288
+ color="primary"
289
+ indeterminate
290
+ />
291
+
292
+ <div
293
+ class="d-block-code-example__preview"
294
+ :class="{
295
+ 'd-block-code-example__preview--scrollable': scrollable,
296
+ 'd-block-code-example__preview--overflow': overflow
297
+ }"
298
+ :style="previewStyle"
299
+ >
300
+ <component
301
+ v-if="component && !errorMessage"
302
+ :is="component"
303
+ class="d-block-code-example__component"
304
+ />
305
+
306
+ <div
307
+ v-else-if="errorMessage"
308
+ class="d-block-code-example__fallback"
309
+ >
310
+ <q-icon
311
+ name="warning"
312
+ size="22px"
313
+ />
314
+ <span>{{ errorMessage }}</span>
315
+ </div>
316
+ </div>
317
+
318
+ <div
319
+ v-if="caption"
320
+ class="d-block-code-example__caption"
321
+ v-html="caption"
322
+ ></div>
323
+ </div>
324
+ </template>
325
+
326
+ <style lang="sass">
327
+ body.body--light
328
+ --d-code-example-bg: #ffffff
329
+ --d-code-example-border: rgba(37, 67, 45, 0.16)
330
+ --d-code-example-toolbar-bg: #f6f8f5
331
+ --d-code-example-toolbar-text: #26352b
332
+ --d-code-example-preview-bg: #ffffff
333
+ --d-code-example-caption: #405148
334
+ --d-code-example-muted: #5d7563
335
+
336
+ body.body--dark
337
+ --d-code-example-bg: #111512
338
+ --d-code-example-border: rgba(197, 220, 200, 0.18)
339
+ --d-code-example-toolbar-bg: #1a211c
340
+ --d-code-example-toolbar-text: #e8efe9
341
+ --d-code-example-preview-bg: #0c0f0d
342
+ --d-code-example-caption: #c7d4ca
343
+ --d-code-example-muted: #9aafa0
344
+
345
+ .d-block-code-example
346
+ background: var(--d-code-example-bg)
347
+ border: 1px solid var(--d-code-example-border)
348
+ border-radius: 6px
349
+ box-shadow: 0 1px 1px rgb(0 0 0 / 8%)
350
+ margin: 18px 0
351
+ max-width: calc(100vw - 40px)
352
+ overflow: hidden
353
+
354
+ &__toolbar
355
+ align-items: center
356
+ background: var(--d-code-example-toolbar-bg)
357
+ color: var(--d-code-example-toolbar-text)
358
+ display: flex
359
+ gap: 4px
360
+ min-height: 42px
361
+ min-width: 0
362
+ padding: 4px 8px 4px 14px
363
+
364
+ &__title
365
+ font-size: 14px
366
+ font-weight: 600
367
+ line-height: 20px
368
+ min-width: 0
369
+ overflow: hidden
370
+ text-overflow: ellipsis
371
+ white-space: nowrap
372
+
373
+ &__button
374
+ color: var(--d-code-example-muted)
375
+ flex: 0 0 auto
376
+
377
+ &__source
378
+ border-top: 1px solid var(--d-code-example-border)
379
+
380
+ .source-code
381
+ box-shadow: none
382
+ margin: 0
383
+ max-width: 100%
384
+
385
+ .source-code-frame
386
+ border: 0
387
+ border-radius: 0
388
+
389
+ &__preview
390
+ background: var(--d-code-example-preview-bg)
391
+ min-height: 96px
392
+ min-width: 0
393
+ overflow: hidden
394
+ position: relative
395
+
396
+ &--scrollable
397
+ overflow-y: auto
398
+
399
+ &--overflow
400
+ overflow: auto
401
+
402
+ &__component
403
+ display: block
404
+ min-width: 0
405
+
406
+ &__fallback
407
+ align-items: center
408
+ color: var(--d-code-example-muted)
409
+ display: flex
410
+ gap: 8px
411
+ min-height: 96px
412
+ padding: 18px
413
+
414
+ &__caption
415
+ border-top: 1px solid var(--d-code-example-border)
416
+ color: var(--d-code-example-caption)
417
+ font-size: 14px
418
+ line-height: 1.55
419
+ padding: 10px 14px 12px
420
+
421
+ p
422
+ margin: 0
423
+ </style>
@@ -2,13 +2,7 @@
2
2
  import { ref, computed, watch } from 'vue'
3
3
  import { useQuasar } from 'quasar'
4
4
  import { useStore } from 'vuex'
5
- import Prism from 'prismjs'
6
- // @ Load Prism languages
7
- import 'prismjs/components/prism-markup-templating' // dependency for prism-php extension
8
- // PHP
9
- import 'prismjs/components/prism-php'
10
- // Bash
11
- import 'prismjs/components/prism-bash'
5
+ import Prism from './code-block-highlighting'
12
6
  import { looksLikeFileName, resolveFileIconUrl } from '../composables/useFileIcon'
13
7
 
14
8
  defineOptions({
@@ -34,6 +34,7 @@ import DBlockQuickLinks from './DBlockQuickLinks.vue'
34
34
  import DBlockTimeline from './DBlockTimeline.vue'
35
35
  import DBlockExpandable from './DBlockExpandable.vue'
36
36
  import DBlockStepper from './DBlockStepper.vue'
37
+ import DBlockCodeExample from './DBlockCodeExample.vue'
37
38
  </script>
38
39
 
39
40
  <template>
@@ -135,6 +136,19 @@ import DBlockStepper from './DBlockStepper.vue'
135
136
  :tabs="token.tabs"
136
137
  />
137
138
 
139
+ <d-block-code-example
140
+ v-else-if="token.tag === 'code-example'"
141
+ :index="id + token.codeIndex"
142
+ :src="token.src"
143
+ :title="token.title"
144
+ :caption="token.caption"
145
+ :expanded="token.expanded"
146
+ :codepen="token.codepen"
147
+ :scrollable="token.scrollable"
148
+ :overflow="token.overflow"
149
+ :height="token.height"
150
+ />
151
+
138
152
  <d-block-mermaid-diagram
139
153
  v-else-if="token.tag === 'mermaid'"
140
154
  :content="token.content"
@@ -0,0 +1,16 @@
1
+ import Prism from 'prismjs'
2
+
3
+ import 'prismjs/components/prism-markup'
4
+ import 'prismjs/components/prism-markup-templating'
5
+ import 'prismjs/components/prism-javascript'
6
+ import 'prismjs/components/prism-css'
7
+ import 'prismjs/components/prism-php'
8
+ import 'prismjs/components/prism-bash'
9
+
10
+ if (!Prism.languages.vue && Prism.languages.markup?.tag?.addInlined) {
11
+ Prism.languages.markup.tag.addInlined('script', 'javascript')
12
+ Prism.languages.markup.tag.addInlined('style', 'css')
13
+ Prism.languages.vue = Prism.languages.markup
14
+ }
15
+
16
+ export default Prism