Repository: unplugin/unplugin-vue-markdown Branch: main Commit: 5dbb5be2a296 Files: 49 Total size: 55.7 KB Directory structure: gitextract_xl89wua4/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── LICENSE ├── README.md ├── eslint.config.js ├── examples/ │ ├── vite/ │ │ ├── App.vue │ │ ├── Counter.vue │ │ ├── Counter2.vue │ │ ├── README.md │ │ ├── index.html │ │ ├── main.ts │ │ ├── package.json │ │ ├── pages/ │ │ │ ├── index.md │ │ │ └── route.vue │ │ ├── vite.config.ts │ │ └── vue-shim.d.ts │ └── vue-cli/ │ ├── babel.config.js │ ├── jsconfig.json │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.vue │ │ ├── Hi.md │ │ └── main.js │ └── vue.config.js ├── package.json ├── pnpm-workspace.yaml ├── scripts/ │ └── postbuild.ts ├── src/ │ ├── core/ │ │ ├── head.ts │ │ ├── markdown.ts │ │ ├── options.ts │ │ └── utils.ts │ ├── esbuild.ts │ ├── index.ts │ ├── rollup.ts │ ├── rspack.ts │ ├── types.ts │ ├── vite.ts │ └── webpack.ts ├── test/ │ ├── __snapshots__/ │ │ ├── excerpt.test.ts.snap │ │ └── transform.test.ts.snap │ ├── excerpt.test.ts │ ├── fixtures/ │ │ └── simple.md │ ├── frontmatterPreprocessor.test.ts │ └── transform.test.ts ├── tsconfig.json └── tsup.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: antfu github: [antfu] ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v*' jobs: release: uses: sxzz/workflows/.github/workflows/release.yml@v1 with: publish: true permissions: contents: write id-token: write ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - main - master pull_request: branches: - main - master jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: node-version: [20.x, lts/*] os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: false steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Setup run: npm i -g pnpm @antfu/ni - name: Install run: nci - name: Build run: nr build - name: Test run: nr test --if-present ================================================ FILE: .gitignore ================================================ node_modules .DS_Store dist example/README.md .idea/ ================================================ FILE: .vscode/settings.json ================================================ { // Enable the ESlint flat config support "eslint.experimental.useFlatConfig": true, // Disable the default formatter, use eslint instead "prettier.enable": false, "editor.formatOnSave": false, // Auto fix "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "never" }, // Silent the stylistic rules in you IDE, but still auto fix them "eslint.rules.customizations": [ { "rule": "style/*", "severity": "off" }, { "rule": "*-indent", "severity": "off" }, { "rule": "*-spacing", "severity": "off" }, { "rule": "*-spaces", "severity": "off" }, { "rule": "*-order", "severity": "off" }, { "rule": "*-dangle", "severity": "off" }, { "rule": "*-newline", "severity": "off" }, { "rule": "*quotes", "severity": "off" }, { "rule": "*semi", "severity": "off" } ], // Enable eslint for all supported languages "eslint.validate": [ "javascript", "javascriptreact", "typescript", "typescriptreact", "vue", "html", "markdown", "json", "jsonc", "yaml" ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020-PRESENT Anthony Fu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # unplugin-vue-markdown [![NPM version](https://img.shields.io/npm/v/unplugin-vue-markdown?color=a1b858)](https://www.npmjs.com/package/unplugin-vue-markdown) Compile Markdown to Vue component. - 📚 Use Markdown as Vue components. - 💚 Use Vue components in Markdown. - 🔌 Supports Vite, Webpack, Vue CLI and more, powered by [unplugin](https://github.com/unjs/unplugin). - ⚡️ The same transformation as [VitePress](https://vitepress.vuejs.org/). ## Install ```bash npm i unplugin-vue-markdown ```
Vite
```ts // vite.config.ts import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' export default defineConfig({ plugins: [ Vue({ include: [/\.vue$/, /\.md$/], // <-- allows Vue to compile Markdown files }), Markdown({ /* options */ }), ], }) ``` Example: [`examples/vite`](./examples/vite/)
Webpack
```ts // webpack.config.js const Markdown = require('unplugin-vue-markdown/webpack') const { VueLoaderPlugin } = require('vue-loader') module.exports = { /* ... */ module: { rules: [ // ... other rules { test: /\.(vue|md)$/, loader: 'vue-loader' } ] }, plugins: [ new VueLoaderPlugin(), Markdown({ /* options */ }) ] } ```
Vue CLI
```ts // vue.config.js const Markdown = require('unplugin-vue-markdown/webpack') module.exports = { parallel: false, // Disable thread-loader which will cause errors, we are still investigating the root cause chainWebpack: (config) => { config.module .rule('vue') .test(/\.(vue|md)$/) // <-- allows Vue to compile Markdown files config .plugin('markdown') .use(Markdown({ markdownUses: [ prism, ], })) }, } ``` Example: [`examples/vue-cli`](./examples/vue-cli/)
## Import Markdown as Vue components ```html ``` ## Use Vue Components inside Markdown You can even use Vue components inside your markdown, for example ```html ``` Note you can either register the components globally, or use the ` ``` Or you can use [`unplugin-vue-components`](#work-with-unplugin-vue-components) for auto components registration. ## Frontmatter Frontmatter will be parsed and inject into Vue's instance data `frontmatter` field. For example: ```md --- name: My Cool App --- # Hello World This is {{frontmatter.name}} ``` Will be rendered as ```html

