├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── react │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── LinkToRepository.jsx │ │ ├── brew.png │ │ ├── content.md │ │ ├── index.css │ │ ├── logo.svg │ │ └── main.jsx │ └── vite.config.js └── vue │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── assets │ │ ├── content.md │ │ └── logo.png │ ├── brew.png │ ├── components │ │ ├── HelloWorld.vue │ │ └── LinkToRepository.vue │ ├── index.css │ └── main.js │ └── vite.config.ts ├── package-lock.json ├── package.json ├── src └── index.ts ├── test └── acceptance │ ├── cypress.json │ ├── integration │ └── vue_spec.js │ ├── plugins │ └── index.js │ └── support │ ├── commands.js │ └── index.js └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "ignorePatterns": [ 8 | "dist/*" 9 | ], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/comma-dangle": ["error", "always-multiline"], 17 | "@typescript-eslint/no-var-requires": "off", 18 | "@typescript-eslint/quotes": ["error", "single"], 19 | "@typescript-eslint/semi": ["error", "never"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | ignore: 8 | - dependency-name: '@types/node' 9 | versions: 10 | - 22.x 11 | groups: 12 | eslint-family: 13 | patterns: 14 | - 'eslint' 15 | - '@typescript-eslint/*' 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Build typescript project 26 | run: | 27 | npm install 28 | npm run build 29 | 30 | - name: Run Linter 31 | run: | 32 | npm run lint 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kengo Hamasaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vite-plugin-markdown 2 | 3 | [![npm](https://img.shields.io/npm/v/vite-plugin-markdown.svg?style=for-the-badge)](https://www.npmjs.com/package/vite-plugin-markdown) [![npm beta channel](https://img.shields.io/npm/v/vite-plugin-markdown/beta?style=for-the-badge&label=beta&color=yellow)](https://www.npmjs.com/package/vite-plugin-markdown/v/beta) 4 | 5 | 6 | A plugin enables you to import a Markdown file as various formats on your [vite](https://github.com/vitejs/vite) project. 7 | 8 | ## Setup 9 | 10 | ``` 11 | npm i -D vite-plugin-markdown 12 | ``` 13 | 14 |
15 | For vite v1 16 | 17 | ``` 18 | npm i -D vite-plugin-markdown@vite-1 19 | ``` 20 |
21 | 22 | ### Config 23 | 24 | ```js 25 | import mdPlugin from 'vite-plugin-markdown' 26 | 27 | module.exports = { 28 | plugins: [mdPlugin(options)] 29 | } 30 | ``` 31 | 32 | Then you can import front matter attributes from `.md` file as default. 33 | 34 | ```md 35 | --- 36 | title: Awesome Title 37 | description: Describe this awesome content 38 | tags: 39 | - "great" 40 | - "awesome" 41 | - "rad" 42 | --- 43 | 44 | # This is awesome 45 | 46 | Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production. 47 | ``` 48 | 49 | ```ts 50 | import { attributes } from './contents/the-doc.md'; 51 | 52 | console.log(attributes) //=> { title: 'Awesome Title', description: 'Describe this awesome content', tags: ['great', 'awesome', 'rad'] } 53 | ``` 54 | 55 | ### Options 56 | 57 | ```ts 58 | mode?: ('html' | 'markdown' | 'toc' | 'react' | 'vue')[] 59 | markdown?: (body: string) => string 60 | markdownIt?: MarkdownIt | MarkdownIt.Options 61 | ``` 62 | 63 | Enum for `mode` is provided as `Mode` 64 | 65 | ```ts 66 | import { Mode } from 'vite-plugin-markdown' 67 | 68 | console.log(Mode.HTML) //=> 'html' 69 | console.log(Mode.MARKDOWN) //=> 'markdown' 70 | console.log(Mode.TOC) //=> 'toc' 71 | console.log(Mode.REACT) //=> 'react' 72 | console.log(Mode.VUE) //=> 'vue' 73 | ``` 74 | 75 | "Mode" enables you to import markdown file in various formats (HTML, ToC, React/Vue Component) 76 | 77 | #### `Mode.HTML` 78 | 79 |
80 | Import compiled HTML 81 | 82 | ```md 83 | # This is awesome 84 | 85 | Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production. 86 | ``` 87 | 88 | ```ts 89 | import { html } from './contents/the-doc.md'; 90 | 91 | console.log(html) //=> "

This is awesome

ite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production.

" 92 | ``` 93 | 94 |
95 | 96 | #### `Mode.MARKDOWN` 97 | 98 |
99 | Import the raw Markdown content 100 | 101 | ```js 102 | import { markdown } from './contents/the-doc.md' 103 | 104 | console.log(markdown) //=> "# This is awesome \n Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production." 105 | ``` 106 |
107 | 108 | #### `Mode.TOC` 109 | 110 |
111 | Import ToC metadata 112 | 113 | ```md 114 | # vite 115 | 116 | Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production. 117 | 118 | ## Status 119 | 120 | ## Getting Started 121 | 122 | # Notes 123 | ``` 124 | 125 | ```ts 126 | import { toc } from './contents/the-doc.md' 127 | 128 | console.log(toc) //=> [{ level: '1', content: 'vite' }, { level: '2', content: 'Status' }, { level: '2', content: 'Getting Started' }, { level: '1', content: 'Notes' },] 129 | ``` 130 | 131 |
132 | 133 | #### `Mode.REACT` 134 | 135 |
136 | Import as a React component 137 | 138 | ```jsx 139 | import React from 'react' 140 | import { ReactComponent } from './contents/the-doc.md' 141 | 142 | function MyReactApp() { 143 | return ( 144 |
145 | 146 |
147 | } 148 | ``` 149 | 150 |
151 | Custom Element on a markdown file can be runnable as a React component as well 152 | 153 | ```md 154 | # This is awesome 155 | 156 | Vite is 157 | ``` 158 | 159 | ```jsx 160 | import React from 'react' 161 | import { ReactComponent } from './contents/the-doc.md' 162 | import { MyComponent } from './my-component' 163 | 164 | function MyReactApp() { 165 | return ( 166 |
167 | 168 |
169 | } 170 | ``` 171 | 172 | `MyComponent` on markdown perform as a React component. 173 | 174 |
175 |
176 | 177 | #### `Mode.VUE` 178 | 179 |
180 | Import as a Vue component 181 | 182 | ```vue 183 | 188 | 189 | 198 | ``` 199 | 200 |
201 | Custom Element on a markdown file can be runnable as a Vue component as well 202 | 203 | ```md 204 | # This is awesome 205 | 206 | Vite is 207 | ``` 208 | 209 | ```vue 210 | 215 | 216 | 226 | ``` 227 | 228 | `MyComponent` on markdown perform as a Vue component. 229 | 230 |
231 |
232 | 233 | 234 | ### Type declarations 235 | 236 | In TypeScript project, need to declare typedefs for `.md` file as you need. 237 | 238 | ```ts 239 | declare module '*.md' { 240 | // "unknown" would be more detailed depends on how you structure frontmatter 241 | const attributes: Record; 242 | 243 | // When "Mode.TOC" is requested 244 | const toc: { level: string, content: string }[]; 245 | 246 | // When "Mode.HTML" is requested 247 | const html: string; 248 | 249 | // When "Mode.RAW" is requested 250 | const raw: string 251 | 252 | // When "Mode.React" is requested. VFC could take a generic like React.VFC<{ MyComponent: TypeOfMyComponent }> 253 | import React from 'react' 254 | const ReactComponent: React.VFC; 255 | 256 | // When "Mode.Vue" is requested 257 | import { ComponentOptions, Component } from 'vue'; 258 | const VueComponent: ComponentOptions; 259 | const VueComponentWith: (components: Record) => ComponentOptions; 260 | 261 | // Modify below per your usage 262 | export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }; 263 | } 264 | ``` 265 | 266 | Save as `vite.d.ts` for instance. 267 | 268 | ## License 269 | 270 | MIT 271 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-markdown-sample-react", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "devDependencies": { 11 | "@vitejs/plugin-react-refresh": "1.3.6", 12 | "vite": "5.4.10", 13 | "vite-plugin-markdown": "file:../.." 14 | }, 15 | "dependencies": { 16 | "@babel/preset-react": "7.23.3", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /examples/react/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import logo from './logo.svg' 3 | import './App.css' 4 | import { html, markdown, toc, ReactComponent } from './content.md' 5 | import LinkToRepository from './LinkToRepository' 6 | 7 | function App() { 8 | const [count, setCount] = useState(0) 9 | 10 | return ( 11 |
12 |
13 | logo 14 |

