@docsector/docsector-reader 4.0.1 → 4.2.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.
Files changed (29) hide show
  1. package/README.md +19 -0
  2. package/bin/docsector.js +1 -1
  3. package/package.json +1 -1
  4. package/public/api/manual/http-client.json +91 -0
  5. package/public/quasar-api/QSeparator.json +39 -0
  6. package/src/components/DBlockApi.vue +634 -0
  7. package/src/components/DBlockApiEntry.js +623 -0
  8. package/src/components/DBlockCodeExample.vue +445 -0
  9. package/src/components/DBlockSourceCode.vue +3 -11
  10. package/src/components/DMenu.vue +70 -25
  11. package/src/components/DPageTokens.vue +22 -0
  12. package/src/components/api-block-model.js +326 -0
  13. package/src/components/code-block-highlighting.js +16 -0
  14. package/src/components/code-example-source.js +363 -0
  15. package/src/components/page-section-tokens.js +141 -1
  16. package/src/components/source-code-lines.js +17 -0
  17. package/src/examples/manual/code-examples/BasicCounter.vue +63 -0
  18. package/src/examples/manual/code-examples/InlineNotice.vue +60 -0
  19. package/src/pages/manual/content/blocks/api-reference.overview.en-US.md +40 -0
  20. package/src/pages/manual/content/blocks/api-reference.overview.pt-BR.md +40 -0
  21. package/src/pages/manual/content/blocks/api-reference.showcase.en-US.md +33 -0
  22. package/src/pages/manual/content/blocks/api-reference.showcase.pt-BR.md +33 -0
  23. package/src/pages/manual/content/blocks/code-examples.overview.en-US.md +56 -0
  24. package/src/pages/manual/content/blocks/code-examples.overview.pt-BR.md +56 -0
  25. package/src/pages/manual/content/blocks/code-examples.showcase.en-US.md +38 -0
  26. package/src/pages/manual/content/blocks/code-examples.showcase.pt-BR.md +38 -0
  27. package/src/pages/manual.index.js +56 -0
  28. package/src/quasar.factory.js +77 -0
  29. package/src/store/Page.js +26 -2
@@ -0,0 +1,445 @@
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-button-bg: #edf3ee
331
+ --d-code-example-button-border: rgba(37, 67, 45, 0.12)
332
+ --d-code-example-button-hover-bg: #e2ebe4
333
+ --d-code-example-toolbar-bg: #f6f8f5
334
+ --d-code-example-toolbar-text: #26352b
335
+ --d-code-example-preview-bg: #ffffff
336
+ --d-code-example-caption: #405148
337
+ --d-code-example-muted: #5d7563
338
+
339
+ body.body--dark
340
+ --d-code-example-bg: #111512
341
+ --d-code-example-border: rgba(197, 220, 200, 0.18)
342
+ --d-code-example-button-bg: #29342d
343
+ --d-code-example-button-border: rgba(197, 220, 200, 0.18)
344
+ --d-code-example-button-hover-bg: #314036
345
+ --d-code-example-toolbar-bg: #1a211c
346
+ --d-code-example-toolbar-text: #e8efe9
347
+ --d-code-example-preview-bg: #0c0f0d
348
+ --d-code-example-caption: #c7d4ca
349
+ --d-code-example-muted: #9aafa0
350
+
351
+ .d-block-code-example
352
+ background: var(--d-code-example-bg)
353
+ border: 1px solid var(--d-code-example-border)
354
+ border-radius: 6px
355
+ box-shadow: 0 1px 1px rgb(0 0 0 / 8%)
356
+ margin: 18px 0
357
+ max-width: calc(100vw - 40px)
358
+ overflow: hidden
359
+
360
+ &__toolbar
361
+ align-items: center
362
+ background: var(--d-code-example-toolbar-bg)
363
+ color: var(--d-code-example-toolbar-text)
364
+ display: flex
365
+ gap: 4px
366
+ min-height: 42px
367
+ min-width: 0
368
+ padding: 4px 8px 4px 14px
369
+
370
+ &__title
371
+ font-size: 14px
372
+ font-weight: 600
373
+ line-height: 20px
374
+ min-width: 0
375
+ overflow: hidden
376
+ text-overflow: ellipsis
377
+ white-space: nowrap
378
+
379
+ &__button
380
+ background: var(--d-code-example-button-bg)
381
+ border: 1px solid var(--d-code-example-button-border)
382
+ border-radius: 999px
383
+ color: var(--d-code-example-muted)
384
+ flex: 0 0 auto
385
+ min-height: 30px
386
+ min-width: 30px
387
+ transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease
388
+
389
+ &:hover,
390
+ &:focus-visible
391
+ background: var(--d-code-example-button-hover-bg)
392
+ color: var(--d-code-example-toolbar-text)
393
+
394
+ &.q-btn--disabled,
395
+ &[disabled]
396
+ opacity: 0.55
397
+
398
+ &__source
399
+ border-top: 1px solid var(--d-code-example-border)
400
+ border-bottom: 1px solid var(--d-code-example-border)
401
+
402
+ .source-code
403
+ box-shadow: none
404
+ margin: 0
405
+ max-width: 100%
406
+
407
+ .source-code-frame
408
+ border: 0
409
+ border-radius: 0
410
+
411
+ &__preview
412
+ background: var(--d-code-example-preview-bg)
413
+ min-height: 96px
414
+ min-width: 0
415
+ overflow: hidden
416
+ position: relative
417
+
418
+ &--scrollable
419
+ overflow-y: auto
420
+
421
+ &--overflow
422
+ overflow: auto
423
+
424
+ &__component
425
+ display: block
426
+ min-width: 0
427
+
428
+ &__fallback
429
+ align-items: center
430
+ color: var(--d-code-example-muted)
431
+ display: flex
432
+ gap: 8px
433
+ min-height: 96px
434
+ padding: 18px
435
+
436
+ &__caption
437
+ border-top: 1px solid var(--d-code-example-border)
438
+ color: var(--d-code-example-caption)
439
+ font-size: 14px
440
+ line-height: 1.55
441
+ padding: 10px 14px 12px
442
+
443
+ p
444
+ margin: 0
445
+ </style>
@@ -2,13 +2,8 @@
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'
6
+ import { countRenderedCodeLines } from './source-code-lines'
12
7
  import { looksLikeFileName, resolveFileIconUrl } from '../composables/useFileIcon'