Hello World

This is My Cool App

``` It will also be passed to the wrapper component's props if you have set `wrapperComponent` option. ## Document head and meta To manage document head and meta, you would need to install [`@unhead/vue`](https://unhead.harlanzw.com/integrations/vue/setup) and do some setup. ```bash npm i @unhead/vue ``` ```js // vite.config.js import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown({ headEnabled: true // <-- }) ] } ``` ```js // src/main.js import { createHead } from '@unhead/vue/client' // <-- import { createApp } from 'vue' const app = createApp(App) const head = createHead() // <-- app.use(head) // <-- ``` Then you can use frontmatter to control the head. For example: ```yaml --- title: My Cool App meta: - name: description content: Hello World --- ``` For more options available, please refer to [`@unhead/vue`'s docs](https://unhead.harlanzw.com/integrations/vue/setup). ## Options `unplugin-vue-markdown` uses [`markdown-exit`](https://github.com/nicepkg/markdown-exit) under the hood, a TypeScript rewrite of markdown-it with built-in async rendering. markdown-it plugins are compatible. > **Note:** The old option names (`markdownItOptions`, `markdownItUses`, `markdownItSetup`) are deprecated but still functional. ```ts // vite.config.js import MarkdownItAnchor from 'markdown-it-anchor' import MarkdownItPrism from 'markdown-it-prism' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Markdown({ // default options passed to markdown-exit markdownOptions: { html: true, linkify: true, typographer: true, }, // A function providing the markdown-exit instance gets the ability to apply custom settings/plugins markdownSetup(md) { // for example md.use(MarkdownItAnchor) md.use(MarkdownItPrism) }, // Class names for the wrapper div wrapperClasses: 'markdown-body' }) ], } ``` See [the tsdoc](./src/types.ts) for more advanced options ## Example See the [/examples](./examples). Or the pre-configured Markdown template [Vitesse](https://github.com/antfu/vitesse). ## Integrations ### Work with [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) ```ts import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' import Pages from 'vite-plugin-pages' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Pages({ extensions: ['vue', 'md'], }), Markdown() ], } ``` Put your markdown under `./src/pages/xx.md`, then you can access the page via route `/xx`. ### Work with [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) `unplugin-vue-components` allows you to do on-demand components auto-importing without worrying about registration. ```ts import Vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown(), // should be placed after `Markdown()` Components({ // allow auto load markdown components under `./src/components/` extensions: ['vue', 'md'], // allow auto import and register components used in markdown include: [/\.vue$/, /\.vue\?vue/, /\.md$/], }) ], } ``` Components under `./src/components` can be directly used in markdown components, and markdown components can also be put under `./src/components` to be auto imported. ## TypeScript Shim ```ts declare module '*.vue' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } declare module '*.md' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } ``` ## License MIT License © 2020-PRESENT [Anthony Fu](https://github.com/antfu) ================================================ FILE: eslint.config.js ================================================ // @ts-check import antfu from '@antfu/eslint-config' export default antfu() ================================================ FILE: examples/vite/App.vue ================================================ ================================================ FILE: examples/vite/Counter.vue ================================================ ================================================ FILE: examples/vite/Counter2.vue ================================================ ================================================ FILE: examples/vite/README.md ================================================ # unplugin-vue-markdown [![NPM version](https://img.shields.io/npm/v/unplugin-vue-markdown?color=a1b858)](https://www.npmjs.com/package/unplugin-vue-markdown) Compile Markdown to Vue component. - 📚 Use Markdown as Vue components. - 💚 Use Vue components in Markdown. - 🔌 Supports Vite, Webpack, Vue CLI and more, powered by [unplugin](https://github.com/unjs/unplugin). - ⚡️ The same transformation as [VitePress](https://vitepress.vuejs.org/). ## Install ```bash npm i unplugin-vue-markdown ```
Vite
```ts // vite.config.ts import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' export default defineConfig({ plugins: [ Vue({ include: [/\.vue$/, /\.md$/], // <-- allows Vue to compile Markdown files }), Markdown({ /* options */ }), ], }) ``` Example: [`examples/vite`](./examples/vite/)
Webpack
```ts // webpack.config.js const Markdown = require('unplugin-vue-markdown/webpack') const { VueLoaderPlugin } = require('vue-loader') module.exports = { /* ... */ module: { rules: [ // ... other rules { test: /\.(vue|md)$/, loader: 'vue-loader' } ] }, plugins: [ new VueLoaderPlugin(), Markdown({ /* options */ }) ] } ```
Vue CLI
```ts // vue.config.js const Markdown = require('unplugin-vue-markdown/webpack') module.exports = { parallel: false, // Disable thread-loader which will cause errors, we are still investigating the root cause chainWebpack: (config) => { config.module .rule('vue') .test(/\.(vue|md)$/) // <-- allows Vue to compile Markdown files config .plugin('markdown') .use(Markdown({ markdownItUses: [ prism, ], })) }, } ``` Example: [`examples/vue-cli`](./examples/vue-cli/)
## Import Markdown as Vue components ```html ``` ## Use Vue Components inside Markdown You can even use Vue components inside your markdown, for example ```html ``` Note you can either register the components globally, or use the ` ``` Or you can use [`unplugin-vue-components`](#work-with-unplugin-vue-components) for auto components registration. ## Frontmatter Frontmatter will be parsed and inject into Vue's instance data `frontmatter` field. For example: ```md --- name: My Cool App --- # Hello World This is {{frontmatter.name}} ``` Will be rendered as ```html