Hello Vite + React!

15 |

16 | 17 |

18 |

19 | Edit App.jsx and save to test HMR updates. 20 |

21 | 27 | Learn React 28 | 29 |
30 |
31 |

HTML

32 | {html} 33 |

ToC

34 | {toc.map((h,i) =>
  • {h.level} - {h.content}
  • )} 35 |

    ReactComponent

    36 | 37 |

    raw markdown

    38 |
    39 | {markdown} 40 |
    41 |
    42 |
    43 | ) 44 | } 45 | 46 | export default App 47 | -------------------------------------------------------------------------------- /examples/react/src/LinkToRepository.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function LinkToRepository({ color, children }) { 4 | const style = { color: color || '#eee' } 5 | return ( 6 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/react/src/brew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmsk/vite-plugin-markdown/26e97108120624c8a0f218ea5b15cbde511b108b/examples/react/src/brew.png -------------------------------------------------------------------------------- /examples/react/src/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Content 3 | --- 4 | # Content 5 | 6 | ## Second header 7 | 8 | Hello `something` okay 9 | 10 | ```js 11 | const a = `{}`; 12 | const b = "\n"; 13 | Object.keys(a).forEach(key => something); 14 | 15 | console.log("#"); 16 | ``` 17 | 18 | # : this is HTML special chars 19 | 20 | ## Link to the repo 21 | 22 | Open Repository 23 | 24 | ## img tag should be closed as JSX 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/react/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/react/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /examples/react/vite.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'vite' 3 | import reactRefresh from '@vitejs/plugin-react-refresh' 4 | import mdPlugin, { Mode } from 'vite-plugin-markdown' 5 | 6 | /** 7 | * @type { import('vite').UserConfig } 8 | */ 9 | export default defineConfig({ 10 | plugins: [reactRefresh, mdPlugin({ mode: [Mode.HTML, Mode.MARKDOWN, Mode.TOC, Mode.REACT] })], 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
    11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-markdown-sample-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "@vitejs/plugin-vue": "5.0.3", 13 | "vite": "5.4.10", 14 | "vite-plugin-markdown": "file:../.." 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmsk/vite-plugin-markdown/26e97108120624c8a0f218ea5b15cbde511b108b/examples/vue/public/favicon.ico -------------------------------------------------------------------------------- /examples/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /examples/vue/src/assets/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: It's Vue sample 3 | description: 4 | This repository is created to run acceptance tests with Vue 3 5 | tags: 6 | - Vue 7 | - vite 8 | - FML 9 | --- 10 | 11 | # Hello 12 | 13 | ## I'm Vue 14 | 15 | Echo 16 | 17 | ```js 18 | {{ something.value }} 19 | 20 | getSomething().then((response) => { 21 | const result = parse(response) 22 | }) 23 | 24 | a = `b` 25 | b = "\n" 26 | 27 | console.log("©"); 28 | ``` 29 | 30 | © : this is HTML special chars 31 | 32 | `{{ something.value }}` 33 | 34 | ## This section has a custom element 35 | 36 | See Repository 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/vue/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmsk/vite-plugin-markdown/26e97108120624c8a0f218ea5b15cbde511b108b/examples/vue/src/assets/logo.png -------------------------------------------------------------------------------- /examples/vue/src/brew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmsk/vite-plugin-markdown/26e97108120624c8a0f218ea5b15cbde511b108b/examples/vue/src/brew.png -------------------------------------------------------------------------------- /examples/vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /examples/vue/src/components/LinkToRepository.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /examples/vue/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: #2c3e50; 7 | margin-top: 60px; 8 | } 9 | -------------------------------------------------------------------------------- /examples/vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.css' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /examples/vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import VuePlugin from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | import plugin, { Mode } from 'vite-plugin-markdown' 4 | 5 | /** 6 | * @type { import('vite').UserConfig } 7 | */ 8 | export default defineConfig({ 9 | plugins: [VuePlugin(), plugin({ mode: [Mode.HTML, Mode.TOC, Mode.VUE] })], 10 | }) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-markdown", 3 | "version": "3.0.0-2", 4 | "description": "Import markdown files in vite", 5 | "exports": "./dist/index.js", 6 | "type": "module", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "dev": "tsc -w -p .", 12 | "build": "rm -rf dist && tsc -p .", 13 | "lint": "eslint . --ext .ts", 14 | "test:acceptance": "npm run cypress:run", 15 | "cypress:run": "cypress run --config-file test/acceptance/cypress.json", 16 | "cypress:open": "cypress open --config-file test/acceptance/cypress.json" 17 | }, 18 | "keywords": [ 19 | "vite", 20 | "frontmatter", 21 | "markdown", 22 | "react", 23 | "vue" 24 | ], 25 | "author": "Kengo Hamasaki ", 26 | "contributors": [ 27 | "sapphi-red ", 28 | "Alex Zheng ", 29 | "Dafrok ", 30 | "Carretta Riccardo ", 31 | "John Sanders ", 32 | "Stefan van Herwijnen ", 33 | "Richard Raita <48387692+ric-rai@users.noreply.github.com>", 34 | "Felix Fritz " 35 | ], 36 | "license": "MIT", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/hmsk/vite-plugin-markdown.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/hmsk/vite-plugin-markdown/issues" 43 | }, 44 | "homepage": "https://github.com/hmsk/vite-plugin-markdown/tree/main/#readme", 45 | "dependencies": { 46 | "domhandler": "^5.0.0", 47 | "front-matter": "^4.0.0", 48 | "htmlparser2": "^9.0.0", 49 | "markdown-it": "^14.0.0" 50 | }, 51 | "peerDependencies": { 52 | "vite": ">= 5.0.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "7.26.0", 56 | "@types/babel__core": "7.20.5", 57 | "@types/markdown-it": "14.1.2", 58 | "@types/node": "20.17.6", 59 | "@typescript-eslint/eslint-plugin": "6.21.0", 60 | "@typescript-eslint/parser": "6.21.0", 61 | "@vue/compiler-sfc": "3.5.12", 62 | "cypress": "13.15.1", 63 | "eslint": "8.56.0", 64 | "typescript": "5.3.3", 65 | "vite": "5.4.10" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Frontmatter, { FrontMatterResult } from 'front-matter' 2 | import MarkdownIt, { Options as MarkdownItOptions } from 'markdown-it' 3 | import { Plugin } from 'vite' 4 | import { TransformResult } from 'rollup' 5 | import { parseDOM, DomUtils } from 'htmlparser2' 6 | import { Element, Node as DomHandlerNode } from 'domhandler' 7 | 8 | export enum Mode { 9 | TOC = 'toc', 10 | HTML = 'html', 11 | REACT = 'react', 12 | VUE = 'vue', 13 | MARKDOWN = 'markdown', 14 | } 15 | 16 | export interface PluginOptions { 17 | mode?: Mode[] 18 | markdown?: (body: string) => string 19 | markdownIt?: MarkdownIt | MarkdownItOptions 20 | } 21 | 22 | const markdownCompiler = (options: PluginOptions): MarkdownIt | { render: (body: string) => string } => { 23 | if (options.markdownIt) { 24 | if (options.markdownIt instanceof MarkdownIt || (options.markdownIt?.constructor?.name === 'MarkdownIt')) { 25 | return options.markdownIt as MarkdownIt 26 | } else if (typeof options.markdownIt === 'object') { 27 | return MarkdownIt(options.markdownIt) 28 | } 29 | } else if (options.markdown) { 30 | return { render: options.markdown } 31 | } 32 | return MarkdownIt({ html: true, xhtmlOut: options.mode?.includes(Mode.REACT) }) // TODO: xhtmlOut should be got rid of in next major update 33 | } 34 | 35 | class ExportedContent { 36 | #exports: string[] = [] 37 | #contextCode = '' 38 | 39 | addContext (contextCode: string): void { 40 | this.#contextCode += `${contextCode}\n` 41 | } 42 | 43 | addExporting (exported: string): void { 44 | this.#exports.push(exported) 45 | } 46 | 47 | export (): string { 48 | return [this.#contextCode, `export { ${this.#exports.join(', ')} }`].join('\n') 49 | } 50 | } 51 | 52 | const tf = async (code: string, id: string, options: PluginOptions): Promise => { 53 | if (!id.endsWith('.md')) return null 54 | 55 | const content = new ExportedContent() 56 | const fm = (Frontmatter as unknown as ((file: string) => FrontMatterResult))(code) 57 | content.addContext(`const attributes = ${JSON.stringify(fm.attributes)}`) 58 | content.addExporting('attributes') 59 | 60 | const html = markdownCompiler(options).render(fm.body) 61 | if (options.mode?.includes(Mode.HTML)) { 62 | content.addContext(`const html = ${JSON.stringify(html)}`) 63 | content.addExporting('html') 64 | } 65 | 66 | if (options.mode?.includes(Mode.MARKDOWN)) { 67 | content.addContext(`const markdown = ${JSON.stringify(fm.body)}`) 68 | content.addExporting('markdown') 69 | } 70 | 71 | if (options.mode?.includes(Mode.TOC)) { 72 | const root = parseDOM(html) 73 | const indicies = root.filter( 74 | rootSibling => rootSibling instanceof Element && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(rootSibling.tagName) 75 | ) as Element[] 76 | 77 | const toc: { level: string; content: string }[] = indicies.map(index => ({ 78 | level: index.tagName.replace('h', ''), 79 | content: DomUtils.getInnerHTML(index), 80 | })) 81 | 82 | content.addContext(`const toc = ${JSON.stringify(toc)}`) 83 | content.addExporting('toc') 84 | } 85 | 86 | if (options.mode?.includes(Mode.REACT)) { 87 | const root = parseDOM(html, { lowerCaseTags: false }) 88 | const subComponentNamespace = 'SubReactComponent' 89 | 90 | const markCodeAsPre = (node: DomHandlerNode): void => { 91 | if (node instanceof Element) { 92 | if (node.tagName.match(/^[A-Z].+/)) { 93 | node.tagName = `${subComponentNamespace}.${node.tagName}` 94 | } 95 | if (['pre', 'code'].includes(node.tagName) && node.attribs?.class) { 96 | node.attribs.className = node.attribs.class 97 | delete node.attribs.class 98 | } 99 | 100 | if (node.tagName === 'code') { 101 | const codeContent = DomUtils.getInnerHTML(node, { decodeEntities: true }) 102 | node.attribs.dangerouslySetInnerHTML = `vfm{{ __html: \`${codeContent.replace(/([\\`])/g, '\\$1')}\`}}vfm` 103 | node.childNodes = [] 104 | } 105 | 106 | if (node.childNodes.length > 0) { 107 | node.childNodes.forEach(markCodeAsPre) 108 | } 109 | } 110 | } 111 | root.forEach(markCodeAsPre) 112 | 113 | const h = DomUtils.getOuterHTML(root, { selfClosingTags: true }).replace(/"vfm{{/g, '{{').replace(/}}vfm"/g, '}}') 114 | 115 | const reactCode = ` 116 | const markdown = 117 |
    118 | ${h} 119 |
    120 | ` 121 | const compiledReactCode = ` 122 | function (props) { 123 | Object.keys(props).forEach(function (key) { 124 | SubReactComponent[key] = props[key] 125 | }) 126 | ${(await import('@babel/core')).transformSync(reactCode, { ast: false, presets: ['@babel/preset-react'] })?.code} 127 | return markdown 128 | } 129 | ` 130 | content.addContext(`import React from "react"\nconst ${subComponentNamespace} = {}\nconst ReactComponent = ${compiledReactCode}`) 131 | content.addExporting('ReactComponent') 132 | } 133 | 134 | if (options.mode?.includes(Mode.VUE)) { 135 | const root = parseDOM(html) 136 | 137 | // Top-level
     tags become 
    138 |     root.forEach((node: DomHandlerNode) => {
    139 |       if (node instanceof Element) {
    140 |         if (['pre', 'code'].includes(node.tagName)) {
    141 |           node.attribs['v-pre'] = 'true'
    142 |         }
    143 |       }
    144 |     })
    145 | 
    146 |     // Any  tag becomes  excepting under `
    `
    147 |     const markCodeAsPre = (node: DomHandlerNode): void => {
    148 |       if (node instanceof Element) {
    149 |         if (node.tagName === 'code') node.attribs['v-pre'] = 'true'
    150 |         if (node.childNodes.length > 0) node.childNodes.forEach(markCodeAsPre)
    151 |       }
    152 |     }
    153 |     root.forEach(markCodeAsPre)
    154 | 
    155 |     const { code: compiledVueCode } = (await import('@vue/compiler-sfc')).compileTemplate({ source: DomUtils.getOuterHTML(root, { decodeEntities: true }), filename: id, id })
    156 |     content.addContext(compiledVueCode.replace('\nexport function render(', '\nfunction vueRender(') + `\nconst VueComponent = { render: vueRender }\nVueComponent.__hmrId = ${JSON.stringify(id)}\nconst VueComponentWith = (components) => ({ components, render: vueRender })\n`)
    157 |     content.addExporting('VueComponent')
    158 |     content.addExporting('VueComponentWith')
    159 |   }
    160 | 
    161 |   return {
    162 |     code: content.export(),
    163 |   }
    164 | }
    165 | 
    166 | export const plugin = (options: PluginOptions = {}): Plugin => {
    167 |   return {
    168 |     name: 'vite-plugin-markdown',
    169 |     enforce: 'pre',
    170 |     transform (code, id) {
    171 |       return tf(code, id, options)
    172 |     },
    173 |   }
    174 | }
    175 | 
    176 | export default plugin
    177 | 
    
    
    --------------------------------------------------------------------------------
    /test/acceptance/cypress.json:
    --------------------------------------------------------------------------------
     1 | {
     2 |   "integrationFolder": "test/acceptance/integration",
     3 |   "testFiles": "**/*_spec.*",
     4 |   "fixturesFolder": "test/acceptance/fixtures",
     5 |   "pluginsFile": "test/acceptance/plugins",
     6 |   "screenshotsFolder": "test/acceptance/screenshots",
     7 |   "supportFile": "test/acceptance/support/index.js",
     8 |   "video": false
     9 | }
    10 | 
    
    
    --------------------------------------------------------------------------------
    /test/acceptance/integration/vue_spec.js:
    --------------------------------------------------------------------------------
    1 | describe('My First Test', () => {
    2 |   it('Does not do much!', () => {
    3 |     expect(true).to.equal(true)
    4 |   })
    5 | })
    6 | 
    
    
    --------------------------------------------------------------------------------
    /test/acceptance/plugins/index.js:
    --------------------------------------------------------------------------------
     1 | /// 
     2 | // ***********************************************************
     3 | // This example plugins/index.js can be used to load plugins
     4 | //
     5 | // You can change the location of this file or turn off loading
     6 | // the plugins file with the 'pluginsFile' configuration option.
     7 | //
     8 | // You can read more here:
     9 | // https://on.cypress.io/plugins-guide
    10 | // ***********************************************************
    11 | 
    12 | // This function is called when a project is opened or re-opened (e.g. due to
    13 | // the project's config changing)
    14 | 
    15 | /**
    16 |  * @type {Cypress.PluginConfig}
    17 |  */
    18 | module.exports = (on, config) => {
    19 |   // `on` is used to hook into various events Cypress emits
    20 |   // `config` is the resolved Cypress config
    21 | }
    22 | 
    
    
    --------------------------------------------------------------------------------
    /test/acceptance/support/commands.js:
    --------------------------------------------------------------------------------
     1 | // ***********************************************
     2 | // This example commands.js shows you how to
     3 | // create various custom commands and overwrite
     4 | // existing commands.
     5 | //
     6 | // For more comprehensive examples of custom
     7 | // commands please read more here:
     8 | // https://on.cypress.io/custom-commands
     9 | // ***********************************************
    10 | //
    11 | //
    12 | // -- This is a parent command --
    13 | // Cypress.Commands.add("login", (email, password) => { ... })
    14 | //
    15 | //
    16 | // -- This is a child command --
    17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
    18 | //
    19 | //
    20 | // -- This is a dual command --
    21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
    22 | //
    23 | //
    24 | // -- This will overwrite an existing command --
    25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
    26 | 
    
    
    --------------------------------------------------------------------------------
    /test/acceptance/support/index.js:
    --------------------------------------------------------------------------------
     1 | // ***********************************************************
     2 | // This example support/index.js is processed and
     3 | // loaded automatically before your test files.
     4 | //
     5 | // This is a great place to put global configuration and
     6 | // behavior that modifies Cypress.
     7 | //
     8 | // You can change the location of this file or turn off
     9 | // automatically serving support files with the
    10 | // 'supportFile' configuration option.
    11 | //
    12 | // You can read more here:
    13 | // https://on.cypress.io/configuration
    14 | // ***********************************************************
    15 | 
    16 | // Import commands.js using ES2015 syntax:
    17 | import './commands'
    18 | 
    19 | // Alternatively you can use CommonJS syntax:
    20 | // require('./commands')
    21 | 
    
    
    --------------------------------------------------------------------------------
    /tsconfig.json:
    --------------------------------------------------------------------------------
     1 | {
     2 |   "compilerOptions": {
     3 |     "target": "ES2022",
     4 |     "moduleResolution": "Node16",
     5 |     "strict": true,
     6 |     "declaration": true,
     7 |     "noUnusedLocals": true,
     8 |     "esModuleInterop": true,
     9 |     "outDir": "dist",
    10 |     "module": "Node16",
    11 |     "lib": ["ES2022", "DOM"],
    12 |     "sourceMap": true
    13 |   },
    14 |   "include": ["./src"]
    15 | }
    16 | 
    
    
    --------------------------------------------------------------------------------