@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 +17 -0
- package/bin/docsector.js +1 -1
- package/package.json +1 -1
- package/src/components/DBlockCodeExample.vue +423 -0
- package/src/components/DBlockSourceCode.vue +1 -7
- package/src/components/DPageTokens.vue +14 -0
- package/src/components/code-block-highlighting.js +16 -0
- package/src/components/code-example-source.js +363 -0
- package/src/components/page-section-tokens.js +89 -1
- package/src/examples/manual/code-examples/BasicCounter.vue +63 -0
- package/src/examples/manual/code-examples/InlineNotice.vue +60 -0
- package/src/pages/manual/content/blocks/code-examples.overview.en-US.md +56 -0
- package/src/pages/manual/content/blocks/code-examples.overview.pt-BR.md +56 -0
- package/src/pages/manual/content/blocks/code-examples.showcase.en-US.md +38 -0
- package/src/pages/manual/content/blocks/code-examples.showcase.pt-BR.md +38 -0
- package/src/pages/manual.index.js +28 -0
- package/src/quasar.factory.js +77 -0
- package/src/store/Page.js +26 -2
|
@@ -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(
|
|
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,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>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="inline-notice-example q-pa-md">
|
|
3
|
+
<q-banner rounded class="inline-notice-example__banner">
|
|
4
|
+
<template #avatar>
|
|
5
|
+
<q-icon name="tips_and_updates" color="primary"></q-icon>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<div class="text-weight-medium">{{ title }}</div>
|
|
9
|
+
<div class="text-body2">{{ message }}</div>
|
|
10
|
+
|
|
11
|
+
<template #action>
|
|
12
|
+
<q-btn flat color="primary" label="Dismiss" @click="dismiss"></q-btn>
|
|
13
|
+
</template>
|
|
14
|
+
</q-banner>
|
|
15
|
+
|
|
16
|
+
<div v-if="dismissed" class="inline-notice-example__status text-caption">
|
|
17
|
+
The notice was dismissed.
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import { ref } from 'vue'
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
setup () {
|
|
27
|
+
const title = 'Documentation tip'
|
|
28
|
+
const message = 'Use expanded examples when the source code is part of the lesson.'
|
|
29
|
+
const dismissed = ref(false)
|
|
30
|
+
|
|
31
|
+
function dismiss () {
|
|
32
|
+
dismissed.value = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
dismissed,
|
|
37
|
+
dismiss,
|
|
38
|
+
message,
|
|
39
|
+
title
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.inline-notice-example {
|
|
47
|
+
display: grid;
|
|
48
|
+
gap: 10px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.inline-notice-example__banner {
|
|
52
|
+
background: rgba(25, 118, 210, 0.08);
|
|
53
|
+
border: 1px solid rgba(25, 118, 210, 0.18);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.inline-notice-example__status {
|
|
57
|
+
color: #607d68;
|
|
58
|
+
padding-left: 8px;
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
Code example blocks render project Vue SFCs as live previews inside Markdown pages.
|
|
4
|
+
|
|
5
|
+
They are useful when documentation needs to show the real behavior of a component and still let readers inspect the exact source behind the preview.
|
|
6
|
+
|
|
7
|
+
The block is authored with the custom Markdown element `<d-block-code-example>`.
|
|
8
|
+
|
|
9
|
+
## Example Files
|
|
10
|
+
|
|
11
|
+
Place example components under `src/examples/**/*.vue` in the project using Docsector.
|
|
12
|
+
|
|
13
|
+
Docsector discovers those files at build time through Vite. The `src` value is normalized to kebab-case, so this block:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<d-block-code-example src="manual/code-examples/basic-counter" title="Basic counter" />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
resolves this file:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
src/examples/manual/code-examples/BasicCounter.vue
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You can also use `file` instead of `src` when migrating examples from Quasar Docs patterns.
|
|
26
|
+
|
|
27
|
+
## Attributes
|
|
28
|
+
|
|
29
|
+
| Attribute | Purpose |
|
|
30
|
+
|-----------|---------|
|
|
31
|
+
| `src` | Example id under `src/examples/**/*.vue` |
|
|
32
|
+
| `file` | Alias for `src` |
|
|
33
|
+
| `title` | Header title shown above the preview |
|
|
34
|
+
| `expanded` | Opens the source panel by default when set to `true` |
|
|
35
|
+
| `codepen` | Shows the CodePen action unless set to `false` |
|
|
36
|
+
| `scrollable` | Gives the preview a fixed scrollable height |
|
|
37
|
+
| `overflow` | Allows both horizontal and vertical overflow in the preview |
|
|
38
|
+
| `height` | Sets a custom preview height, such as `360` or `420px` |
|
|
39
|
+
|
|
40
|
+
## Source Panel
|
|
41
|
+
|
|
42
|
+
The source button opens the Vue SFC split into Template, Script, Style, and All tabs when those sections are present.
|
|
43
|
+
|
|
44
|
+
The source panel reuses the standard Docsector code block renderer, so readers get syntax highlighting, copy support, and the same dark/light treatment as regular code blocks.
|
|
45
|
+
|
|
46
|
+
## GitHub Source Link
|
|
47
|
+
|
|
48
|
+
The GitHub button opens the example SFC in the project repository when Docsector can derive a repository URL from `github.editBaseUrl` or `links.github` in `docsector.config.js`.
|
|
49
|
+
|
|
50
|
+
## CodePen Export
|
|
51
|
+
|
|
52
|
+
The CodePen button is available when the source can be transformed safely for a browser-only demo.
|
|
53
|
+
|
|
54
|
+
The first implementation 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. Examples that use `<script setup>`, TypeScript scripts, or local imports still render in Docsector, but the CodePen action is disabled.
|
|
55
|
+
|
|
56
|
+
Set `codepen="false"` when an example is intentionally not meant to be exported.
|