13
8
 
14
9
  defineOptions({
@@ -101,10 +96,7 @@ const activeBreadcrumbItems = computed(() => {
101
96
  })
102
97
  const activeBreadcrumbTitle = computed(() => activeBreadcrumbs.value.join(' > '))
103
98
  const hasBreadcrumbs = computed(() => activeBreadcrumbs.value.length > 0)
104
- const lines = computed(() => {
105
- const splited = activeText.value.split(/\r\n|\n/)
106
- return splited.length - 1
107
- })
99
+ const lines = computed(() => countRenderedCodeLines(activeText.value))
108
100
  const showHeader = computed(() => hasTabs.value || hasBreadcrumbs.value || (lines.value && lines.value > 1))
109
101
  const codeLanguageClass = computed(() => `language-${activeLanguage.value}`)
110
102
  const highlighted = computed(() => {
@@ -22,6 +22,8 @@ const term = ref(null)
22
22
  const founds = ref(false)
23
23
  const items = ref([])
24
24
  const scrolling = ref(null)
25
+ const isMenuHovered = ref(false)
26
+ const pendingScroll = ref(false)
25
27
 
26
28
  const subpage = computed(() => {
27
29
  const parent = $route.matched[0]?.path
@@ -312,41 +314,79 @@ const getMenuItemHeaderLabel = (meta) => {
312
314
  return label // String raw
313
315
  }
314
316
 
317
+ const executeScrollToActiveMenuItem = () => {
318
+ const menu = document.getElementById('menu')
319
+ if (!menu) {
320
+ return
321
+ }
322
+
323
+ const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
+ if (!menuItemActive || typeof menuItemActive !== 'object') {
325
+ return
326
+ }
327
+
328
+ const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
329
+ const offsetTop2 = menuItemActive.offsetTop
330
+
331
+ const innerHeightBy2 = window.innerHeight / 2
332
+
333
+ const searchBarHeight = 50
334
+ let expansionHeaderHeight = 0
335
+ if (offsetTop1 > 0) {
336
+ expansionHeaderHeight = 45
337
+ }
338
+ const fixedHeight = searchBarHeight + expansionHeaderHeight
339
+
340
+ const target = scroll.getScrollTarget(menuItemActive)
341
+ const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
342
+ const duration = 300
343
+
344
+ if (offset > 0) {
345
+ scroll.setVerticalScrollPosition(target, offset, duration)
346
+ }
347
+ }
348
+
349
+ const flushPendingMenuScroll = () => {
350
+ if (!pendingScroll.value || isMenuHovered.value) {
351
+ return
352
+ }
353
+
354
+ if (scrolling.value) {
355
+ clearTimeout(scrolling.value)
356
+ scrolling.value = null
357
+ }
358
+
359
+ pendingScroll.value = false
360
+ executeScrollToActiveMenuItem()
361
+ }
362
+
315
363
  const scrollToActiveMenuItem = () => {
364
+ pendingScroll.value = true
365
+
316
366
  if (scrolling.value) {
317
367
  clearTimeout(scrolling.value)
368
+ scrolling.value = null
369
+ }
370
+
371
+ if (isMenuHovered.value) {
372
+ return
318
373
  }
319
374
 
320
375
  scrolling.value = setTimeout(() => {
321
- const menu = document.getElementById('menu')
322
- if (menu) {
323
- const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
- if (menuItemActive && typeof menuItemActive === 'object') {
325
- const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
326
- const offsetTop2 = menuItemActive.offsetTop
327
-
328
- const innerHeightBy2 = window.innerHeight / 2
329
-
330
- const searchBarHeight = 50
331
- let expansionHeaderHeight = 0
332
- if (offsetTop1 > 0) {
333
- expansionHeaderHeight = 45
334
- }
335
- const fixedHeight = searchBarHeight + expansionHeaderHeight
336
-
337
- const target = scroll.getScrollTarget(menuItemActive)
338
- const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
339
- const duration = 300
340
-
341
- if (offset > 0) {
342
- scroll.setVerticalScrollPosition(target, offset, duration)
343
- }
344
- }
345
- }
346
376
  scrolling.value = null
377
+ flushPendingMenuScroll()
347
378
  }, 1500)
348
379
  }
349
380
 
381
+ const handleMenuMouseEnter = () => {
382
+ isMenuHovered.value = true
383
+ }
384
+
385
+ const handleMenuMouseLeave = () => {
386
+ isMenuHovered.value = false
387
+ flushPendingMenuScroll()
388
+ }
389
+
350
390
  onMounted(() => {
351
391
  scrollToActiveMenuItem()
352
392
 
@@ -361,6 +401,9 @@ onBeforeUnmount(() => {
361
401
  if (scrolling.value) {
362
402
  clearTimeout(scrolling.value)
363
403
  }
404
+
405
+ isMenuHovered.value = false
406
+ pendingScroll.value = false
364
407
  })
365
408
 
366
409
  const buildMenuItems = () => {
@@ -428,6 +471,8 @@ watch([currentBookId, activeVersionId], rebuildItems)
428
471
  </transition>
429
472
 
430
473
  <q-scroll-area id="menu"
474
+ @mouseenter="handleMenuMouseEnter"
475
+ @mouseleave="handleMenuMouseLeave"
431
476
  :visible="true"
432
477
  :class="$q.dark.isActive ? '' : 'bg-grey-2'"
433
478
  >
@@ -34,6 +34,8 @@ 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'
38
+ import DBlockApi from './DBlockApi.vue'
37
39
  </script>
38
40
 
39
41
  <template>
@@ -135,6 +137,26 @@ import DBlockStepper from './DBlockStepper.vue'
135
137
  :tabs="token.tabs"
136
138
  />
137
139
 
140
+ <d-block-code-example
141
+ v-else-if="token.tag === 'code-example'"
142
+ :index="id + token.codeIndex"
143
+ :src="token.src"
144
+ :title="token.title"
145
+ :caption="token.caption"
146
+ :expanded="token.expanded"
147
+ :codepen="token.codepen"
148
+ :scrollable="token.scrollable"
149
+ :overflow="token.overflow"
150
+ :height="token.height"
151
+ />
152
+
153
+ <d-block-api
154
+ v-else-if="token.tag === 'api'"
155
+ :src="token.src"
156
+ :title="token.title"
157
+ :page-link="token.pageLink"
158
+ />
159
+
138
160
  <d-block-mermaid-diagram
139
161
  v-else-if="token.tag === 'mermaid'"
140
162
  :content="token.content"