@docsector/docsector-reader 4.0.0 → 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.
@@ -0,0 +1,363 @@
1
+ const PART_LABELS = Object.freeze({
2
+ template: 'Template',
3
+ script: 'Script',
4
+ style: 'Style'
5
+ })
6
+
7
+ const PART_ORDER = ['Template', 'Script', 'Style']
8
+ const ALLOWED_CODEPEN_IMPORTS = new Set(['vue', 'quasar'])
9
+
10
+ const createSfcOpeningTagPattern = () => /<(template|script|style)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi
11
+
12
+ const createSameTagPattern = (tag) => new RegExp(`</?${tag}\\b((?:"[^"]*"|'[^']*'|[^'">])*)>`, 'gi')
13
+
14
+ const findSfcBlockRange = (source = '', tag = '', searchStart = 0) => {
15
+ const pattern = createSameTagPattern(tag)
16
+ pattern.lastIndex = searchStart
17
+
18
+ let depth = 1
19
+ let match = pattern.exec(source)
20
+
21
+ while (match !== null) {
22
+ const token = match[0]
23
+ const isClosing = token.startsWith('</')
24
+ const isSelfClosing = /\/\s*>$/.test(token)
25
+
26
+ if (isClosing) {
27
+ depth--
28
+ } else if (!isSelfClosing) {
29
+ depth++
30
+ }
31
+
32
+ if (depth === 0) {
33
+ return {
34
+ closingStart: match.index,
35
+ blockEnd: pattern.lastIndex
36
+ }
37
+ }
38
+
39
+ match = pattern.exec(source)
40
+ }
41
+
42
+ return null
43
+ }
44
+
45
+ const parseVueSfcBlocks = (source = '') => {
46
+ const blocks = {
47
+ template: [],
48
+ script: [],
49
+ style: []
50
+ }
51
+ const content = String(source)
52
+ const openingPattern = createSfcOpeningTagPattern()
53
+
54
+ let cursor = 0
55
+ while (cursor < content.length) {
56
+ openingPattern.lastIndex = cursor
57
+
58
+ const match = openingPattern.exec(content)
59
+ if (!match) {
60
+ break
61
+ }
62
+
63
+ const tag = match[1].toLowerCase()
64
+ const openingStart = match.index
65
+ const openingEnd = openingPattern.lastIndex
66
+ const range = findSfcBlockRange(content, tag, openingEnd)
67
+
68
+ if (!range) {
69
+ cursor = openingEnd
70
+ continue
71
+ }
72
+
73
+ blocks[tag].push({
74
+ tag,
75
+ attrs: match[2] || '',
76
+ content: content.slice(openingEnd, range.closingStart).trim(),
77
+ raw: content.slice(openingStart, range.blockEnd).trim()
78
+ })
79
+
80
+ cursor = range.blockEnd
81
+ }
82
+
83
+ return blocks
84
+ }
85
+
86
+ const hasAttribute = (rawAttrs = '', name = '') => {
87
+ const pattern = new RegExp(`(?:^|\\s)${name}(?:\\s|=|$)`, 'i')
88
+ return pattern.test(String(rawAttrs || ''))
89
+ }
90
+
91
+ const getLangAttribute = (rawAttrs = '') => {
92
+ const match = String(rawAttrs || '').match(/(?:^|\s)lang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i)
93
+ return (match?.[1] || match?.[2] || match?.[3] || '').trim().toLowerCase()
94
+ }
95
+
96
+ const normalizeNamedImport = (rawName = '') => {
97
+ const trimmed = rawName.trim()
98
+ const aliasMatch = trimmed.match(/^(.+?)\s+as\s+(.+)$/i)
99
+
100
+ if (aliasMatch) {
101
+ return `${aliasMatch[1].trim()}: ${aliasMatch[2].trim()}`
102
+ }
103
+
104
+ return trimmed
105
+ }
106
+
107
+ const createGlobalDestructure = (globalName, rawNames = '') => {
108
+ const names = String(rawNames)
109
+ .split(',')
110
+ .map((name) => normalizeNamedImport(name))
111
+ .filter(Boolean)
112
+
113
+ if (names.length === 0) {
114
+ return ''
115
+ }
116
+
117
+ return `const { ${names.join(', ')} } = ${globalName}`
118
+ }
119
+
120
+ const findUnsupportedImport = (script = '') => {
121
+ const importsWithSourcePattern = /^\s*import\s+(.+?)\s+from\s+["']([^"']+)["'];?\s*$/gm
122
+ const sideEffectImportPattern = /^\s*import\s+["']([^"']+)["'];?\s*$/gm
123
+
124
+ let match = importsWithSourcePattern.exec(script)
125
+ while (match !== null) {
126
+ const importSpecifiers = match[1].trim()
127
+ const importSource = match[2].trim()
128
+
129
+ if (!ALLOWED_CODEPEN_IMPORTS.has(importSource) || !importSpecifiers.startsWith('{')) {
130
+ return importSource
131
+ }
132
+
133
+ match = importsWithSourcePattern.exec(script)
134
+ }
135
+
136
+ match = sideEffectImportPattern.exec(script)
137
+ if (match !== null) {
138
+ return match[1].trim()
139
+ }
140
+
141
+ return ''
142
+ }
143
+
144
+ const transformAllowedImports = (script = '') => {
145
+ return String(script)
146
+ .replace(/^\s*import\s+\{([^}]+)\}\s+from\s+["']vue["'];?\s*$/gm, (_, imports) => createGlobalDestructure('Vue', imports))
147
+ .replace(/^\s*import\s+\{([^}]+)\}\s+from\s+["']quasar["'];?\s*$/gm, (_, imports) => createGlobalDestructure('Quasar', imports))
148
+ }
149
+
150
+ const getScriptForValidation = (script = '') => {
151
+ return transformAllowedImports(script).trim()
152
+ }
153
+
154
+ const stripSfcTags = (blocks = []) => {
155
+ return blocks
156
+ .map((block) => block.content)
157
+ .filter(Boolean)
158
+ .join('\n\n')
159
+ .trim()
160
+ }
161
+
162
+ const getStylePreprocessor = (styleBlock) => {
163
+ const lang = getLangAttribute(styleBlock?.attrs || '')
164
+ return lang || 'none'
165
+ }
166
+
167
+ const getPartLanguage = (label, text = '') => {
168
+ if (label === 'Template') return 'html'
169
+ if (label === 'Script') return getLangAttribute(text.match(/^<script\b([^>]*)>/i)?.[1] || '') || 'javascript'
170
+ if (label === 'Style') return getLangAttribute(text.match(/^<style\b([^>]*)>/i)?.[1] || '') || 'css'
171
+ if (label === 'All') return 'vue'
172
+ return 'text'
173
+ }
174
+
175
+ const createEditorsFlag = ({ html, css, js }) => {
176
+ const flag = (html ? 0b100 : 0) | (css ? 0b010 : 0) | (js ? 0b001 : 0)
177
+ return flag.toString(2)
178
+ }
179
+
180
+ const createCodepenResources = (quasarVersion = 'latest') => {
181
+ const version = String(quasarVersion || 'latest').trim() || 'latest'
182
+
183
+ return {
184
+ cssExternal: [
185
+ 'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons',
186
+ `https://cdn.jsdelivr.net/npm/quasar@${version}/dist/quasar.min.css`
187
+ ].join(';'),
188
+ jsExternal: [
189
+ 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js',
190
+ `https://cdn.jsdelivr.net/npm/quasar@${version}/dist/quasar.umd.prod.js`
191
+ ].join(';')
192
+ }
193
+ }
194
+
195
+ const normalizeRepositoryFilePath = (filePath = '') => {
196
+ return String(filePath || '')
197
+ .replace(/\\/g, '/')
198
+ .replace(/^\/+/, '')
199
+ .trim()
200
+ }
201
+
202
+ const createCodepenJs = (script = '') => {
203
+ const transformedScript = transformAllowedImports(script).trim()
204
+ const componentScript = transformedScript
205
+ ? transformedScript.replace(/\bexport\s+default\b/, 'const __CodeExample =')
206
+ : 'const __CodeExample = {}'
207
+
208
+ return `${componentScript}
209
+
210
+ const app = Vue.createApp(__CodeExample)
211
+
212
+ app.use(Quasar, { config: {} })
213
+ app.mount('#q-app')
214
+ `
215
+ }
216
+
217
+ export const parseVueSfcParts = (source = '') => {
218
+ const blocks = parseVueSfcBlocks(source)
219
+ const parts = {}
220
+
221
+ Object.entries(blocks).forEach(([tag, tagBlocks]) => {
222
+ const label = PART_LABELS[tag]
223
+ const raw = tagBlocks
224
+ .map((block) => block.raw)
225
+ .filter(Boolean)
226
+ .join('\n\n')
227
+ .trim()
228
+
229
+ if (label && raw) {
230
+ parts[label] = raw
231
+ }
232
+ })
233
+
234
+ return parts
235
+ }
236
+
237
+ export const createCodeExampleTabs = (source = '') => {
238
+ const trimmedSource = String(source || '').trim()
239
+ const parts = parseVueSfcParts(trimmedSource)
240
+ const tabs = PART_ORDER
241
+ .filter((label) => parts[label])
242
+ .map((label) => ({
243
+ label,
244
+ language: getPartLanguage(label, parts[label]),
245
+ text: parts[label]
246
+ }))
247
+
248
+ if (tabs.length > 1) {
249
+ tabs.push({
250
+ label: 'All',
251
+ language: getPartLanguage('All'),
252
+ text: trimmedSource
253
+ })
254
+ }
255
+
256
+ if (tabs.length === 0 && trimmedSource) {
257
+ tabs.push({
258
+ label: 'Source',
259
+ language: 'text',
260
+ text: trimmedSource
261
+ })
262
+ }
263
+
264
+ return tabs
265
+ }
266
+
267
+ export const getCodepenUnsupportedReason = (source = '') => {
268
+ const blocks = parseVueSfcBlocks(source)
269
+
270
+ if (blocks.template.length === 0 || !stripSfcTags(blocks.template)) {
271
+ return 'CodePen export requires a Vue SFC template section.'
272
+ }
273
+
274
+ if (blocks.script.length > 1) {
275
+ return 'CodePen export supports a single script section in this version.'
276
+ }
277
+
278
+ const scriptBlock = blocks.script[0]
279
+ if (!scriptBlock) {
280
+ return ''
281
+ }
282
+
283
+ if (hasAttribute(scriptBlock.attrs, 'setup')) {
284
+ return 'CodePen export does not support script setup examples yet.'
285
+ }
286
+
287
+ if (getLangAttribute(scriptBlock.attrs) === 'ts') {
288
+ return 'CodePen export does not support TypeScript script sections yet.'
289
+ }
290
+
291
+ const unsupportedImport = findUnsupportedImport(scriptBlock.content)
292
+ if (unsupportedImport) {
293
+ return `CodePen export does not support local or external imports (${unsupportedImport}).`
294
+ }
295
+
296
+ const validationScript = getScriptForValidation(scriptBlock.content)
297
+ if (validationScript && !/\bexport\s+default\b/.test(validationScript)) {
298
+ return 'CodePen export requires an Options API default export in this version.'
299
+ }
300
+
301
+ return ''
302
+ }
303
+
304
+ export const canCreateCodepenPayload = (source = '') => {
305
+ return getCodepenUnsupportedReason(source) === ''
306
+ }
307
+
308
+ export const createCodepenPayload = (source = '', options = {}) => {
309
+ const unsupportedReason = getCodepenUnsupportedReason(source)
310
+ if (unsupportedReason) {
311
+ throw new Error(unsupportedReason)
312
+ }
313
+
314
+ const blocks = parseVueSfcBlocks(source)
315
+ const html = stripSfcTags(blocks.template)
316
+ const css = stripSfcTags(blocks.style)
317
+ const script = blocks.script[0]?.content || ''
318
+ const js = createCodepenJs(script)
319
+ const resources = createCodepenResources(options.quasarVersion)
320
+ const sourceUrl = String(options.sourceUrl || '').trim()
321
+ const sourceComment = sourceUrl
322
+ ? `<!--\nGenerated from:\n${sourceUrl}\n-->\n`
323
+ : ''
324
+
325
+ return {
326
+ title: String(options.title || 'Docsector code example').trim() || 'Docsector code example',
327
+ html: `${sourceComment}<div id="q-app" style="min-height: 100vh;">
328
+ ${html}
329
+ </div>`,
330
+ head: '',
331
+ html_pre_processor: 'none',
332
+ css,
333
+ css_pre_processor: getStylePreprocessor(blocks.style[0]),
334
+ css_external: resources.cssExternal,
335
+ js,
336
+ js_pre_processor: 'babel',
337
+ js_external: resources.jsExternal,
338
+ editors: createEditorsFlag({ html, css, js })
339
+ }
340
+ }
341
+
342
+ export const createCodeExampleGitHubUrl = (filePath = '', config = {}) => {
343
+ const normalizedFilePath = normalizeRepositoryFilePath(filePath)
344
+
345
+ if (!normalizedFilePath) {
346
+ return ''
347
+ }
348
+
349
+ const editBaseUrl = String(config.github?.editBaseUrl || '').trim()
350
+ const editMatch = editBaseUrl.match(/^(https:\/\/github\.com\/[^/]+\/[^/]+)\/(?:edit|blob|tree)\/([^/]+)(?:\/.*)?$/)
351
+
352
+ if (editMatch) {
353
+ return `${editMatch[1]}/blob/${editMatch[2]}/${normalizedFilePath}`
354
+ }
355
+
356
+ const githubUrl = String(config.links?.github || '').trim().replace(/\/+$/, '')
357
+
358
+ if (/^https:\/\/github\.com\/[^/]+\/[^/]+$/.test(githubUrl)) {
359
+ return `${githubUrl}/blob/main/${normalizedFilePath}`
360
+ }
361
+
362
+ return ''
363
+ }
@@ -20,6 +20,7 @@ const STEPPER_MARKER_PREFIX = '@@DOCSECTOR_STEPPER_'
20
20
  const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
21
21
  const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
22
22
  const EMBEDDED_URL_MARKER_PREFIX = '@@DOCSECTOR_EMBEDDED_URL_'
23
+ const CODE_EXAMPLE_MARKER_PREFIX = '@@DOCSECTOR_CODE_EXAMPLE_'
23
24
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
24
25
  const MATH_KATEX_OPTIONS = {
25
26
  throwOnError: false,
@@ -246,6 +247,24 @@ const parseExpandableOpenState = (raw = '') => {
246
247
  return ['1', 'true', 'yes', 'on'].includes(String(raw).trim().toLowerCase())
247
248
  }
248
249
 
250
+ const parseBooleanAttribute = (raw, fallback = false) => {
251
+ if (raw === undefined || raw === null || raw === '') {
252
+ return fallback
253
+ }
254
+
255
+ const normalized = String(raw).trim().toLowerCase()
256
+
257
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
258
+ return true
259
+ }
260
+
261
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
262
+ return false
263
+ }
264
+
265
+ return fallback
266
+ }
267
+
249
268
  const parseTimelineTags = (raw = '') => {
250
269
  return decodeHtmlEntities(raw)
251
270
  .split(',')
@@ -517,6 +536,46 @@ const extractEmbeddedUrlBlocks = (source = '') => {
517
536
  }
518
537
  }
519
538
 
539
+ const extractCodeExampleBlocks = (source = '') => {
540
+ const map = new Map()
541
+ let index = 0
542
+
543
+ const replaceBlock = (match, rawAttrs, rawCaption = '') => {
544
+ const attrs = parseCustomTagAttributes(rawAttrs)
545
+ const src = decodeHtmlEntities(attrs.src || attrs.file || '').trim()
546
+
547
+ if (!src) {
548
+ return match
549
+ }
550
+
551
+ const marker = `${CODE_EXAMPLE_MARKER_PREFIX}${index}@@`
552
+ index++
553
+
554
+ map.set(marker, {
555
+ src,
556
+ title: decodeHtmlEntities(attrs.title || '').trim(),
557
+ expanded: parseBooleanAttribute(attrs.expanded, false),
558
+ codepen: parseBooleanAttribute(attrs.codepen, true),
559
+ scrollable: parseBooleanAttribute(attrs.scrollable, false),
560
+ overflow: parseBooleanAttribute(attrs.overflow, false),
561
+ height: decodeHtmlEntities(attrs.height || '').trim(),
562
+ caption: String(rawCaption).trim()
563
+ })
564
+
565
+ return `\n${marker}\n`
566
+ }
567
+
568
+ const replacedSelfClosing = String(source).replace(/<d-block-code-example\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
569
+ return replaceBlock(match, rawAttrs)
570
+ })
571
+ const replaced = replacedSelfClosing.replace(/<d-block-code-example\b([^>]*)>([\s\S]*?)<\/d-block-code-example>/gi, replaceBlock)
572
+
573
+ return {
574
+ source: replaced,
575
+ codeExampleMap: map
576
+ }
577
+ }
578
+
520
579
  const parseFenceAttributes = (raw = '') => {
521
580
  const parsed = {}
522
581
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -871,6 +930,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
871
930
  const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithCards)
