feature: vor und nachteile

This commit is contained in:
Hikyu
2026-05-31 13:54:31 +02:00
parent 552a947a1c
commit e3dad17446
23270 changed files with 3024794 additions and 200 deletions
+62
View File
@@ -0,0 +1,62 @@
import type { Frontmatter, MetaProperty, ResolvedOptions } from '../types'
const headProperties = [
'title',
'meta',
'link',
'base',
'style',
'script',
'htmlAttrs',
'bodyAttrs',
]
export function preprocessHead<T extends Frontmatter>(frontmatter: T, options: ResolvedOptions) {
if (!options.headEnabled)
return frontmatter
const head: Frontmatter = options.headField
? (frontmatter[options.headField] as Frontmatter || {})
: frontmatter
const meta = (head.meta = head.meta || []) as MetaProperty[]
if (head.title) {
if (!meta.find((i: any) => i.property === 'og:title'))
meta.push({ property: 'og:title', content: head.title })
if (!meta.find((i: any) => i.name === 'twitter:title'))
meta.push({ name: 'twitter:title', content: head.title })
}
if (head.description) {
if (!meta.find((i: any) => i.name === 'description'))
meta.push({ name: 'description', content: head.description })
if (!meta.find((i: any) => i.property === 'og:description'))
meta.push({ property: 'og:description', content: head.description })
if (!meta.find((i: any) => i.name === 'twitter:description'))
meta.push({ name: 'twitter:description', content: head.description })
}
if (head.image) {
if (!meta.find((i: any) => i.property === 'og:image'))
meta.push({ property: 'og:image', content: head.image })
if (!meta.find((i: any) => i.name === 'twitter:image'))
meta.push({ name: 'twitter:image', content: head.image })
if (!meta.find((i: any) => i.property === 'twitter:card'))
meta.push({ name: 'twitter:card', content: 'summary_large_image' })
}
const result: any = {}
for (const [key, value] of Object.entries(head)) {
if (headProperties.includes(key))
result[key] = value
}
return Object.entries(result).length === 0 ? null : result
}
+250
View File
@@ -0,0 +1,250 @@
import type { TransformResult } from 'vite'
import type { MarkdownEnv, ResolvedOptions } from '../types'
import { toArray, uniq } from '@antfu/utils'
import { componentPlugin } from '@mdit-vue/plugin-component'
import { frontmatterPlugin } from '@mdit-vue/plugin-frontmatter'
import MarkdownIt from 'markdown-it-async'
import { preprocessHead } from './head'
const scriptSetupRE = /<\s*script([^>]*)\bsetup\b([^>]*)>([\s\S]*)<\/script>/g
const defineExposeRE = /defineExpose\s*\(/g
const EXPORTS_KEYWORDS = [
'class',
'default',
'export',
'function',
'import',
'let',
'var',
'const',
'from',
'as',
'return',
'if',
'else',
'switch',
'case',
'break',
'for',
'while',
'do',
]
interface ScriptMeta {
code: string
attr: string
}
function extractScriptSetup(html: string) {
const scripts: ScriptMeta[] = []
html = html.replace(scriptSetupRE, (_, attr1, attr2, code) => {
scripts.push({
code,
attr: `${attr1} ${attr2}`.trim(),
})
return ''
})
return { html, scripts }
}
function extractCustomBlock(html: string, options: ResolvedOptions) {
const blocks: string[] = []
for (const tag of options.customSfcBlocks) {
html = html.replace(new RegExp(`<${tag}[^>]*\\b[^>]*>[^<]*<\\/${tag}>`, 'gm'), (code) => {
blocks.push(code)
return ''
})
}
return { html, blocks }
}
export function createMarkdown(options: ResolvedOptions) {
const isVue2 = options.vueVersion.startsWith('2.')
const markdown = MarkdownIt({
html: true,
linkify: true,
typographer: true,
...options.markdownItOptions,
})
markdown.use(componentPlugin, options.componentOptions)
if (options.frontmatter || options.excerpt) {
markdown.use(frontmatterPlugin, {
...options.frontmatterOptions,
grayMatterOptions: {
excerpt: options.excerpt,
...options.frontmatterOptions.grayMatterOptions,
},
})
}
markdown.linkify.set({ fuzzyLink: false })
options.markdownItUses.forEach((e) => {
const [plugin, options] = toArray(e)
markdown.use(plugin, options)
})
const setupPromise = (async () => {
await options.markdownItSetup(markdown)
})()
return async (id: string, raw: string): Promise<TransformResult> => {
await setupPromise
const {
wrapperClasses,
wrapperComponent,
transforms,
headEnabled,
frontmatterPreprocess,
} = options
raw = raw.trimStart()
raw = transforms.before?.(raw, id) ?? raw
const env: MarkdownEnv = { id }
let html = await markdown.renderAsync(raw, env)
const { excerpt = '', frontmatter: data = null } = env
const wrapperClassesResolved = toArray(
typeof wrapperClasses === 'function'
? wrapperClasses(id, raw)
: wrapperClasses,
)
.filter(Boolean)
.join(' ')
if (wrapperClassesResolved)
html = `<div class="${wrapperClassesResolved}">${html}</div>`
else
html = `<div>${html}</div>`
const wrapperComponentName = typeof wrapperComponent === 'function'
? wrapperComponent(id, raw)
: wrapperComponent
if (wrapperComponentName) {
const attrs = [
options.frontmatter && ':frontmatter="frontmatter"',
options.excerpt && ':excerpt="excerpt"',
].filter(Boolean).join(' ')
html = `<${wrapperComponentName} ${attrs}>${html}</${wrapperComponentName}>`
}
html = transforms.after?.(html, id) ?? html
if (options.escapeCodeTagInterpolation) {
// escape curly brackets interpolation in <code>, #14
html = html.replace(/<code(.*?)>/g, '<code$1 v-pre>')
}
const hoistScripts = extractScriptSetup(html)
html = hoistScripts.html
const customBlocks = extractCustomBlock(html, options)
html = customBlocks.html
const scriptLines: string[] = []
let frontmatterExportsLines: string[] = []
let excerptExportsLine = ''
let excerptKeyOverlapping = false
function hasExplicitExports() {
return defineExposeRE.test(hoistScripts.scripts.map(i => i.code).join(''))
}
if (options.frontmatter) {
if (options.excerpt && data) {
if (data.excerpt !== undefined)
excerptKeyOverlapping = true
data.excerpt = excerpt
}
const { head, frontmatter } = frontmatterPreprocess(data || {}, options, id, preprocessHead)
if (options.excerpt && !excerptKeyOverlapping && frontmatter.excerpt !== undefined)
delete frontmatter.excerpt
scriptLines.push(`const frontmatter = ${JSON.stringify(frontmatter)}`)
if (options.exportFrontmatter) {
frontmatterExportsLines = Object.entries(frontmatter)
.map(([key, value]) => {
if (EXPORTS_KEYWORDS.includes(key))
key = `_${key}`
return `export const ${key} = ${JSON.stringify(value)}`
})
}
if (!isVue2 && options.exposeFrontmatter && !hasExplicitExports())
scriptLines.push('defineExpose({ frontmatter })')
if (!isVue2 && headEnabled && head) {
// @ts-expect-error legacy option
if (headEnabled === 'vueuse')
throw new Error('unplugin-vue-markdown no longer supports @vueuse/head. Change `headEnabled` to `true` and install `@unhead/vue` instead.')
scriptLines.push(`const head = ${JSON.stringify(head)}`)
scriptLines.unshift(`import { useHead } from "@unhead/vue"`)
scriptLines.push('useHead(head)')
}
scriptLines.push(...transforms.extraScripts?.(frontmatter, id) || [])
}
if (options.excerpt) {
scriptLines.push(`const excerpt = ${JSON.stringify(excerpt)}`)
if (!excerptKeyOverlapping)
excerptExportsLine = `export const excerpt = ${JSON.stringify(excerpt)}\n`
if (!isVue2 && options.exposeExcerpt && !hasExplicitExports())
scriptLines.push('defineExpose({ excerpt })')
}
scriptLines.push(...hoistScripts.scripts.map(i => i.code))
let attrs = uniq(hoistScripts.scripts.map(i => i.attr)).join(' ').trim()
if (attrs)
attrs = ` ${attrs}`
const scripts = isVue2
? [
`<script${attrs}>`,
...scriptLines,
...frontmatterExportsLines,
excerptExportsLine,
'export default { data() { return { frontmatter } } }',
'</script>',
]
: [
`<script setup${attrs}>`,
...scriptLines,
'</script>',
...((frontmatterExportsLines.length || excerptExportsLine)
? [
`<script${attrs}>`,
...frontmatterExportsLines,
excerptExportsLine,
'</script>',
]
: []),
]
const code = [
`<template>${html}</template>`,
...scripts.map(i => i.trim()).filter(Boolean),
...customBlocks.blocks,
].join('\n')
return {
code,
map: { mappings: '' } as any,
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import type { Options, ResolvedOptions } from '../types'
import { getVueVersion } from './utils'
export function resolveOptions(userOptions: Options): ResolvedOptions {
const defaultOptions: ResolvedOptions = {
headEnabled: false,
headField: '',
frontmatter: true,
excerpt: false,
exposeFrontmatter: true,
exposeExcerpt: false,
exportFrontmatter: true,
escapeCodeTagInterpolation: true,
customSfcBlocks: ['route', 'i18n', 'style'],
componentOptions: {},
frontmatterOptions: {},
markdownItOptions: {},
markdownItUses: [],
markdownItSetup: () => {},
wrapperComponent: null,
transforms: {},
vueVersion: userOptions.vueVersion || getVueVersion(),
wrapperClasses: 'markdown-body',
include: null,
exclude: null,
frontmatterPreprocess: (frontmatter, options, _id, defaults) => {
return {
head: defaults(frontmatter, options),
frontmatter,
}
},
}
const options = {
...defaultOptions,
...userOptions,
}
return options as ResolvedOptions
}
+17
View File
@@ -0,0 +1,17 @@
import { createRequire } from 'node:module'
const _require = typeof require === 'undefined'
? createRequire(import.meta.url)
: require
export function getVueVersion(defaultVersion = '3.2.0') {
try {
let v = _require('vue')
if (v.default)
v = v.default
return v.version || defaultVersion
}
catch {
return defaultVersion
}
}
+4
View File
@@ -0,0 +1,4 @@
import { createEsbuildPlugin } from 'unplugin'
import { unpluginFactory } from '.'
export default createEsbuildPlugin(unpluginFactory)
+47
View File
@@ -0,0 +1,47 @@
import type { UnpluginFactory } from 'unplugin'
import type { Options } from './types'
import { createFilter } from '@rollup/pluginutils'
import { createUnplugin } from 'unplugin'
import { createMarkdown } from './core/markdown'
import { resolveOptions } from './core/options'
const cssIdRE = /\.(css|postcss|sass|scss|less|stylus|styl)($|\?)/
export const unpluginFactory: UnpluginFactory<Options> = (userOptions = {}) => {
const options = resolveOptions(userOptions)
const markdownToVue = createMarkdown(options)
const filter = createFilter(
userOptions.include || /\.md$|\.md\?vue/,
userOptions.exclude || cssIdRE,
)
return {
name: 'unplugin-vue-markdown',
enforce: 'pre',
transformInclude(id) {
return filter(id)
},
async transform(raw, id) {
try {
return await markdownToVue(id, raw)
}
catch (e: any) {
this.error(e)
}
},
vite: {
async handleHotUpdate(ctx) {
if (!filter(ctx.file))
return
const defaultRead = ctx.read
ctx.read = async function () {
return (await markdownToVue(ctx.file, await defaultRead())).code
}
},
},
}
}
export default /* #__PURE__ */ createUnplugin(unpluginFactory)
+4
View File
@@ -0,0 +1,4 @@
import { createRollupPlugin } from 'unplugin'
import { unpluginFactory } from '.'
export default createRollupPlugin(unpluginFactory)
+4
View File
@@ -0,0 +1,4 @@
import { createRspackPlugin } from 'unplugin'
import { unpluginFactory } from '.'
export default createRspackPlugin(unpluginFactory)
+206
View File
@@ -0,0 +1,206 @@
import type { ComponentPluginOptions } from '@mdit-vue/plugin-component'
import type { FrontmatterPluginOptions } from '@mdit-vue/plugin-frontmatter'
import type { MarkdownItEnv } from '@mdit-vue/types'
import type { FilterPattern } from '@rollup/pluginutils'
import type {
MarkdownItAsync,
MarkdownItAsyncOptions,
PluginSimple as MarkdownItPluginSimple,
PluginWithOptions as MarkdownItPluginWithOptions,
} from 'markdown-it-async'
import type { preprocessHead } from './core/head'
/** a `<meta />` property in HTML is defined with the following name/values */
export interface MetaProperty {
key?: string
/**
* the "name" property used by Facebook and other providers who
* use the Opengraph standards
*/
property?: string
/**
* used by google to identify the "name" of the name/value pair
*/
itemprop?: string
/**
* used by Twitter to indicate the "name" field in a meta properties
* name/value pairing
*/
name?: string
/**
* The value of the meta property
*/
content?: any
[key: string]: unknown
}
/**
* Frontmatter content is represented as key/value dictionary
*/
export interface Frontmatter {
title?: string
name?: string
description?: string
meta?: MetaProperty[]
[key: string]: unknown
}
export interface Options {
/**
* Explicitly set the Vue version
*
* @default auto detected
*/
vueVersion?: string
/**
* Enable head support, need to install @unhead/vue and register to App in main.js
*
* @default true
*/
headEnabled?: boolean
/**
* The head field in frontmatter used to be used for @unhead/vue
*
* When an empty string is passed, it will use the root properties of the frontmatter
*
* @default ''
*/
headField?: string
/**
* Parse for frontmatter
*
* @default true
*/
frontmatter?: boolean
/**
* Parse for excerpt
*
* If `true`, it will be passed to `frontmatterPreprocess` as `frontmatter.excerpt`, replacing the `excerpt` key in frontmatter, if there's any
*
* @default false
*/
excerpt?: boolean
/**
* Remove custom SFC block
*
* @default ['route', 'i18n']
*/
customSfcBlocks?: string[]
/**
* Options passed to [@mdit-vue/plugin-component](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component)
*/
componentOptions?: ComponentPluginOptions
/**
* Options passed to [@mdit-vue/plugin-frontmatter](https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter)
*/
frontmatterOptions?: FrontmatterPluginOptions
/**
* Custom function to provide defaults to the frontmatter and
* move certain attributes into the "meta" category.
*
* Note: _overriding this will remove built-in functionality setting
* "meta" properties and the built-in "head" support. Do this only
* if you know what you're doing._
*/
frontmatterPreprocess?: (
frontmatter: Frontmatter,
options: ResolvedOptions,
id: string,
defaultHeadProcess: typeof preprocessHead,
) => {
head: Record<string, any>
frontmatter: Frontmatter
}
/**
* Expose frontmatter via expose API
*
* @default true
*/
exposeFrontmatter?: boolean
/**
* Expose excerpt via expose API
*
* @default false
*/
exposeExcerpt?: boolean
/**
* Export frontmatter in component module
*
* @default true
*/
exportFrontmatter?: boolean
/**
* Add `v-pre` to `<code>` tag to escape curly brackets interpolation
*
* @see https://github.com/unplugin/unplugin-vue-markdown/issues/14
* @default true
*/
escapeCodeTagInterpolation?: boolean
/**
* Options passed to Markdown It
*/
markdownItOptions?: MarkdownItAsyncOptions
/**
* Plugins for Markdown It
*/
markdownItUses?: (
| MarkdownItPluginSimple
| [MarkdownItPluginSimple | MarkdownItPluginWithOptions<any>, any]
| any
)[]
/**
* A function providing the Markdown It instance gets the ability to apply custom
* settings/plugins
*/
markdownItSetup?: (MarkdownIt: MarkdownItAsync) => void | Promise<void>
/**
* Class names for wrapper div
*
* @default 'markdown-body'
*/
wrapperClasses?: string | string[] | undefined | null | ((id: string, code: string) => string | string[] | undefined | null)
/**
* Component name to wrapper with
*
* @default undefined
*/
wrapperComponent?: string | undefined | null | ((id: string, code: string) => string | undefined | null)
/**
* Custom tranformations apply before and after the markdown transformation
*/
transforms?: {
before?: (code: string, id: string) => string
after?: (code: string, id: string) => string
/**
* Return extra code to be injected into the `<script>` tag
*/
extraScripts?: (frontmatter: Record<string, any>, id: string) => string[]
}
include?: FilterPattern
exclude?: FilterPattern
}
export interface ResolvedOptions extends Required<Options> {}
export interface MarkdownEnv extends MarkdownItEnv {
id: string
}
+4
View File
@@ -0,0 +1,4 @@
import { createVitePlugin } from 'unplugin'
import { unpluginFactory } from '.'
export default createVitePlugin(unpluginFactory)
+4
View File
@@ -0,0 +1,4 @@
import { createWebpackPlugin } from 'unplugin'
import { unpluginFactory } from '.'
export default createWebpackPlugin(unpluginFactory) as any