Hello World

This is My Cool App

``` It will also be passed to the wrapper component's props if you have set `wrapperComponent` option. ## Document head and meta To manage document head and meta, you would need to install [`@unhead/vue`](https://unhead.harlanzw.com/integrations/vue/setup) and do some setup. ```bash npm i @unhead/vue ``` ```js // vite.config.js import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown({ headEnabled: true // <-- }) ] } ``` ```js // src/main.js import { createHead } from '@unhead/vue/client' // <-- import { createApp } from 'vue' const app = createApp(App) const head = createHead() // <-- app.use(head) // <-- ``` Then you can use frontmatter to control the head. For example: ```yaml --- title: My Cool App meta: - name: description content: Hello World --- ``` For more options available, please refer to [`@unhead/vue`'s docs](https://unhead.harlanzw.com/integrations/vue/setup). ## Options `unplugin-vue-markdown` uses [`markdown-it`](https://github.com/markdown-it/markdown-it) under the hood, see [`markdown-it`'s docs](https://markdown-it.github.io/markdown-it/) for more details ```ts // vite.config.js import MarkdownItAnchor from 'markdown-it-anchor' import MarkdownItPrism from 'markdown-it-prism' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Markdown({ // default options passed to markdown-it // see: https://markdown-it.github.io/markdown-it/ markdownItOptions: { html: true, linkify: true, typographer: true, }, // A function providing the Markdown It instance gets the ability to apply custom settings/plugins markdownItSetup(md) { // for example md.use(MarkdownItAnchor) md.use(MarkdownItPrism) }, // Class names for the wrapper div wrapperClasses: 'markdown-body' }) ], } ``` See [the tsdoc](./src/types.ts) for more advanced options ## Example See the [/examples](./examples). Or the pre-configured Markdown template [Vitesse](https://github.com/antfu/vitesse). ## Integrations ### Work with [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages) ```ts import Vue from '@vitejs/plugin-vue' import Markdown from 'unplugin-vue-markdown/vite' import Pages from 'vite-plugin-pages' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Pages({ extensions: ['vue', 'md'], }), Markdown() ], } ``` Put your markdown under `./src/pages/xx.md`, then you can access the page via route `/xx`. ### Work with [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) `unplugin-vue-components` allows you to do on-demand components auto-importing without worrying about registration. ```ts import Vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import Markdown from 'unplugin-vue-markdown/vite' export default { plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown(), // should be placed after `Markdown()` Components({ // allow auto load markdown components under `./src/components/` extensions: ['vue', 'md'], // allow auto import and register components used in markdown include: [/\.vue$/, /\.vue\?vue/, /\.md$/], }) ], } ``` Components under `./src/components` can be directly used in markdown components, and markdown components can also be put under `./src/components` to be auto imported. ## TypeScript Shim ```ts declare module '*.vue' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } declare module '*.md' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } ``` ## License MIT License © 2020-PRESENT [Anthony Fu](https://github.com/antfu) ================================================ FILE: examples/vite/index.html ================================================ Vite MD
================================================ FILE: examples/vite/main.ts ================================================ import { createHead } from '@unhead/vue/client' import routes from 'pages-generated' import { createApp } from 'vue' import { createRouter, createWebHistory, } from 'vue-router' import App from './App.vue' import Counter from './Counter.vue' import 'prismjs' import 'prismjs/components/prism-bash' import 'prismjs/components/prism-typescript' import 'prismjs/components/prism-javascript' import 'prismjs/components/prism-markup' import 'prismjs/components/prism-markup-templating' const app = createApp(App) const head = createHead() const router = createRouter({ history: createWebHistory(), routes, }) app.use(head) app.use(router) app.component('Counter', Counter) app.mount('#app') ================================================ FILE: examples/vite/package.json ================================================ { "type": "module", "private": true, "scripts": { "dev": "DEBUG=\"unplugin-vue-markdown:*\" vite", "build": "DEBUG=\"unplugin-vue-markdown:*\" vite build" }, "dependencies": { "@unhead/vue": "^2.0.17", "vue": "^3.5.21", "vue-router": "^4.5.1" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", "markdown-it-prism": "^3.0.0", "prismjs": "^1.30.0", "typescript": "^5.9.2", "unplugin-vue-markdown": "workspace:*", "vite": "^7.1.7", "vite-plugin-inspect": "^11.3.3", "vite-plugin-pages": "^0.33.1" } } ================================================ FILE: examples/vite/pages/index.md ================================================ --- title: Hello meta: - name: description content: Hello World test: test --- # Hello world! Front matter: {{ frontmatter }}
Home { meta: { layout: 'home' } } ================================================ FILE: examples/vite/pages/route.vue ================================================ { meta: { layout: 'home' } } ================================================ FILE: examples/vite/vite.config.ts ================================================ import Vue from '@vitejs/plugin-vue' import prism from 'markdown-it-prism' import Markdown from 'unplugin-vue-markdown/vite' import { defineConfig } from 'vite' import Inspect from 'vite-plugin-inspect' import Pages from 'vite-plugin-pages' export default defineConfig({ plugins: [ Vue({ include: [/\.vue$/, /\.md$/], }), Markdown({ markdownOptions: { }, headEnabled: true, markdownUses: [ prism, ], }), Pages({ pagesDir: 'pages', extensions: ['vue', 'md'], }), Inspect(), ], build: { sourcemap: true, }, }) ================================================ FILE: examples/vite/vue-shim.d.ts ================================================ declare module '*.vue' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } declare module '*.md' { import type { ComponentOptions } from 'vue' const Component: ComponentOptions export default Component } ================================================ FILE: examples/vue-cli/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset', ], } ================================================ FILE: examples/vue-cli/jsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": [ "esnext", "dom", "dom.iterable", "scripthost" ], "baseUrl": "./", "module": "esnext", "moduleResolution": "node", "paths": { "@/*": [ "src/*" ] } } } ================================================ FILE: examples/vue-cli/package.json ================================================ { "name": "example-vue-cli", "version": "0.1.0", "private": true, "scripts": { "dev": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "core-js": "^3.45.1", "vue": "^3.5.21" }, "devDependencies": { "@babel/core": "^7.28.4", "@vue/cli-plugin-babel": "~5.0.9", "@vue/cli-service": "~5.0.9", "unplugin-vue-markdown": "workspace:*" }, "browserslist": [ "> 1%", "last 2 versions", "not dead", "not ie 11" ] } ================================================ FILE: examples/vue-cli/public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: examples/vue-cli/src/App.vue ================================================ ================================================ FILE: examples/vue-cli/src/Hi.md ================================================ # Hello Hey ================================================ FILE: examples/vue-cli/src/main.js ================================================ import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue-cli/vue.config.js ================================================ const { defineConfig } = require('@vue/cli-service') const prism = require('markdown-it-prism') const Markdown = require('unplugin-vue-markdown/webpack') module.exports = defineConfig({ transpileDependencies: true, parallel: false, chainWebpack: (config) => { config.module .rule('vue') .test(/\.(vue|md)$/) config .plugin('markdown') .use(Markdown({ markdownUses: [ prism, ], })) }, }) ================================================ FILE: package.json ================================================ { "name": "unplugin-vue-markdown", "type": "module", "version": "30.0.0", "packageManager": "pnpm@10.17.1", "description": "Compile Markdown to Vue component", "author": "Anthony Fu ", "license": "MIT", "funding": "https://github.com/sponsors/antfu", "homepage": "https://github.com/unplugin/unplugin-vue-markdown", "repository": { "type": "git", "url": "https://github.com/unplugin/unplugin-vue-markdown" }, "bugs": "https://github.com/unplugin/unplugin-vue-markdown/issues", "keywords": [ "vite", "unplugin", "markdown-exit" ], "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./vite": { "types": "./dist/vite.d.ts", "import": "./dist/vite.js", "require": "./dist/vite.cjs" }, "./webpack": { "types": "./dist/webpack.d.ts", "import": "./dist/webpack.js", "require": "./dist/webpack.cjs" }, "./rollup": { "types": "./dist/rollup.d.ts", "import": "./dist/rollup.js", "require": "./dist/rollup.cjs" }, "./esbuild": { "types": "./dist/esbuild.d.ts", "import": "./dist/esbuild.js", "require": "./dist/esbuild.cjs" }, "./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js", "require": "./dist/types.cjs" }, "./*": "./*" }, "main": "dist/index.cjs", "module": "dist/index.js", "types": "dist/index.d.ts", "typesVersions": { "*": { "*": [ "./dist/*", "./*" ] } }, "engines": { "node": ">=20" }, "scripts": { "dev": "tsup --watch", "example:dev": "pnpm -C examples/vite run dev", "example:build": "pnpm -C examples/vite run build", "build": "tsup", "build:fix": "tsx scripts/postbuild.ts", "test": "vitest", "test:update": "vitest -u", "lint": "eslint .", "prepublishOnly": "pnpm run build", "release": "bumpp" }, "peerDependencies": { "vite": "^2.0.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" }, "dependencies": { "@mdit-vue/plugin-component": "^3.0.2", "@mdit-vue/plugin-frontmatter": "^3.0.2", "@mdit-vue/types": "^3.0.2", "markdown-exit": "^1.0.0-beta.8", "unplugin": "^2.3.10", "unplugin-utils": "^0.3.0" }, "devDependencies": { "@antfu/eslint-config": "^5.4.1", "@antfu/ni": "^26.0.1", "@antfu/utils": "^9.2.1", "@types/node": "^24.5.2", "@unhead/vue": "^2.0.17", "@vue/test-utils": "^2.4.6", "ansis": "^4.1.0", "bumpp": "^10.2.3", "eslint": "^9.36.0", "fast-glob": "^3.3.3", "rollup": "^4.52.2", "tsup": "^8.5.0", "tsx": "^4.20.5", "typescript": "^5.9.2", "vite": "^7.1.7", "vitest": "^3.2.4", "vue": "^3.5.21", "webpack": "^5.101.3" }, "pnpm": { "onlyBuiltDependencies": [ "core-js", "esbuild" ] } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - examples/* ignoreWorkspaceRootCheck: true strictPeerDependencies: false shamefullyHoist: true shellEmulator: true ================================================ FILE: scripts/postbuild.ts ================================================ import { promises as fs } from 'node:fs' import { basename, dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import ansis from 'ansis' import fg from 'fast-glob' async function run() { // fix cjs exports const files = await fg('*.cjs', { ignore: ['chunk-*'], absolute: true, cwd: resolve(dirname(fileURLToPath(import.meta.url)), '../dist'), }) for (const file of files) { console.log(ansis.cyan.inverse(' POST '), `Fix ${basename(file)}`) let code = await fs.readFile(file, 'utf8') code = code.replace('exports.default =', 'module.exports =') code += 'exports.default = module.exports;' await fs.writeFile(file, code) } } run() ================================================ FILE: src/core/head.ts ================================================ import type { Frontmatter, MetaProperty, ResolvedOptions } from '../types' const headProperties = [ 'title', 'meta', 'link', 'base', 'style', 'script', 'htmlAttrs', 'bodyAttrs', ] export function preprocessHead(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 } ================================================ FILE: src/core/markdown.ts ================================================ 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 { createMarkdownExit } from 'markdown-exit' 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 setupPromise = (async () => { const md = createMarkdownExit({ html: true, linkify: true, typographer: true, ...options.markdownOptions, }) md.use(componentPlugin as any, options.componentOptions) if (options.frontmatter || options.excerpt) { md.use(frontmatterPlugin as any, { ...options.frontmatterOptions, grayMatterOptions: { excerpt: options.excerpt, ...options.frontmatterOptions.grayMatterOptions, }, }) } md.linkify.set({ fuzzyLink: false }) options.markdownUses.forEach((e) => { const [plugin, options] = toArray(e) md.use(plugin, options) }) await options.markdownSetup(md) return md })() return async (id: string, raw: string): Promise => { const md = await setupPromise const { wrapperDiv, wrapperClasses, wrapperComponent, transforms, headEnabled, frontmatterPreprocess, } = options raw = raw.trimStart() raw = await transforms.before?.(raw, id) ?? raw const env: MarkdownEnv = { id } let html = await md.renderAsync(raw, env) const { excerpt = '', frontmatter: data = null } = env if (wrapperDiv) { const wrapperClassesResolved = toArray( typeof wrapperClasses === 'function' ? wrapperClasses(id, raw) : wrapperClasses, ) .filter(Boolean) .join(' ') if (wrapperClassesResolved) html = `
${html}
` else html = `
${html}
` } 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}` } html = await transforms.after?.(html, id) ?? html if (options.escapeCodeTagInterpolation) { // escape curly brackets interpolation in , #14 html = html.replace(//g, '') } 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(...await 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 ? [ ``, ...scriptLines, ...frontmatterExportsLines, excerptExportsLine, 'export default { data() { return { frontmatter } } }', '', ] : [ `', ...((frontmatterExportsLines.length || excerptExportsLine) ? [ ``, ...frontmatterExportsLines, excerptExportsLine, '', ] : []), ] const code = [ ``, ...scripts.map(i => i.trim()).filter(Boolean), ...customBlocks.blocks, ].join('\n') return { code, map: { mappings: '' } as any, } } } ================================================ FILE: src/core/options.ts ================================================ import type { Options, ResolvedOptions } from '../types' import { getVueVersion } from './utils' export function resolveOptions(userOptions: Options): ResolvedOptions { const noop = () => {} // Resolve with new names taking precedence over deprecated old names const markdownOptions = userOptions.markdownOptions ?? userOptions.markdownItOptions ?? {} const markdownUses = userOptions.markdownUses ?? userOptions.markdownItUses ?? [] const markdownSetup = userOptions.markdownSetup ?? userOptions.markdownItSetup ?? noop const defaultOptions: ResolvedOptions = { headEnabled: false, headField: '', frontmatter: true, excerpt: false, exposeFrontmatter: true, exposeExcerpt: false, exportFrontmatter: true, escapeCodeTagInterpolation: true, customSfcBlocks: ['route', 'i18n', 'style'], componentOptions: {}, frontmatterOptions: {}, markdownOptions: {}, markdownUses: [], markdownSetup: noop, markdownItOptions: {}, markdownItUses: [], markdownItSetup: noop, wrapperDiv: true, 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, // Set both old and new keys to the resolved value markdownOptions, markdownUses, markdownSetup, markdownItOptions: markdownOptions, markdownItUses: markdownUses, markdownItSetup: markdownSetup, } return options as ResolvedOptions } ================================================ FILE: src/core/utils.ts ================================================ 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 } } ================================================ FILE: src/esbuild.ts ================================================ import { createEsbuildPlugin } from 'unplugin' import { unpluginFactory } from '.' export default createEsbuildPlugin(unpluginFactory) ================================================ FILE: src/index.ts ================================================ import type { UnpluginFactory } from 'unplugin' import type { Options } from './types' import { createUnplugin } from 'unplugin' import { createFilter } from 'unplugin-utils' import { createMarkdown } from './core/markdown' import { resolveOptions } from './core/options' const cssIdRE = /\.(css|postcss|sass|scss|less|stylus|styl)($|\?)/ export const unpluginFactory: UnpluginFactory = (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) ================================================ FILE: src/rollup.ts ================================================ import { createRollupPlugin } from 'unplugin' import { unpluginFactory } from '.' export default createRollupPlugin(unpluginFactory) ================================================ FILE: src/rspack.ts ================================================ import { createRspackPlugin } from 'unplugin' import { unpluginFactory } from '.' export default createRspackPlugin(unpluginFactory) ================================================ FILE: src/types.ts ================================================ 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 MarkdownExit from 'markdown-exit' import type { MarkdownExitOptions, PluginSimple, PluginWithOptions, } from 'markdown-exit' import type { FilterPattern } from 'unplugin-utils' import type { preprocessHead } from './core/head' /** a `` 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 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 `` tag to escape curly brackets interpolation * * @see https://github.com/unplugin/unplugin-vue-markdown/issues/14 * @default true */ escapeCodeTagInterpolation?: boolean /** * Options passed to markdown-exit */ markdownOptions?: MarkdownExitOptions /** * Plugins for markdown-exit */ markdownUses?: ( | PluginSimple | [PluginSimple | PluginWithOptions, any] | any )[] /** * A function providing the markdown-exit instance gets the ability to apply custom * settings/plugins */ markdownSetup?: (md: MarkdownExit) => void | Promise /** * @deprecated Use `markdownOptions` instead */ markdownItOptions?: MarkdownExitOptions /** * @deprecated Use `markdownUses` instead */ markdownItUses?: ( | PluginSimple | [PluginSimple | PluginWithOptions, any] | any )[] /** * @deprecated Use `markdownSetup` instead */ markdownItSetup?: (md: MarkdownExit) => void | Promise /** * Wrap the rendered html in a div * * @default true */ wrapperDiv?: boolean /** * Class names for wrapper div * * This option will be ignored if `wrapperDiv` is set to `false` * * @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 | Promise after?: (code: string, id: string) => string | Promise /** * Return extra code to be injected into the ` " `; exports[`excerpt > rendered excerpt 1`] = ` " " `; ================================================ FILE: test/__snapshots__/transform.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`transform > basic 1`] = ` " " `; exports[`transform > code escape 1`] = ` " " `; exports[`transform > couldn't expose frontmatter 1`] = ` " " `; exports[`transform > escapeCodeTagInterpolation 1`] = ` " " `; exports[`transform > export keyword frontmatters 1`] = ` " " `; exports[`transform > exposes frontmatter 1`] = ` " " `; exports[`transform > frontmatter interpolation 1`] = ` " " `; exports[`transform > script setup 1`] = ` " " `; exports[`transform > style 1`] = ` " " `; exports[`transform > vue directives 1`] = ` " " `; ================================================ FILE: test/excerpt.test.ts ================================================ import { describe, expect, it } from 'vitest' import { createMarkdown } from '../src/core/markdown' import { resolveOptions } from '../src/core/options' describe('excerpt', () => { it('rendered excerpt', async () => { const options = resolveOptions({ excerpt: true, frontmatterOptions: { grayMatterOptions: { excerpt: true, excerpt_separator: '', }, }, }) const markdownToVue = createMarkdown(options) const md = `--- title: Hey --- This is an excerpt which has been rendered to **HTML**. # Hello - A - B - C` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('raw excerpt', async () => { const options = resolveOptions({ excerpt: true, frontmatterOptions: { renderExcerpt: false, grayMatterOptions: { excerpt: true, excerpt_separator: '', }, }, }) const markdownToVue = createMarkdown(options) const md = `--- title: Hey --- This is an excerpt which is kept as **raw Markdown**. # Hello - A - B - C` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) }) ================================================ FILE: test/fixtures/simple.md ================================================ --- title: Hello World description: testing is the path to true happiness --- # My H1 lorem ipsum ## Transformed base: {{ title }} ================================================ FILE: test/frontmatterPreprocessor.test.ts ================================================ import type { MetaProperty, ResolvedOptions } from '../src/types' import { readFile } from 'node:fs/promises' import { describe, expect, it } from 'vitest' import { createMarkdown } from '../src/core/markdown' import { resolveOptions } from '../src/core/options' const frontmatterPreprocess: ResolvedOptions['frontmatterPreprocess'] = (fm) => { const frontmatter = { title: 'default title', description: 'default description', ...fm, } const meta: MetaProperty[] = [ { property: 'og:title', name: 'twitter:title', itemprop: 'title', content: frontmatter.title }, { property: 'og:description', name: 'twitter:description', itemprop: 'description', content: frontmatter.description, }, ] return { head: { ...frontmatter, meta }, frontmatter: { ...frontmatter, meta }, } } describe('provide bespoke frontmatter processor', () => { it('inline markdown is used over default properties', async () => { const markdownToVue = createMarkdown(resolveOptions({ frontmatterPreprocess })) const md = (await markdownToVue('', await readFile('test/fixtures/simple.md', 'utf-8'))).code // Positive tests expect( md.includes('Hello World'), 'the title attribute is retained over the default \'title\' value', ).toBeTruthy() expect( md.includes('testing is the path to true happiness'), 'description property is also retained', ).toBeTruthy() // Negative tests expect( md.includes('default title'), 'the title attribute is retained over the default \'title\' value', ).toBeFalsy() expect(md.includes('default description'), 'default description is ignored').toBeFalsy() // Meta props expect(md.includes('og:title')).toBeTruthy() expect(md.includes('og:description')).toBeTruthy() }) }) ================================================ FILE: test/transform.test.ts ================================================ import { describe, expect, it } from 'vitest' import { createMarkdown } from '../src/core/markdown' import { resolveOptions } from '../src/core/options' describe('transform', async () => { const options = resolveOptions({}) const markdownToVue = createMarkdown(options) it('basic', async () => { const md = `--- title: Hey --- # Hello - A - B - C ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('style', async () => { const md = ` # Hello ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('script setup', async () => { const md = ` # Hello ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('exposes frontmatter', async () => { const md = `--- title: Hey --- # Hello` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('couldn\'t expose frontmatter', async () => { const md = `--- title: Hey --- ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('escapeCodeTagInterpolation', async () => { const md = `
{{hello}}
\`\`\`ts
{{hello}}
\`\`\` ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('frontmatter interpolation', async () => { const md = ` --- name: 'My Cool App' --- # Hello World This is {{frontmatter.name}} ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('vue directives', async () => { const md = ` --- name: 'My Cool App' --- ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('export keyword frontmatters', async () => { const md = ` --- class: 'text' default: 'foo' --- Hello ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) it('code escape', async () => { const md = ` Hello \`{{ world }}\` \`\`\`js console.log(\`{{ world }}\`) \`\`\` ` expect((await markdownToVue('', md)).code).toMatchSnapshot() }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es2020", "jsx": "preserve", "lib": ["esnext", "DOM"], "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "strict": true, "strictNullChecks": true, "esModuleInterop": true, "skipLibCheck": true }, "exclude": [ "examples/**", "dist/**" ] } ================================================ FILE: tsup.config.ts ================================================ import type { Options } from 'tsup' export default { entryPoints: [ 'src/*.ts', ], clean: true, format: ['cjs', 'esm'], dts: true, shims: true, splitting: true, onSuccess: 'npm run build:fix', }