872
931
  const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
873
932
  const { source: sourceWithEmbeddedUrls, embeddedUrlMap } = extractEmbeddedUrlBlocks(sourceWithFiles)
933
+ const { source: sourceWithCodeExamples, codeExampleMap } = extractCodeExampleBlocks(sourceWithEmbeddedUrls)
874
934
 
875
935
  fileMap.forEach((data, marker) => {
876
936
  fileMap.set(marker, {
@@ -886,10 +946,17 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
886
946
  })
887
947
  })
888
948
 
949
+ codeExampleMap.forEach((data, marker) => {
950
+ codeExampleMap.set(marker, {
951
+ ...data,
952
+ caption: restoreShieldedCodeSegments(data.caption, codeSegmentsMap)
953
+ })
954
+ })
955
+
889
956
  const markdown = createMarkdownBlockParser()
890
957
  const markdownInline = createMarkdownInlineParser()
891
958
  const markdownEnv = {}
892
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithEmbeddedUrls, codeSegmentsMap), markdownEnv)
959
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithCodeExamples, codeSegmentsMap), markdownEnv)
893
960
  const tokens = []
894
961
 
895
962
  let level = 0
@@ -1146,6 +1213,27 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
1146
1213
  break
1147
1214
  }
1148
1215
 
1216
+ if (codeExampleMap.has(element.content.trim())) {
1217
+ const data = codeExampleMap.get(element.content.trim())
1218
+
1219
+ tokens.push({
1220
+ tag: 'code-example',
1221
+ map: element.map,
1222
+ codeIndex: parserState.codeIndex++,
1223
+ src: data.src,
1224
+ title: data.title,
1225
+ expanded: data.expanded,
1226
+ codepen: data.codepen,
1227
+ scrollable: data.scrollable,
1228
+ overflow: data.overflow,
1229
+ height: data.height,
1230
+ caption: data.caption !== ''
1231
+ ? markdownInline.renderInline(data.caption, markdownEnv)
1232
+ : ''
1233
+ })
1234
+ break
1235
+ }
1236
+
1149
1237
  if (tag === 'p') {
1150
1238
  const imageToken = parseStandaloneImageToken(element.content)
1151
1239
 
@@ -0,0 +1,42 @@
1
+ export const DEFAULT_ACTIVE_ANCHOR_OFFSET = 50
2
+
3
+ const toFiniteNumber = (value) => {
4
+ const normalized = Number(value)
5
+ return Number.isFinite(normalized) ? normalized : 0
6
+ }
7
+
8
+ export function getActiveAnchorId({
9
+ anchors = [],
10
+ scrollTop = 0,
11
+ scrollOffset = DEFAULT_ACTIVE_ANCHOR_OFFSET,
12
+ rootAnchorId = 0,
13
+ getAnchorOffsetTop = () => undefined
14
+ } = {}) {
15
+ const thresholdTop = Math.max(0, toFiniteNumber(scrollTop)) + Math.max(0, toFiniteNumber(scrollOffset))
16
+ let activeAnchorId = rootAnchorId
17
+ const seenAnchors = new Set()
18
+
19
+ for (const anchorId of Array.isArray(anchors) ? anchors : []) {
20
+ if (anchorId === null || anchorId === undefined || anchorId === false || anchorId === 0 || anchorId === '0') {
21
+ continue
22
+ }
23
+
24
+ if (seenAnchors.has(anchorId)) {
25
+ continue
26
+ }
27
+
28
+ seenAnchors.add(anchorId)
29
+
30
+ const resolvedOffsetTop = Number(getAnchorOffsetTop(anchorId))
31
+
32
+ if (!Number.isFinite(resolvedOffsetTop)) {
33
+ continue
34
+ }
35
+
36
+ if (thresholdTop >= resolvedOffsetTop) {
37
+ activeAnchorId = anchorId
38
+ }
39
+ }
40
+
41
+ return activeAnchorId
42
+ }
@@ -3,6 +3,8 @@ import { useStore } from 'vuex'
3
3
  import { useRouter, useRoute } from 'vue-router'
4
4
  import { ref } from 'vue'
5
5
 
6
+ import { DEFAULT_ACTIVE_ANCHOR_OFFSET, getActiveAnchorId } from './useActiveAnchor'
7
+
6
8
  export default function useNavigator() {
7
9
  const store = useStore()
8
10
  const router = useRouter()
@@ -46,7 +48,10 @@ export default function useNavigator() {
46
48
  const select = (id) => {
47
49
  const normalized = normalizeStoreAnchorId(id)
48
50
 
49
- store.commit('page/setAnchor', normalized)
51
+ if (store.state.page.anchor !== normalized) {
52
+ store.commit('page/setAnchor', normalized)
53
+ }
54
+
50
55
  store.commit('page/pushNodesExpanded', normalized)
51
56
  }
52
57
 
@@ -78,27 +83,29 @@ export default function useNavigator() {
78
83
  return
79
84
  }
80
85
 
81
- const scrollPositionTop = scroll.position.top + 50
82
- const anchors = store.state.page.anchors
86
+ const activeAnchorId = getActiveAnchorId({
87
+ anchors: store.state.page.anchors,
88
+ scrollTop: scroll?.position?.top,
89
+ scrollOffset: DEFAULT_ACTIVE_ANCHOR_OFFSET,
90
+ rootAnchorId: 0,
91
+ getAnchorOffsetTop: (anchorId) => {
92
+ const domAnchorId = normalizeDomAnchorId(anchorId)
83
93
 
84
- for (let i = 0; i < anchors.length; i++) {
85
- const anchorId = anchors[i]
86
- const domAnchorId = normalizeDomAnchorId(anchorId)
94
+ if (domAnchorId === '') {
95
+ return undefined
96
+ }
87
97
 
88
- if (domAnchorId === '0') {
89
- continue
90
- }
98
+ const Anchor = document.getElementById(domAnchorId)
91
99
 
92
- const Anchor = document.getElementById(domAnchorId)
93
- let AnchorOffsetTop = 20
94
- if (Anchor !== null && typeof Anchor === 'object') {
95
- AnchorOffsetTop = Anchor.offsetTop
96
- }
100
+ if (Anchor !== null && typeof Anchor === 'object') {
101
+ return Anchor.offsetTop
102
+ }
97
103
 
98
- if (scrollPositionTop >= AnchorOffsetTop) {
99
- select(anchorId)
104
+ return undefined
100
105
  }
101
- }
106
+ })
107
+
108
+ select(activeAnchorId)
102
109
  }
103
110
 
104
111
  const navigate = (value, toAnchor = true) => {
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="basic-counter-example q-pa-md">
3
+ <q-card class="basic-counter-example__card" flat bordered>
4
+ <q-card-section>
5
+ <div class="text-subtitle2 text-grey-7">Interactive preview</div>
6
+ <div class="basic-counter-example__value">{{ count }}</div>
7
+ <div class="text-body2 text-grey-7">{{ countLabel }}</div>
8
+ </q-card-section>
9
+
10
+ <q-separator></q-separator>
11
+
12
+ <q-card-actions align="right">
13
+ <q-btn flat color="primary" label="Reset" @click="reset"></q-btn>
14
+ <q-btn unelevated color="primary" label="Increment" @click="increment"></q-btn>
15
+ </q-card-actions>
16
+ </q-card>
17
+ </div>
18
+ </template>
19
+
20
+ <script>
21
+ import { computed, ref } from 'vue'
22
+
23
+ export default {
24
+ setup () {
25
+ const count = ref(0)
26
+ const countLabel = computed(() => count.value === 1 ? 'click recorded' : 'clicks recorded')
27
+
28
+ function increment () {
29
+ count.value++
30
+ }
31
+
32
+ function reset () {
33
+ count.value = 0
34
+ }
35
+
36
+ return {
37
+ count,
38
+ countLabel,
39
+ increment,
40
+ reset
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <style scoped>
47
+ .basic-counter-example {
48
+ display: flex;
49
+ justify-content: center;
50
+ }
51
+
52
+ .basic-counter-example__card {
53
+ max-width: 360px;
54
+ width: 100%;
55
+ }
56
+
57
+ .basic-counter-example__value {
58
+ font-size: 48px;
59
+ font-weight: 700;
60
+ line-height: 1;
61
+ margin: 12px 0 6px;
62
+ }
63
+ </style>