mirror of
https://git.hmsn.ink/kospo/svcm/oa.git
synced 2026-03-19 21:05:06 +09:00
first
This commit is contained in:
42
vite-plugin/purge-comments/index.ts
Normal file
42
vite-plugin/purge-comments/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Plugin } from 'vite'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
const commentRe = /<!--(?:.{2,}?)-->/sg
|
||||
|
||||
/**
|
||||
* This plugin removes HTML comments from your code.
|
||||
*/
|
||||
export function PurgeComments() {
|
||||
let sourcemap: boolean | 'inline' | 'hidden' | undefined
|
||||
return {
|
||||
name: 'purge-comments',
|
||||
enforce: 'pre',
|
||||
configResolved(config) {
|
||||
sourcemap = config.build.sourcemap
|
||||
},
|
||||
transform: (code, id) => {
|
||||
if (
|
||||
!(
|
||||
id.endsWith('.vue')
|
||||
|| id.endsWith('.html')
|
||||
|| id.endsWith('.svg')
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (!code.includes('<!--')) {
|
||||
return
|
||||
}
|
||||
|
||||
const s = new MagicString(code)
|
||||
s.replace(commentRe, '')
|
||||
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: sourcemap ? s.generateMap() : null,
|
||||
}
|
||||
}
|
||||
},
|
||||
} satisfies Plugin
|
||||
}
|
||||
15
vite-plugin/vuero-doc/README.md
Normal file
15
vite-plugin/vuero-doc/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# vite-plugin-vuero-doc
|
||||
|
||||
This folder hold the custom `vite-plugin-vuero-doc` vite plugin.
|
||||
|
||||
We use this plugin to allow loading of markdown files in
|
||||
[../documentation](../documentation) folder and be loaded as vue components **at build time**.
|
||||
Those components are used in [../src/pages/components](../src/pages/components)
|
||||
and [../src/pages/elements](../src/pages/elements) pages
|
||||
|
||||
The plugin will converts `<!--code--> ... <!--/code-->` and
|
||||
`<!--example--> ... <!--example-->` into slot content and inject them in the
|
||||
[../src/components/partials/documentation/DocumentationItem.vue](../src/components/partials/documentation/DocumentationItem.vue)
|
||||
component
|
||||
|
||||
This is only intended to be an example.
|
||||
192
vite-plugin/vuero-doc/index.ts
Normal file
192
vite-plugin/vuero-doc/index.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { Plugin, ResolvedConfig } from 'vite'
|
||||
import type {
|
||||
ThemeRegistration,
|
||||
ThemeRegistrationRaw,
|
||||
BuiltinTheme,
|
||||
StringLiteralUnion,
|
||||
} from 'shiki'
|
||||
|
||||
import { join, basename } from 'pathe'
|
||||
import { compileTemplate, parse } from '@vue/compiler-sfc'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
import { createProcessor } from './markdown'
|
||||
import { transformExampleMarkup, transformSlots } from './transform'
|
||||
|
||||
function parseId(id: string) {
|
||||
const index = id.indexOf('?')
|
||||
if (index < 0) return id
|
||||
else return id.slice(0, index)
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
pathPrefix?: string
|
||||
wrapperComponent: string
|
||||
shiki: {
|
||||
themes: Partial<
|
||||
Record<
|
||||
string,
|
||||
ThemeRegistration | ThemeRegistrationRaw | StringLiteralUnion<BuiltinTheme>
|
||||
>
|
||||
>
|
||||
}
|
||||
sourceMeta?: {
|
||||
enabled?: boolean
|
||||
editProtocol?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function VueroMarkdownDoc(options: PluginOptions) {
|
||||
let config: ResolvedConfig | undefined
|
||||
let processor: Awaited<ReturnType<typeof createProcessor>> | undefined
|
||||
|
||||
const cwd = process.cwd()
|
||||
const pathPrefix = options.pathPrefix ? join(cwd, options.pathPrefix) : cwd
|
||||
|
||||
async function markdownToVue(id: string, raw: string) {
|
||||
const path = parseId(id)
|
||||
|
||||
// transform example markup to use kebab-case without self-closing elements.
|
||||
// this is needed because rehype-raw requires valid html (only some tags are self-closable)
|
||||
const input = transformExampleMarkup(raw)
|
||||
|
||||
// process markdown with remark
|
||||
if (!processor) processor = await createProcessor(options.shiki.themes)
|
||||
|
||||
const vFile = await processor?.process(input)
|
||||
|
||||
const content = vFile.toString()
|
||||
const frontmatter = vFile.data?.frontmatter ?? {}
|
||||
|
||||
// replace code/example slots placeholders
|
||||
const slot = transformSlots(content)
|
||||
|
||||
// wrap content in wrapper component default slot
|
||||
const sfc = [
|
||||
`<template>`,
|
||||
` <${options.wrapperComponent} :frontmatter="frontmatter" :source-meta="sourceMeta">`,
|
||||
` ${slot}`,
|
||||
` </${options.wrapperComponent}>`,
|
||||
`</template>`,
|
||||
].join('\n')
|
||||
|
||||
// parse template with vue sfc compiler
|
||||
const result = parse(sfc, {
|
||||
filename: path,
|
||||
sourceMap: Boolean(config?.build?.sourcemap),
|
||||
templateParseOptions: {
|
||||
isCustomElement: tag => ['iconify-icon'].includes(tag),
|
||||
},
|
||||
})
|
||||
|
||||
if (result.errors.length || result.descriptor.template === null) {
|
||||
console.error(result.errors)
|
||||
|
||||
throw new Error(`Markdown: unable to parse virtual file generated for "${id}"`)
|
||||
}
|
||||
|
||||
// compile template with vue sfc compiler
|
||||
const isSSR = Boolean(config?.build?.ssr)
|
||||
const { code, errors } = compileTemplate({
|
||||
filename: path,
|
||||
id: path,
|
||||
ast: result.descriptor.template.ast,
|
||||
source: result.descriptor.template.content,
|
||||
preprocessLang: result.descriptor.template.lang,
|
||||
ssr: isSSR,
|
||||
ssrCssVars: result.descriptor?.cssVars,
|
||||
transformAssetUrls: false,
|
||||
isProd: config?.isProduction,
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => ['iconify-icon'].includes(tag),
|
||||
},
|
||||
})
|
||||
|
||||
if (errors.length) {
|
||||
console.error(errors)
|
||||
|
||||
throw new Error(`Markdown: unable to compile virtual file "${id}"`)
|
||||
}
|
||||
|
||||
// source is used to display edit source link in the docs
|
||||
let sourceMeta = 'undefined'
|
||||
if (options.sourceMeta?.enabled) {
|
||||
sourceMeta = JSON.stringify({
|
||||
relativePath: path.substring(cwd.length),
|
||||
basename: basename(path),
|
||||
path: config?.isProduction ? '' : path,
|
||||
editProtocol: config?.isProduction ? '' : options.sourceMeta.editProtocol,
|
||||
})
|
||||
}
|
||||
|
||||
const s = new MagicString(code, {
|
||||
filename: path,
|
||||
})
|
||||
|
||||
s.prepend(`import { reactive } from 'vue'\n`)
|
||||
s.prepend(`import { useDarkmode } from '/@src/composables/darkmode'\n`)
|
||||
|
||||
if (isSSR) {
|
||||
s.replace('export function ssrRender', 'function ssrRender')
|
||||
}
|
||||
else {
|
||||
s.replace('export function render', 'function render')
|
||||
}
|
||||
|
||||
s.append(`const __frontmatter = ${JSON.stringify(frontmatter)};\n`)
|
||||
s.append(`const setup = () => ({\n`)
|
||||
s.append(` frontmatter: reactive(__frontmatter),\n`)
|
||||
s.append(` darkmode: useDarkmode(),\n`)
|
||||
s.append(` sourceMeta: ${sourceMeta},\n`)
|
||||
s.append(`});\n`)
|
||||
|
||||
if (isSSR) {
|
||||
s.append(`const __script = { ssrRender, setup };\n`)
|
||||
}
|
||||
else {
|
||||
s.append(`const __script = { render, setup };\n`)
|
||||
}
|
||||
|
||||
if (!config?.isProduction) {
|
||||
s.append([
|
||||
`__script.__hmrId = ${JSON.stringify(path)};`,
|
||||
'if (import.meta.hot) {',
|
||||
' typeof __VUE_HMR_RUNTIME__ !== "undefined" && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script);',
|
||||
' import.meta.hot.accept((mod) => {',
|
||||
' if (!mod)',
|
||||
' return;',
|
||||
' const { default: updated, _rerender_only: _rerender_only2 } = mod;',
|
||||
' if (_rerender_only2) {',
|
||||
' __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);',
|
||||
' } else {',
|
||||
' __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);',
|
||||
' }',
|
||||
' });',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
s.append(`export default __script;\n`)
|
||||
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: config?.build?.sourcemap ? s.generateMap() : null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'vite-plugin-vuero-doc',
|
||||
enforce: 'pre',
|
||||
configResolved(_config) {
|
||||
config = _config
|
||||
},
|
||||
transform(raw, id) {
|
||||
if (id.endsWith('.md') && id.startsWith(pathPrefix)) {
|
||||
return markdownToVue(id, raw)
|
||||
}
|
||||
},
|
||||
} satisfies Plugin
|
||||
}
|
||||
|
||||
export default VueroMarkdownDoc
|
||||
82
vite-plugin/vuero-doc/markdown.ts
Normal file
82
vite-plugin/vuero-doc/markdown.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import yaml from 'js-yaml'
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeStringify from 'rehype-stringify'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import remarkFrontmatter from 'remark-frontmatter'
|
||||
import rehypeShiki from '@shikijs/rehype'
|
||||
import type {
|
||||
ThemeRegistration,
|
||||
ThemeRegistrationRaw,
|
||||
BuiltinLanguage,
|
||||
BuiltinTheme,
|
||||
StringLiteralUnion,
|
||||
} from 'shiki'
|
||||
import { unified } from 'unified'
|
||||
import type { Literal, Parent } from 'unist'
|
||||
|
||||
const langs = [
|
||||
'vue',
|
||||
'vue-html',
|
||||
'typescript',
|
||||
'bash',
|
||||
'scss',
|
||||
] satisfies BuiltinLanguage[]
|
||||
|
||||
export async function createProcessor(
|
||||
themes: Partial<
|
||||
Record<
|
||||
string,
|
||||
ThemeRegistration | ThemeRegistrationRaw | StringLiteralUnion<BuiltinTheme>
|
||||
>
|
||||
>,
|
||||
) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkFrontmatter)
|
||||
.use(() => (tree, file) => {
|
||||
if ('children' in tree) {
|
||||
const parent = tree as Parent
|
||||
if (parent.children[0].type === 'yaml') {
|
||||
// store frontmatter in vfile
|
||||
const value = (parent.children[0] as Literal).value
|
||||
file.data.frontmatter = typeof value === 'string' ? yaml.load(value) : undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype, { allowDangerousHtml: true })
|
||||
.use(rehypeRaw)
|
||||
.use(rehypeShiki, {
|
||||
themes,
|
||||
langs,
|
||||
})
|
||||
.use(rehypeExternalLinks, { rel: ['nofollow'], target: '_blank' })
|
||||
.use(rehypeSlug)
|
||||
.use(rehypeAutolinkHeadings, {
|
||||
behavior: 'append',
|
||||
content: {
|
||||
type: 'element',
|
||||
tagName: 'iconify-icon',
|
||||
properties: {
|
||||
className: ['iconify toc-link-anchor'],
|
||||
icon: 'lucide:link',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
test: (node) => {
|
||||
if (
|
||||
Array.isArray(node.properties?.className)
|
||||
&& node.properties?.className?.includes('toc-ignore')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return Boolean(node.properties?.id)
|
||||
},
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
}
|
||||
73
vite-plugin/vuero-doc/transform.ts
Normal file
73
vite-plugin/vuero-doc/transform.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { kebabCase } from 'scule'
|
||||
|
||||
const SELF_CLOSING_TAG_REGEX = /<([^\s></]+)([^>]+)\/>/g
|
||||
const OPEN_TAG_REGEX = /<([^\s></]+)/g
|
||||
const CLOSE_TAG_REGEX = /<\/([^\s></]+)/g
|
||||
|
||||
export function transformExampleMarkup(raw: string) {
|
||||
let output = raw
|
||||
let content: RegExpMatchArray | null = null
|
||||
if ((content = raw.match(/<!--example-->([\s\S]*?)<!--\/example-->/))) {
|
||||
const kebabContent = content[1]
|
||||
.replaceAll(SELF_CLOSING_TAG_REGEX, (substring, tag) => {
|
||||
return substring.replace('/>', `></${tag.trim()}>`)
|
||||
})
|
||||
.replaceAll(OPEN_TAG_REGEX, (substring) => {
|
||||
return `<${kebabCase(substring.substring(1).trim())}`
|
||||
})
|
||||
.replaceAll(CLOSE_TAG_REGEX, (substring) => {
|
||||
return `</${kebabCase(substring.substring(2).trim())}`
|
||||
})
|
||||
.replaceAll(''', '\'')
|
||||
|
||||
output = output.replace(content[1], kebabContent)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function transformSlots(source: string, condition: string = '') {
|
||||
if (source.includes('<!--code-->') && source.includes('<!--example-->')) {
|
||||
return `<template ${condition} #default>${source}`
|
||||
.replace(
|
||||
`<!--code-->`,
|
||||
`</template><template ${condition} #code>\n<slot name="code"><div v-pre>`,
|
||||
)
|
||||
.replace(`<!--/code-->`, `</div></slot>\n</template>`)
|
||||
.replace(
|
||||
`<!--example-->`,
|
||||
`<template ${condition} #example>\n<slot name="example">`,
|
||||
)
|
||||
.replace(`<!--/example-->`, `</slot>\n</template>`)
|
||||
}
|
||||
|
||||
if (source.includes('<!--code-->')) {
|
||||
return `<template ${condition} #default>${source}`
|
||||
.replace(
|
||||
`<!--code-->`,
|
||||
`</template><template ${condition} #code>\n<slot name="code"><div v-pre>`,
|
||||
)
|
||||
.replace(
|
||||
`<!--/code-->`,
|
||||
`</div></slot>\n</template>\n<template ${condition} #example><slot name="example"></slot></template>`,
|
||||
)
|
||||
}
|
||||
|
||||
if (source.includes('<!--example-->')) {
|
||||
return `<template ${condition} #default>${source}`
|
||||
.replace(
|
||||
`<!--example-->`,
|
||||
`</template><template ${condition} #example>\n<slot name="example">`,
|
||||
)
|
||||
.replace(
|
||||
`<!--/example-->`,
|
||||
`</slot>\n</template>\n<template ${condition} #code><slot name="code"></slot></template>`,
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
`<template ${condition} #default>${source}</template>`
|
||||
+ `<template ${condition} #example><slot name="example"></slot></template>`
|
||||
+ `<template ${condition} #code><slot name="code"></slot></template>`
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user