mirror of https://github.com/vuejs/vitepress
parent
21d3cd8cbe
commit
5c47bbb463
@ -1,3 +0,0 @@
|
|||||||
export function markdownToVue(content: string): string {
|
|
||||||
return `<template>${content}</template>`
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import path from 'path'
|
|
||||||
import {
|
|
||||||
createServer as createViteServer,
|
|
||||||
cachedRead,
|
|
||||||
Plugin,
|
|
||||||
Resolver
|
|
||||||
} from 'vite'
|
|
||||||
import { markdownToVue } from './markdown'
|
|
||||||
|
|
||||||
const debug = require('debug')('vitepress')
|
|
||||||
|
|
||||||
// built ts files are placed into /dist
|
|
||||||
const appPath = path.join(__dirname, '../lib/app')
|
|
||||||
// TODO detect user configured theme
|
|
||||||
const themePath = path.join(__dirname, '../lib/theme-default')
|
|
||||||
|
|
||||||
const VitePressResolver: Resolver = {
|
|
||||||
publicToFile(publicPath) {
|
|
||||||
if (publicPath.startsWith('/@app')) {
|
|
||||||
return path.join(appPath, publicPath.replace(/^\/@app\/?/, ''))
|
|
||||||
}
|
|
||||||
if (publicPath.startsWith('/@theme')) {
|
|
||||||
return path.join(themePath, publicPath.replace(/^\/@theme\/?/, ''))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fileToPublic(filePath) {
|
|
||||||
if (filePath.startsWith(appPath)) {
|
|
||||||
return `/@app/${path.relative(appPath, filePath)}`
|
|
||||||
}
|
|
||||||
if (filePath.startsWith(themePath)) {
|
|
||||||
return `/@theme/${path.relative(themePath, filePath)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const VitePressPlugin: Plugin = ({ app, root, watcher, resolver }) => {
|
|
||||||
// watch theme files if it's outside of project root
|
|
||||||
if (path.relative(root, themePath).startsWith('..')) {
|
|
||||||
debug(`watching theme dir outside of project root: ${themePath}`)
|
|
||||||
watcher.add(themePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hot reload .md files as .vue files
|
|
||||||
watcher.on('change', async (file) => {
|
|
||||||
if (file.endsWith('.md')) {
|
|
||||||
const content = await cachedRead(null, file)
|
|
||||||
watcher.handleVueReload(file, Date.now(), markdownToVue(content))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(async (ctx, next) => {
|
|
||||||
if (ctx.path.endsWith('.md')) {
|
|
||||||
await cachedRead(ctx, resolver.publicToFile(ctx.path))
|
|
||||||
// let vite know this is supposed to be treated as vue file
|
|
||||||
ctx.vue = true
|
|
||||||
ctx.body = markdownToVue(ctx.body)
|
|
||||||
debug(`serving ${ctx.url}`)
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect and serve vitepress files
|
|
||||||
const file = VitePressResolver.publicToFile(ctx.path, root)
|
|
||||||
if (file) {
|
|
||||||
ctx.type = path.extname(file)
|
|
||||||
await cachedRead(ctx, file)
|
|
||||||
|
|
||||||
debug(`serving file: ${ctx.url}`)
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
|
|
||||||
await next()
|
|
||||||
|
|
||||||
// serve our index.html after vite history fallback
|
|
||||||
if (ctx.url === '/index.html') {
|
|
||||||
await cachedRead(ctx, path.join(appPath, 'index-dev.html'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createServer() {
|
|
||||||
return createViteServer({
|
|
||||||
plugins: [VitePressPlugin],
|
|
||||||
resolvers: [VitePressResolver]
|
|
||||||
})
|
|
||||||
}
|
|
@ -0,0 +1,105 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { RuleBlock } from 'markdown-it/lib/parser_block'
|
||||||
|
|
||||||
|
// Replacing the default htmlBlock rule to allow using custom components at
|
||||||
|
// root level
|
||||||
|
|
||||||
|
const blockNames: string[] = require('markdown-it/lib/common/html_blocks')
|
||||||
|
const HTML_OPEN_CLOSE_TAG_RE: RegExp = require('markdown-it/lib/common/html_re')
|
||||||
|
.HTML_OPEN_CLOSE_TAG_RE
|
||||||
|
|
||||||
|
// An array of opening and corresponding closing sequences for html tags,
|
||||||
|
// last argument defines whether it can terminate a paragraph or not
|
||||||
|
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [
|
||||||
|
[/^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true],
|
||||||
|
[/^<!--/, /-->/, true],
|
||||||
|
[/^<\?/, /\?>/, true],
|
||||||
|
[/^<![A-Z]/, />/, true],
|
||||||
|
[/^<!\[CDATA\[/, /\]\]>/, true],
|
||||||
|
// PascalCase Components
|
||||||
|
[/^<[A-Z]/, />/, true],
|
||||||
|
// custom elements with hyphens
|
||||||
|
[/^<\w+\-/, />/, true],
|
||||||
|
[
|
||||||
|
new RegExp('^</?(' + blockNames.join('|') + ')(?=(\\s|/?>|$))', 'i'),
|
||||||
|
/^$/,
|
||||||
|
true
|
||||||
|
],
|
||||||
|
[new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false]
|
||||||
|
]
|
||||||
|
|
||||||
|
export const componentPlugin = (md: MarkdownIt) => {
|
||||||
|
md.block.ruler.at('html_block', htmlBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlBlock: RuleBlock = (
|
||||||
|
state,
|
||||||
|
startLine,
|
||||||
|
endLine,
|
||||||
|
silent
|
||||||
|
): boolean => {
|
||||||
|
let i, nextLine, lineText
|
||||||
|
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||||
|
let max = state.eMarks[startLine]
|
||||||
|
|
||||||
|
// if it's indented more than 3 spaces, it should be a code block
|
||||||
|
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.md.options.html) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.src.charCodeAt(pos) !== 0x3c /* < */) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
lineText = state.src.slice(pos, max)
|
||||||
|
|
||||||
|
for (i = 0; i < HTML_SEQUENCES.length; i++) {
|
||||||
|
if (HTML_SEQUENCES[i][0].test(lineText)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === HTML_SEQUENCES.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
// true if this sequence can be a terminator, false otherwise
|
||||||
|
return HTML_SEQUENCES[i][2]
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLine = startLine + 1
|
||||||
|
|
||||||
|
// If we are here - we detected HTML block.
|
||||||
|
// Let's roll down till block end.
|
||||||
|
if (!HTML_SEQUENCES[i][1].test(lineText)) {
|
||||||
|
for (; nextLine < endLine; nextLine++) {
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = state.bMarks[nextLine] + state.tShift[nextLine]
|
||||||
|
max = state.eMarks[nextLine]
|
||||||
|
lineText = state.src.slice(pos, max)
|
||||||
|
|
||||||
|
if (HTML_SEQUENCES[i][1].test(lineText)) {
|
||||||
|
if (lineText.length !== 0) {
|
||||||
|
nextLine++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.line = nextLine
|
||||||
|
|
||||||
|
const token = state.push('html_block', '', 0)
|
||||||
|
token.map = [startLine, nextLine]
|
||||||
|
token.content = state.getLines(startLine, nextLine, state.blkIndent, true)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import Token from 'markdown-it/lib/token'
|
||||||
|
|
||||||
|
const container = require('markdown-it-container')
|
||||||
|
|
||||||
|
export const containerPlugin = (md: MarkdownIt) => {
|
||||||
|
md.use(...createContainer('tip', 'TIP'))
|
||||||
|
.use(...createContainer('warning', 'WARNING'))
|
||||||
|
.use(...createContainer('danger', 'WARNING'))
|
||||||
|
// explicitly escape Vue syntax
|
||||||
|
.use(container, 'v-pre', {
|
||||||
|
render: (tokens: Token[], idx: number) =>
|
||||||
|
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContainerArgs = [
|
||||||
|
typeof container,
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
render(tokens: Token[], idx: number): string
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function createContainer(klass: string, defaultTitle: string): ContainerArgs {
|
||||||
|
return [
|
||||||
|
container,
|
||||||
|
klass,
|
||||||
|
{
|
||||||
|
render(tokens, idx) {
|
||||||
|
const token = tokens[idx]
|
||||||
|
const info = token.info.trim().slice(klass.length).trim()
|
||||||
|
if (token.nesting === 1) {
|
||||||
|
return `<div class="${klass} custom-block"><p class="custom-block-title">${
|
||||||
|
info || defaultTitle
|
||||||
|
}</p>\n`
|
||||||
|
} else {
|
||||||
|
return `</div>\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
const chalk = require('chalk')
|
||||||
|
const prism = require('prismjs')
|
||||||
|
const loadLanguages = require('prismjs/components/index')
|
||||||
|
const escapeHtml = require('escape-html')
|
||||||
|
|
||||||
|
// required to make embedded highlighting work...
|
||||||
|
loadLanguages(['markup', 'css', 'javascript'])
|
||||||
|
|
||||||
|
function wrap(code: string, lang: string): string {
|
||||||
|
if (lang === 'text') {
|
||||||
|
code = escapeHtml(code)
|
||||||
|
}
|
||||||
|
return `<pre v-pre class="language-${lang}"><code>${code}</code></pre>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const highlight = (str: string, lang: string) => {
|
||||||
|
if (!lang) {
|
||||||
|
return wrap(str, 'text')
|
||||||
|
}
|
||||||
|
lang = lang.toLowerCase()
|
||||||
|
const rawLang = lang
|
||||||
|
if (lang === 'vue' || lang === 'html') {
|
||||||
|
lang = 'markup'
|
||||||
|
}
|
||||||
|
if (lang === 'md') {
|
||||||
|
lang = 'markdown'
|
||||||
|
}
|
||||||
|
if (lang === 'ts') {
|
||||||
|
lang = 'typescript'
|
||||||
|
}
|
||||||
|
if (lang === 'py') {
|
||||||
|
lang = 'python'
|
||||||
|
}
|
||||||
|
if (!prism.languages[lang]) {
|
||||||
|
try {
|
||||||
|
loadLanguages([lang])
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`[vuepress] Syntax highlight for language "${lang}" is not supported.`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prism.languages[lang]) {
|
||||||
|
const code = prism.highlight(str, prism.languages[lang], lang)
|
||||||
|
return wrap(code, rawLang)
|
||||||
|
}
|
||||||
|
return wrap(str, 'text')
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
// Modified from https://github.com/egoist/markdown-it-highlight-lines
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
const RE = /{([\d,-]+)}/
|
||||||
|
const wrapperRE = /^<pre .*?><code>/
|
||||||
|
|
||||||
|
export const highlightLinePlugin = (md: MarkdownIt) => {
|
||||||
|
const fence = md.renderer.rules.fence!
|
||||||
|
md.renderer.rules.fence = (...args) => {
|
||||||
|
const [tokens, idx, options] = args
|
||||||
|
const token = tokens[idx]
|
||||||
|
|
||||||
|
const rawInfo = token.info
|
||||||
|
if (!rawInfo || !RE.test(rawInfo)) {
|
||||||
|
return fence(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const langName = rawInfo.replace(RE, '').trim()
|
||||||
|
// ensure the next plugin get the correct lang.
|
||||||
|
token.info = langName
|
||||||
|
|
||||||
|
const lineNumbers = RE.exec(rawInfo)![1]
|
||||||
|
.split(',')
|
||||||
|
.map(v => v.split('-').map(v => parseInt(v, 10)))
|
||||||
|
|
||||||
|
const code = options.highlight
|
||||||
|
? options.highlight(token.content, langName)
|
||||||
|
: token.content
|
||||||
|
|
||||||
|
const rawCode = code.replace(wrapperRE, '')
|
||||||
|
const highlightLinesCode = rawCode.split('\n').map((split, index) => {
|
||||||
|
const lineNumber = index + 1
|
||||||
|
const inRange = lineNumbers.some(([start, end]) => {
|
||||||
|
if (start && end) {
|
||||||
|
return lineNumber >= start && lineNumber <= end
|
||||||
|
}
|
||||||
|
return lineNumber === start
|
||||||
|
})
|
||||||
|
if (inRange) {
|
||||||
|
return `<div class="highlighted"> </div>`
|
||||||
|
}
|
||||||
|
return '<br>'
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
const highlightLinesWrapperCode =
|
||||||
|
`<div class="highlight-lines">${highlightLinesCode}</div>`
|
||||||
|
|
||||||
|
return highlightLinesWrapperCode + code
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
export const hoistPlugin = (md: MarkdownIt & { __data: any }) => {
|
||||||
|
const RE = /^<(script|style)(?=(\s|>|$))/i
|
||||||
|
|
||||||
|
md.renderer.rules.html_block = (tokens, idx) => {
|
||||||
|
const content = tokens[idx].content
|
||||||
|
const hoistedTags = md.__data.hoistedTags || (md.__data.hoistedTags = [])
|
||||||
|
if (RE.test(content.trim())) {
|
||||||
|
hoistedTags.push(content)
|
||||||
|
return ''
|
||||||
|
} else {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { parseHeaders } from '../utils/parseHeaders'
|
||||||
|
import { highlight } from './highlight'
|
||||||
|
import { slugify } from './slugify'
|
||||||
|
import { highlightLinePlugin } from './highlightLines'
|
||||||
|
import { lineNumberPlugin } from './lineNumbers'
|
||||||
|
import { componentPlugin } from './component'
|
||||||
|
import { containerPlugin } from './containers'
|
||||||
|
import { snippetPlugin } from './snippet'
|
||||||
|
import { hoistPlugin } from './hoist'
|
||||||
|
import { preWrapperPlugin } from './preWrapper'
|
||||||
|
import { linkPlugin } from './link'
|
||||||
|
|
||||||
|
const emoji = require('markdown-it-emoji')
|
||||||
|
const anchor = require('markdown-it-anchor')
|
||||||
|
const toc = require('markdown-it-table-of-contents')
|
||||||
|
|
||||||
|
export interface MarkdownOpitons extends MarkdownIt.Options {
|
||||||
|
lineNumbers?: boolean
|
||||||
|
config?: (md: MarkdownIt) => void
|
||||||
|
anchor?: {
|
||||||
|
permalink?: boolean
|
||||||
|
permalinkBefore?: boolean
|
||||||
|
permalinkSymbol?: string
|
||||||
|
}
|
||||||
|
// https://github.com/Oktavilla/markdown-it-table-of-contents
|
||||||
|
toc?: any
|
||||||
|
externalLinks?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarkdownRenderer {
|
||||||
|
__data?: any
|
||||||
|
render: (src: string, env?: any) => { html: string; data: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMarkdownRenderer = (
|
||||||
|
options: MarkdownOpitons = {}
|
||||||
|
): MarkdownRenderer => {
|
||||||
|
const md = MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
highlight,
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
|
||||||
|
// custom plugins
|
||||||
|
md
|
||||||
|
.use(componentPlugin)
|
||||||
|
.use(highlightLinePlugin)
|
||||||
|
.use(preWrapperPlugin)
|
||||||
|
.use(snippetPlugin)
|
||||||
|
.use(hoistPlugin)
|
||||||
|
.use(containerPlugin)
|
||||||
|
.use(linkPlugin, {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
...options.externalLinks
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3rd party plugins
|
||||||
|
.use(emoji)
|
||||||
|
.use(
|
||||||
|
anchor,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
slugify,
|
||||||
|
permalink: true,
|
||||||
|
permalinkBefore: true,
|
||||||
|
permalinkSymbol: '#'
|
||||||
|
},
|
||||||
|
options.anchor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.use(
|
||||||
|
toc,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
slugify,
|
||||||
|
includeLevel: [2, 3],
|
||||||
|
format: parseHeaders
|
||||||
|
},
|
||||||
|
options.toc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// apply user config
|
||||||
|
if (options.config) {
|
||||||
|
options.config(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.lineNumbers) {
|
||||||
|
md.use(lineNumberPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataReturnable(md)
|
||||||
|
|
||||||
|
return md as any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataReturnable = (md: MarkdownIt) => {
|
||||||
|
// override render to allow custom plugins return data
|
||||||
|
const render = md.render
|
||||||
|
const wrappedRender: MarkdownRenderer['render'] = (src) => {
|
||||||
|
(md as any).__data = {}
|
||||||
|
const html = render.call(md, src)
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
data: (md as any).__data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(md as any).render = wrappedRender
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
// markdown-it plugin for generating line numbers.
|
||||||
|
// It depends on preWrapper plugin.
|
||||||
|
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
export const lineNumberPlugin = (md: MarkdownIt) => {
|
||||||
|
const fence = md.renderer.rules.fence!
|
||||||
|
md.renderer.rules.fence = (...args) => {
|
||||||
|
const rawCode = fence(...args)
|
||||||
|
const code = rawCode.slice(
|
||||||
|
rawCode.indexOf('<code>'),
|
||||||
|
rawCode.indexOf('</code>')
|
||||||
|
)
|
||||||
|
|
||||||
|
const lines = code.split('\n')
|
||||||
|
const lineNumbersCode = [...Array(lines.length - 1)]
|
||||||
|
.map((line, index) => `<span class="line-number">${index + 1}</span><br>`)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const lineNumbersWrapperCode = `<div class="line-numbers-wrapper">${lineNumbersCode}</div>`
|
||||||
|
|
||||||
|
const finalCode = rawCode
|
||||||
|
.replace('<!--beforeend-->', `${lineNumbersWrapperCode}<!--beforeend-->`)
|
||||||
|
.replace('extra-class', 'line-numbers-mode')
|
||||||
|
|
||||||
|
return finalCode
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
// markdown-it plugin for:
|
||||||
|
// 1. adding target="_blank" to external links
|
||||||
|
// 2. normalize internal links to end with `.html`
|
||||||
|
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
const indexRE = /(^|.*\/)(index|readme).md(#?.*)$/i
|
||||||
|
|
||||||
|
export const linkPlugin = (
|
||||||
|
md: MarkdownIt,
|
||||||
|
externalAttrs: Record<string, string>
|
||||||
|
) => {
|
||||||
|
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||||
|
const token = tokens[idx]
|
||||||
|
const hrefIndex = token.attrIndex('href')
|
||||||
|
if (hrefIndex >= 0) {
|
||||||
|
const hrefAttr = token.attrs![hrefIndex]
|
||||||
|
const url = hrefAttr[1]
|
||||||
|
const isExternal = /^https?:/.test(url)
|
||||||
|
const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(url)
|
||||||
|
if (isExternal) {
|
||||||
|
Object.entries(externalAttrs).forEach(([key, val]) => {
|
||||||
|
token.attrSet(key, val)
|
||||||
|
})
|
||||||
|
} else if (isSourceLink) {
|
||||||
|
normalizeHref(hrefAttr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.renderToken(tokens, idx, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHref(hrefAttr: [string, string]) {
|
||||||
|
const data = (md as any).__data
|
||||||
|
let url = hrefAttr[1]
|
||||||
|
|
||||||
|
// convert link to filename and export it for existence check
|
||||||
|
const links = data.links || (data.links = [])
|
||||||
|
links.push(url)
|
||||||
|
|
||||||
|
const indexMatch = url.match(indexRE)
|
||||||
|
if (indexMatch) {
|
||||||
|
const [, path, , hash] = indexMatch
|
||||||
|
url = path + hash
|
||||||
|
} else {
|
||||||
|
url = url.replace(/\.md$/, '.html').replace(/\.md(#.*)$/, '.html$1')
|
||||||
|
}
|
||||||
|
|
||||||
|
// relative path usage.
|
||||||
|
if (!url.startsWith('/')) {
|
||||||
|
url = ensureBeginningDotSlash(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// markdown-it encodes the uri
|
||||||
|
hrefAttr[1] = decodeURI(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beginningSlashRE = /^\.\//
|
||||||
|
const ensureBeginningDotSlash = (path: string) => {
|
||||||
|
if (beginningSlashRE.test(path)) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return './' + path
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// markdown-it plugin for wrapping <pre> ... </pre>.
|
||||||
|
//
|
||||||
|
// If your plugin was chained before preWrapper, you can add additional eleemnt directly.
|
||||||
|
// If your plugin was chained after preWrapper, you can use these slots:
|
||||||
|
// 1. <!--beforebegin-->
|
||||||
|
// 2. <!--afterbegin-->
|
||||||
|
// 3. <!--beforeend-->
|
||||||
|
// 4. <!--afterend-->
|
||||||
|
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
|
||||||
|
export const preWrapperPlugin = (md: MarkdownIt) => {
|
||||||
|
const fence = md.renderer.rules.fence!
|
||||||
|
md.renderer.rules.fence = (...args) => {
|
||||||
|
const [tokens, idx] = args
|
||||||
|
const token = tokens[idx]
|
||||||
|
const rawCode = fence(...args)
|
||||||
|
return (
|
||||||
|
`<!--beforebegin--><div class="language-${token.info.trim()} extra-class">` +
|
||||||
|
`<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
// string.js slugify drops non ascii chars so we have to
|
||||||
|
// use a custom implementation here
|
||||||
|
const removeDiacritics = require('diacritics').remove
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const rControl = /[\u0000-\u001f]/g
|
||||||
|
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
|
||||||
|
|
||||||
|
export const slugify = (str: string): string => {
|
||||||
|
return removeDiacritics(str)
|
||||||
|
// Remove control characters
|
||||||
|
.replace(rControl, '')
|
||||||
|
// Replace special characters
|
||||||
|
.replace(rSpecial, '-')
|
||||||
|
// Remove continous separators
|
||||||
|
.replace(/\-{2,}/g, '-')
|
||||||
|
// Remove prefixing and trailing separtors
|
||||||
|
.replace(/^\-+|\-+$/g, '')
|
||||||
|
// ensure it doesn't start with a number (#121)
|
||||||
|
.replace(/^(\d)/, '_$1')
|
||||||
|
// lowercase
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import { RuleBlock } from 'markdown-it/lib/parser_block'
|
||||||
|
|
||||||
|
export const snippetPlugin = (md: MarkdownIt, root: string) => {
|
||||||
|
const parser: RuleBlock = (state, startLine, endLine, silent) => {
|
||||||
|
const CH = '<'.charCodeAt(0)
|
||||||
|
const pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||||
|
const max = state.eMarks[startLine]
|
||||||
|
|
||||||
|
// if it's indented more than 3 spaces, it should be a code block
|
||||||
|
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; ++i) {
|
||||||
|
const ch = state.src.charCodeAt(pos + i)
|
||||||
|
if (ch !== CH || pos + i >= max) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = pos + 3
|
||||||
|
const end = state.skipSpacesBack(max, pos)
|
||||||
|
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
|
||||||
|
const filename = rawPath.split(/{/).shift()!.trim()
|
||||||
|
const content = fs.existsSync(filename)
|
||||||
|
? fs.readFileSync(filename).toString()
|
||||||
|
: 'Not found: ' + filename
|
||||||
|
const meta = rawPath.replace(filename, '')
|
||||||
|
|
||||||
|
state.line = startLine + 1
|
||||||
|
|
||||||
|
const token = state.push('fence', 'code', 0)
|
||||||
|
token.info = filename.split('.').pop() + meta
|
||||||
|
token.content = content
|
||||||
|
token.markup = '```'
|
||||||
|
token.map = [startLine, startLine + 1]
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
md.block.ruler.before('fence', 'snippet', parser)
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { createMarkdownRenderer, MarkdownOpitons } from './markdown/index'
|
||||||
|
import LRUCache from 'lru-cache'
|
||||||
|
|
||||||
|
const debug = require('debug')('vitepress:md')
|
||||||
|
const cache = new LRUCache<string, string>({ max: 1024 })
|
||||||
|
|
||||||
|
const matter = require('gray-matter')
|
||||||
|
|
||||||
|
export function createMarkdownFn(root: string, options: MarkdownOpitons = {}) {
|
||||||
|
const md = createMarkdownRenderer(options)
|
||||||
|
return (src: string, file: string) => {
|
||||||
|
file = path.relative(root, file)
|
||||||
|
const cached = cache.get(src)
|
||||||
|
if (cached) {
|
||||||
|
debug(`[cache hit] ${file}`)
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
const start = Date.now()
|
||||||
|
|
||||||
|
const { content, data } = matter(src)
|
||||||
|
const { html } = md.render(content)
|
||||||
|
|
||||||
|
// TODO make use of data
|
||||||
|
|
||||||
|
const vueSrc = `<template>${html}</template>`
|
||||||
|
debug(`[render] ${file} in ${Date.now() - start}ms.`, data)
|
||||||
|
cache.set(src, vueSrc)
|
||||||
|
return vueSrc
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { Resolver } from "vite"
|
||||||
|
|
||||||
|
// built ts files are placed into /dist
|
||||||
|
export const APP_PATH = path.join(__dirname, '../lib/app')
|
||||||
|
// TODO detect user configured theme
|
||||||
|
export const THEME_PATH = path.join(__dirname, '../lib/theme-default')
|
||||||
|
|
||||||
|
export const VitePressResolver: Resolver = {
|
||||||
|
publicToFile(publicPath) {
|
||||||
|
if (publicPath.startsWith('/@app')) {
|
||||||
|
return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, ''))
|
||||||
|
}
|
||||||
|
if (publicPath.startsWith('/@theme')) {
|
||||||
|
return path.join(THEME_PATH, publicPath.replace(/^\/@theme\/?/, ''))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fileToPublic(filePath) {
|
||||||
|
if (filePath.startsWith(APP_PATH)) {
|
||||||
|
return `/@app/${path.relative(APP_PATH, filePath)}`
|
||||||
|
}
|
||||||
|
if (filePath.startsWith(THEME_PATH)) {
|
||||||
|
return `/@theme/${path.relative(THEME_PATH, filePath)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { createServer as createViteServer, cachedRead, Plugin } from 'vite'
|
||||||
|
import { createMarkdownFn } from './markdownToVue'
|
||||||
|
import { VitePressResolver, THEME_PATH, APP_PATH } from './resolver'
|
||||||
|
|
||||||
|
const debug = require('debug')('vitepress')
|
||||||
|
|
||||||
|
const VitePressPlugin: Plugin = ({ app, root, watcher, resolver }) => {
|
||||||
|
const markdownToVue = createMarkdownFn(root)
|
||||||
|
|
||||||
|
// watch theme files if it's outside of project root
|
||||||
|
if (path.relative(root, THEME_PATH).startsWith('..')) {
|
||||||
|
debug(`watching theme dir outside of project root: ${THEME_PATH}`)
|
||||||
|
watcher.add(THEME_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hot reload .md files as .vue files
|
||||||
|
watcher.on('change', async (file) => {
|
||||||
|
if (file.endsWith('.md')) {
|
||||||
|
const content = await cachedRead(null, file)
|
||||||
|
watcher.handleVueReload(file, Date.now(), markdownToVue(content, file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
if (ctx.path.endsWith('.md')) {
|
||||||
|
const file = resolver.publicToFile(ctx.path)
|
||||||
|
await cachedRead(ctx, file)
|
||||||
|
// let vite know this is supposed to be treated as vue file
|
||||||
|
ctx.vue = true
|
||||||
|
ctx.body = markdownToVue(ctx.body, file)
|
||||||
|
debug(`serving ${ctx.url}`)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect and serve vitepress files
|
||||||
|
const file = VitePressResolver.publicToFile(ctx.path, root)
|
||||||
|
if (file) {
|
||||||
|
ctx.type = path.extname(file)
|
||||||
|
await cachedRead(ctx, file)
|
||||||
|
|
||||||
|
debug(`serving file: ${ctx.url}`)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
await next()
|
||||||
|
|
||||||
|
// serve our index.html after vite history fallback
|
||||||
|
if (ctx.url === '/index.html') {
|
||||||
|
await cachedRead(ctx, path.join(APP_PATH, 'index-dev.html'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createServer() {
|
||||||
|
return createViteServer({
|
||||||
|
plugins: [VitePressPlugin],
|
||||||
|
resolvers: [VitePressResolver]
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
// Since VuePress needs to extract the header from the markdown source
|
||||||
|
// file and display it in the sidebar or title (#238), this file simply
|
||||||
|
// removes some unnecessary elements to make header displays well at
|
||||||
|
// sidebar or title.
|
||||||
|
//
|
||||||
|
// But header's parsing in the markdown content is done by the markdown
|
||||||
|
// loader based on markdown-it. markdown-it parser will will always keep
|
||||||
|
// HTML in headers, so in VuePress, after being parsed by the markdiwn
|
||||||
|
// loader, the raw HTML in headers will finally be parsed by Vue-loader.
|
||||||
|
// so that we can write HTML/Vue in the header. One exception is the HTML
|
||||||
|
// wrapped by <code>(markdown token: '`') tag.
|
||||||
|
|
||||||
|
const parseEmojis = (str: string) => {
|
||||||
|
const emojiData = require('markdown-it-emoji/lib/data/full.json')
|
||||||
|
return String(str).replace(/:(.+?):/g, (placeholder, key) => emojiData[key] || placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
const unescapeHtml = (html: string) => String(html)
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, '\'')
|
||||||
|
.replace(/:/g, ':')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
|
const removeMarkdownTokens = (str: string) => String(str)
|
||||||
|
.replace(/\[(.*)\]\(.*\)/, '$1') // []()
|
||||||
|
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
|
||||||
|
.replace(/(\\)(\*|_|`|\!)/g, '$2') // remove escape char '\'
|
||||||
|
|
||||||
|
const trim = (str: string) => str.trim()
|
||||||
|
|
||||||
|
// This method remove the raw HTML but reserve the HTML wrapped by `<code>`.
|
||||||
|
// e.g.
|
||||||
|
// Input: "<a> b", Output: "b"
|
||||||
|
// Input: "`<a>` b", Output: "`<a>` b"
|
||||||
|
export const removeNonCodeWrappedHTML = (str: string) => {
|
||||||
|
return String(str).replace(/(^|[^><`])<.*>([^><`]|$)/g, '$1$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
const compose = (...processors: ((str: string) => string)[]) => {
|
||||||
|
if (processors.length === 0) return (input: string) => input
|
||||||
|
if (processors.length === 1) return processors[0]
|
||||||
|
return processors.reduce((prev, next) => {
|
||||||
|
return (str) => next(prev(str))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unescape html, parse emojis and remove some md tokens.
|
||||||
|
export const parseHeaders = compose(
|
||||||
|
unescapeHtml,
|
||||||
|
parseEmojis,
|
||||||
|
removeMarkdownTokens,
|
||||||
|
trim
|
||||||
|
)
|
||||||
|
|
||||||
|
// Also clean the html that isn't wrapped by code.
|
||||||
|
// Because we want to support using VUE components in headers.
|
||||||
|
// e.g. https://vuepress.vuejs.org/guide/using-vue.html#badge
|
||||||
|
export const deeplyParseHeaders = compose(
|
||||||
|
removeNonCodeWrappedHTML,
|
||||||
|
parseHeaders
|
||||||
|
)
|
Loading…
Reference in new issue