tangjinzhou
3 years ago
27 changed files with 1146 additions and 8 deletions
@ -0,0 +1,8 @@
|
||||
declare module '*.vue' { |
||||
import type { DefineComponent } from 'vue'; |
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const component: DefineComponent<any, any, any> & { readonly pageDate?: PageData }; |
||||
export default component; |
||||
} |
||||
|
||||
declare module '*.svg'; |
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8" /> |
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> |
||||
<meta http-equiv="Pragma" content="no-cache" /> |
||||
<meta http-equiv="Expires" content="0" /> |
||||
<meta |
||||
name="description" |
||||
content="An enterprise-class UI components based on Ant Design and Vue" |
||||
/> |
||||
<title>Ant Design Vue</title> |
||||
<meta |
||||
name="keywords" |
||||
content="ant design vue,ant-design-vue,ant-design-vue admin,ant design pro,vue ant design,vue ant design pro,vue ant design admin,ant design vue官网,ant design vue中文文档,ant design vue文档" |
||||
/> |
||||
<link rel="shortcut icon" type="image/x-icon" href="https://qn.antdv.com/favicon.ico" /> |
||||
<style id="nprogress-style"> |
||||
#nprogress { |
||||
display: none; |
||||
} |
||||
</style> |
||||
<script type="module" src="/examples/index.ts"></script> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="app" style="padding: 50px"></div> |
||||
</body> |
||||
</html> |
@ -0,0 +1,24 @@
|
||||
import { createVueToMarkdownRenderFn } from './vueToMarkdown'; |
||||
import type { MarkdownOptions } from '../md/markdown/markdown'; |
||||
import type { Plugin } from 'vite'; |
||||
import { createMarkdownToVueRenderFn } from '../md/markdownToVue'; |
||||
|
||||
interface Options { |
||||
root?: string; |
||||
markdown?: MarkdownOptions; |
||||
} |
||||
|
||||
export default (options: Options = {}): Plugin => { |
||||
const { root, markdown } = options; |
||||
const vueToMarkdown = createVueToMarkdownRenderFn(root); |
||||
const markdownToVue = createMarkdownToVueRenderFn(root, markdown); |
||||
return { |
||||
name: 'vueToMdToVue', |
||||
transform(code, id) { |
||||
if (id.endsWith('.vue') && id.indexOf('/demo/') > -1 && id.indexOf('index.vue') === -1) { |
||||
// transform .md files into vueSrc so plugin-vue can handle it
|
||||
return { code: markdownToVue(vueToMarkdown(code, id).vueSrc, id).vueSrc, map: null }; |
||||
} |
||||
}, |
||||
}; |
||||
}; |
@ -0,0 +1,43 @@
|
||||
import path from 'path'; |
||||
import LRUCache from 'lru-cache'; |
||||
import slash from 'slash'; |
||||
import fetchCode from '../md/utils/fetchCode'; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const debug = require('debug')('vitepress:md'); |
||||
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }); |
||||
|
||||
interface MarkdownCompileResult { |
||||
vueSrc: string; |
||||
} |
||||
|
||||
export function createVueToMarkdownRenderFn(root: string = process.cwd()): any { |
||||
return (src: string, file: string): MarkdownCompileResult => { |
||||
const relativePath = slash(path.relative(root, file)); |
||||
|
||||
const cached = cache.get(src); |
||||
if (cached) { |
||||
debug(`[cache hit] ${relativePath}`); |
||||
return cached; |
||||
} |
||||
|
||||
const start = Date.now(); |
||||
const docs = fetchCode(src, 'docs')?.trim(); |
||||
const template = fetchCode(src, 'template'); |
||||
const script = fetchCode(src, 'script'); |
||||
const style = fetchCode(src, 'style'); |
||||
const newContent = `${docs} |
||||
\`\`\`vue
|
||||
${template} |
||||
${script} |
||||
${style} |
||||
\`\`\` |
||||
`;
|
||||
debug(`[render] ${file} in ${Date.now() - start}ms.`); |
||||
const result = { |
||||
vueSrc: newContent?.trim(), |
||||
}; |
||||
cache.set(src, result); |
||||
return result; |
||||
}; |
||||
} |
@ -0,0 +1,22 @@
|
||||
import { createMarkdownToVueRenderFn } from './markdownToVue'; |
||||
import type { MarkdownOptions } from './markdown/markdown'; |
||||
import type { Plugin } from 'vite'; |
||||
|
||||
interface Options { |
||||
root?: string; |
||||
markdown?: MarkdownOptions; |
||||
} |
||||
|
||||
export default (options: Options = {}): Plugin => { |
||||
const { root, markdown } = options; |
||||
const markdownToVue = createMarkdownToVueRenderFn(root, markdown); |
||||
return { |
||||
name: 'mdToVue', |
||||
transform(code, id) { |
||||
if (id.endsWith('.md')) { |
||||
// transform .md files into vueSrc so plugin-vue can handle it
|
||||
return { code: markdownToVue(code, id).vueSrc, map: null }; |
||||
} |
||||
}, |
||||
}; |
||||
}; |
@ -0,0 +1,107 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */ |
||||
import MarkdownIt from 'markdown-it'; |
||||
import { parseHeader } from '../utils/parseHeader'; |
||||
import { highlight } from './plugins/highlight'; |
||||
import { slugify } from './plugins/slugify'; |
||||
import { highlightLinePlugin } from './plugins/highlightLines'; |
||||
import { lineNumberPlugin } from './plugins/lineNumbers'; |
||||
import { componentPlugin } from './plugins/component'; |
||||
import { containerPlugin } from './plugins/containers'; |
||||
import { snippetPlugin } from './plugins/snippet'; |
||||
import { hoistPlugin } from './plugins/hoist'; |
||||
import { preWrapperPlugin } from './plugins/preWrapper'; |
||||
import { linkPlugin } from './plugins/link'; |
||||
import { extractHeaderPlugin } from './plugins/header'; |
||||
import type { Header } from '../../shared'; |
||||
|
||||
const emoji = require('markdown-it-emoji'); |
||||
const anchor = require('markdown-it-anchor'); |
||||
const toc = require('markdown-it-table-of-contents'); |
||||
|
||||
export interface MarkdownOptions 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 MarkdownParsedData { |
||||
hoistedTags?: string[]; |
||||
links?: string[]; |
||||
headers?: Header[]; |
||||
vueCode?: string; |
||||
} |
||||
|
||||
export interface MarkdownRenderer { |
||||
__data: MarkdownParsedData; |
||||
render: (src: string, env?: any) => { html: string; data: any }; |
||||
} |
||||
|
||||
export const createMarkdownRenderer = (options: MarkdownOptions = {}): MarkdownRenderer => { |
||||
const md = MarkdownIt({ |
||||
html: true, |
||||
linkify: true, |
||||
highlight, |
||||
...options, |
||||
}); |
||||
|
||||
// custom plugins
|
||||
md.use(componentPlugin) |
||||
.use(highlightLinePlugin) |
||||
.use(preWrapperPlugin) |
||||
.use(snippetPlugin) |
||||
.use(hoistPlugin) |
||||
.use(containerPlugin) |
||||
.use(extractHeaderPlugin) |
||||
.use(linkPlugin, { |
||||
target: '_blank', |
||||
rel: 'noopener noreferrer', |
||||
...options.externalLinks, |
||||
}) |
||||
|
||||
// 3rd party plugins
|
||||
.use(emoji) |
||||
.use(anchor, { |
||||
slugify, |
||||
permalink: false, |
||||
permalinkBefore: true, |
||||
permalinkSymbol: '#', |
||||
permalinkAttrs: () => ({ 'aria-hidden': true }), |
||||
...options.anchor, |
||||
}) |
||||
.use(toc, { |
||||
slugify, |
||||
includeLevel: [2, 3], |
||||
format: parseHeader, |
||||
...options.toc, |
||||
}); |
||||
|
||||
// apply user config
|
||||
if (options.config) { |
||||
options.config(md); |
||||
} |
||||
|
||||
if (options.lineNumbers) { |
||||
md.use(lineNumberPlugin); |
||||
} |
||||
|
||||
// wrap render so that we can return both the html and extracted 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; |
||||
|
||||
return md as any; |
||||
}; |
@ -0,0 +1,97 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */ |
||||
import type MarkdownIt from 'markdown-it'; |
||||
import type { 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,44 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */ |
||||
import type MarkdownIt from 'markdown-it'; |
||||
import type 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,25 @@
|
||||
import type MarkdownIt from 'markdown-it'; |
||||
import type { MarkdownParsedData } from '../markdown'; |
||||
import { deeplyParseHeader } from '../../utils/parseHeader'; |
||||
import { slugify } from './slugify'; |
||||
|
||||
export const extractHeaderPlugin = (md: MarkdownIt, include = ['h2', 'h3']) => { |
||||
md.renderer.rules.heading_open = (tokens, i, options, env, self) => { |
||||
const token = tokens[i]; |
||||
if (include.includes(token.tag)) { |
||||
const title = tokens[i + 1].content; |
||||
const idAttr = token.attrs!.find(([name]) => name === 'id'); |
||||
const slug = idAttr && idAttr[1]; |
||||
const content = tokens[i + 4].content; |
||||
const data = (md as any).__data as MarkdownParsedData; |
||||
const headers = data.headers || (data.headers = []); |
||||
headers.push({ |
||||
level: parseInt(token.tag.slice(1), 10), |
||||
title: deeplyParseHeader(title), |
||||
slug: slug || slugify(title), |
||||
content, |
||||
}); |
||||
} |
||||
return self.renderToken(tokens, i, options); |
||||
}; |
||||
}; |
@ -0,0 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */ |
||||
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><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(`[vitepress] 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 type 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,20 @@
|
||||
import type MarkdownIt from 'markdown-it'; |
||||
import type { MarkdownParsedData } from '../markdown'; |
||||
|
||||
// hoist <script> and <style> tags out of the returned html
|
||||
// so that they can be placed outside as SFC blocks.
|
||||
export const hoistPlugin = (md: MarkdownIt) => { |
||||
const RE = /^<(script|style)(?=(\s|>|$))/i; |
||||
|
||||
md.renderer.rules.html_block = (tokens, idx) => { |
||||
const content = tokens[idx].content; |
||||
const data = (md as any).__data as MarkdownParsedData; |
||||
const hoistedTags = data.hoistedTags || (data.hoistedTags = []); |
||||
if (RE.test(content.trim())) { |
||||
hoistedTags.push(content); |
||||
return ''; |
||||
} else { |
||||
return content; |
||||
} |
||||
}; |
||||
}; |
@ -0,0 +1,25 @@
|
||||
// markdown-it plugin for generating line numbers.
|
||||
// It depends on preWrapper plugin.
|
||||
|
||||
import type 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(/<\/div>$/, `${lineNumbersWrapperCode}</div>`) |
||||
.replace(/"(language-\w+)"/, '"$1 line-numbers-mode"'); |
||||
|
||||
return finalCode; |
||||
}; |
||||
}; |
@ -0,0 +1,69 @@
|
||||
// markdown-it plugin for:
|
||||
// 1. adding target="_blank" to external links
|
||||
// 2. normalize internal links to end with `.html`
|
||||
|
||||
import type MarkdownIt from 'markdown-it'; |
||||
import type { MarkdownParsedData } from '../markdown'; |
||||
import { URL } from 'url'; |
||||
|
||||
const indexRE = /(^|.*\/)index.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); |
||||
if (isExternal) { |
||||
Object.entries(externalAttrs).forEach(([key, val]) => { |
||||
token.attrSet(key, val); |
||||
}); |
||||
} else if ( |
||||
// internal anchor links
|
||||
!url.startsWith('#') && |
||||
// mail links
|
||||
!url.startsWith('mailto:') |
||||
) { |
||||
normalizeHref(hrefAttr); |
||||
} |
||||
} |
||||
return self.renderToken(tokens, idx, options); |
||||
}; |
||||
|
||||
function normalizeHref(hrefAttr: [string, string]) { |
||||
let url = hrefAttr[1]; |
||||
|
||||
const indexMatch = url.match(indexRE); |
||||
if (indexMatch) { |
||||
const [, path, hash] = indexMatch; |
||||
url = path + hash; |
||||
} else { |
||||
let cleanUrl = url.replace(/\#.*$/, '').replace(/\?.*$/, ''); |
||||
// .md -> .html
|
||||
if (cleanUrl.endsWith('.md')) { |
||||
cleanUrl = cleanUrl.replace(/\.md$/, '.html'); |
||||
} |
||||
// ./foo -> ./foo.html
|
||||
if (!cleanUrl.endsWith('.html') && !cleanUrl.endsWith('/')) { |
||||
cleanUrl += '.html'; |
||||
} |
||||
const parsed = new URL(url, 'http://a.com'); |
||||
url = cleanUrl + parsed.search + parsed.hash; |
||||
} |
||||
|
||||
// ensure leading . for relative paths
|
||||
if (!url.startsWith('/') && !/^\.\//.test(url)) { |
||||
url = './' + url; |
||||
} |
||||
|
||||
// export it for existence check
|
||||
const data = (md as any).__data as MarkdownParsedData; |
||||
const links = data.links || (data.links = []); |
||||
links.push(url.replace(/\.html$/, '')); |
||||
|
||||
// markdown-it encodes the uri
|
||||
hrefAttr[1] = decodeURI(url); |
||||
} |
||||
}; |
@ -0,0 +1,25 @@
|
||||
// markdown-it plugin for wrapping <pre> ... </pre>.
|
||||
//
|
||||
// If your plugin was chained before preWrapper, you can add additional element 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'; |
||||
import { MarkdownParsedData } from '../markdown'; |
||||
|
||||
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 data = (md as any).__data as MarkdownParsedData; |
||||
if (token.info.trim() === 'vue') { |
||||
data.vueCode = token.content; |
||||
} |
||||
const rawCode = fence(...args).replace(/<pre /g, `<pre class="language-${token.info.trim()}" `); |
||||
return rawCode; //`<div class="language-${token.info.trim()}">${rawCode}</div>`;
|
||||
}; |
||||
}; |
@ -0,0 +1,24 @@
|
||||
// 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 continuos 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 type MarkdownIt from 'markdown-it'; |
||||
import type { 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,172 @@
|
||||
import fs from 'fs'; |
||||
import path from 'path'; |
||||
import matter from 'gray-matter'; |
||||
import LRUCache from 'lru-cache'; |
||||
import type { MarkdownOptions, MarkdownParsedData, MarkdownRenderer } from './markdown/markdown'; |
||||
import { createMarkdownRenderer } from './markdown/markdown'; |
||||
import { deeplyParseHeader } from './utils/parseHeader'; |
||||
import type { PageData, HeadConfig } from '../shared'; |
||||
import slash from 'slash'; |
||||
import escapeHtml from 'escape-html'; |
||||
import fetchCode from './utils/fetchCode'; |
||||
import tsToJs from './utils/tsToJs'; |
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const debug = require('debug')('vitepress:md'); |
||||
const cache = new LRUCache<string, MarkdownCompileResult>({ max: 1024 }); |
||||
|
||||
interface MarkdownCompileResult { |
||||
vueSrc: string; |
||||
pageData: PageData; |
||||
} |
||||
|
||||
export function createMarkdownToVueRenderFn( |
||||
root: string = process.cwd(), |
||||
options: MarkdownOptions = {}, |
||||
): any { |
||||
const md = createMarkdownRenderer(options); |
||||
|
||||
return (src: string, file: string): MarkdownCompileResult => { |
||||
const relativePath = slash(path.relative(root, file)); |
||||
|
||||
const cached = cache.get(src); |
||||
if (cached) { |
||||
debug(`[cache hit] ${relativePath}`); |
||||
return cached; |
||||
} |
||||
|
||||
const start = Date.now(); |
||||
|
||||
const { content, data: frontmatter } = matter(src); |
||||
// eslint-disable-next-line prefer-const
|
||||
let { html, data } = md.render(content); |
||||
// avoid env variables being replaced by vite
|
||||
html = html |
||||
.replace(/import\.meta/g, 'import.<wbr/>meta') |
||||
.replace(/process\.env/g, 'process.<wbr/>env'); |
||||
// TODO validate data.links?
|
||||
const pageData: PageData = { |
||||
title: inferTitle(frontmatter, content), |
||||
description: inferDescription(frontmatter), |
||||
frontmatter, |
||||
headers: data.headers, |
||||
relativePath, |
||||
content: escapeHtml(content), |
||||
html, |
||||
// TODO use git timestamp?
|
||||
lastUpdated: Math.round(fs.statSync(file).mtimeMs), |
||||
}; |
||||
const newContent = data.vueCode |
||||
? genComponentCode(md, data, pageData) |
||||
: ` |
||||
<template><article class="markdown">${html}</article></template> |
||||
|
||||
<script> |
||||
export default { pageData: ${JSON.stringify(pageData)} } |
||||
</script> |
||||
${fetchCode(content, 'style')} |
||||
`;
|
||||
|
||||
debug(`[render] ${file} in ${Date.now() - start}ms.`); |
||||
const result = { |
||||
vueSrc: newContent?.trim(), |
||||
pageData, |
||||
}; |
||||
cache.set(src, result); |
||||
return result; |
||||
}; |
||||
} |
||||
|
||||
function genComponentCode(md: MarkdownRenderer, data: PageData, pageData: PageData) { |
||||
const { vueCode, headers = [] } = data as MarkdownParsedData; |
||||
const cn = headers.find(h => h.title === 'zh-CN')?.content; |
||||
const us = headers.find(h => h.title === 'en-US')?.content; |
||||
let { html } = md.render(`\`\`\`vue
|
||||
${vueCode?.trim()} |
||||
\`\`\``); |
||||
html = html |
||||
.replace(/import\.meta/g, 'import.<wbr/>meta') |
||||
.replace(/process\.env/g, 'process.<wbr/>env'); |
||||
const template = fetchCode(vueCode, 'template'); |
||||
const script = fetchCode(vueCode, 'script'); |
||||
const style = fetchCode(vueCode, 'style'); |
||||
const scriptContent = fetchCode(vueCode, 'scriptContent'); |
||||
let jsCode = tsToJs(scriptContent)?.trim(); |
||||
jsCode = jsCode |
||||
? `<script>
|
||||
${jsCode} |
||||
</script>` |
||||
: ''; |
||||
const jsSourceCode = ` |
||||
${template} |
||||
${jsCode} |
||||
${style} |
||||
`.trim();
|
||||
let { html: jsVersion } = md.render(`\`\`\`vue
|
||||
${jsSourceCode} |
||||
\`\`\``); |
||||
jsVersion = jsVersion |
||||
.replace(/import\.meta/g, 'import.<wbr/>meta') |
||||
.replace(/process\.env/g, 'process.<wbr/>env'); |
||||
|
||||
const jsfiddle = escapeHtml( |
||||
JSON.stringify({ |
||||
us, |
||||
cn, |
||||
docHtml: pageData.html.split('<pre class="language-vue" v-pre>')[0], |
||||
...pageData.frontmatter, |
||||
relativePath: pageData.relativePath, |
||||
// htmlCode: Buffer.from(html).toString('base64'),
|
||||
// jsVersionHtml: Buffer.from(jsVersion).toString('base64'),
|
||||
sourceCode: Buffer.from(vueCode).toString('base64'), |
||||
jsSourceCode: Buffer.from(jsSourceCode).toString('base64'), |
||||
}), |
||||
); |
||||
|
||||
const newContent = ` |
||||
<template> |
||||
<demo-box :jsfiddle="${jsfiddle}"> |
||||
${template.replace('<template>', '<template v-slot:default>')} |
||||
<template #htmlCode>${html}</template> |
||||
<template #jsVersionHtml>${jsVersion}</template> |
||||
</demo-box> |
||||
</template> |
||||
${script} |
||||
${style} |
||||
`;
|
||||
return newContent; |
||||
} |
||||
|
||||
const inferTitle = (frontmatter: any, content: string) => { |
||||
if (frontmatter.home) { |
||||
return 'Home'; |
||||
} |
||||
if (frontmatter.title) { |
||||
return deeplyParseHeader(frontmatter.title); |
||||
} |
||||
const match = content.match(/^\s*#+\s+(.*)/m); |
||||
if (match) { |
||||
return deeplyParseHeader(match[1].trim()); |
||||
} |
||||
return ''; |
||||
}; |
||||
|
||||
const inferDescription = (frontmatter: Record<string, any>) => { |
||||
if (!frontmatter.head) { |
||||
return ''; |
||||
} |
||||
|
||||
return getHeadMetaContent(frontmatter.head, 'description') || ''; |
||||
}; |
||||
|
||||
const getHeadMetaContent = (head: HeadConfig[], name: string): string | undefined => { |
||||
if (!head || !head.length) { |
||||
return undefined; |
||||
} |
||||
|
||||
const meta = head.find(([tag, attrs = {}]) => { |
||||
return tag === 'meta' && attrs.name === name && attrs.content; |
||||
}); |
||||
|
||||
return meta && meta[1].content; |
||||
}; |
@ -0,0 +1,30 @@
|
||||
// import cheerio from 'cheerio';
|
||||
const scriptRE = /<script[^>]*>([\s\S]*)<\/script>/; |
||||
const scriptContentRE = /(?<=<script[^>]*>)([\s\S]*)(?=<\/script>)/; |
||||
const templateRE = /<template[^>]*>([\s\S]*)<\/template>/; |
||||
const styleRE = /<style[^>]*>([\s\S]*)<\/style>/; |
||||
const docsRE = /(?<=<docs>)([\s\S]*)(?=<\/docs>)/; |
||||
const reObj = { |
||||
script: scriptRE, |
||||
style: styleRE, |
||||
docs: docsRE, |
||||
template: templateRE, |
||||
scriptContent: scriptContentRE, |
||||
}; |
||||
|
||||
export default function fetchCode(src: string, type: string): string { |
||||
if (type === 'template') { |
||||
// const $ = cheerio.load(src, {
|
||||
// decodeEntities: false,
|
||||
// xmlMode: false,
|
||||
// recognizeSelfClosing: true,
|
||||
// _useHtmlParser2: true,
|
||||
// });
|
||||
// return `<template>
|
||||
// ${$(type).html().trim()}
|
||||
// </template>`;
|
||||
src = src.split('<script')[0]; |
||||
} |
||||
const matches = src.match(reObj[type]); |
||||
return matches ? matches[0] : ''; |
||||
} |
@ -0,0 +1,58 @@
|
||||
// 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 markdown
|
||||
// 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) => { |
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
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(/(\[(.[^\]]+)\]\((.[^)]+)\))/g, '$2') // []()
|
||||
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
.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): 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 parseHeader = 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 deeplyParseHeader = compose(removeNonCodeWrappedHTML, parseHeader); |
@ -0,0 +1,27 @@
|
||||
import qs from 'querystring'; |
||||
|
||||
export interface VueQuery { |
||||
vue?: boolean; |
||||
src?: boolean; |
||||
type?: 'script' | 'template' | 'style' | 'custom'; |
||||
index?: number; |
||||
lang?: string; |
||||
} |
||||
|
||||
export function parseVueRequest(id: string): any { |
||||
const [filename, rawQuery] = id.split(`?`, 2); |
||||
const query = qs.parse(rawQuery) as VueQuery; |
||||
if (query.vue != null) { |
||||
query.vue = true; |
||||
} |
||||
if (query.src != null) { |
||||
query.src = true; |
||||
} |
||||
if (query.index != null) { |
||||
query.index = Number(query.index); |
||||
} |
||||
return { |
||||
filename, |
||||
query, |
||||
}; |
||||
} |
@ -0,0 +1,28 @@
|
||||
import { transformSync } from '@babel/core'; |
||||
import { CLIEngine } from 'eslint'; |
||||
const engine = new CLIEngine({ |
||||
fix: true, |
||||
useEslintrc: false, |
||||
}); |
||||
const tsToJs = (content: string): string => { |
||||
if (!content) { |
||||
return ''; |
||||
} |
||||
const { code } = transformSync(content, { |
||||
configFile: false, |
||||
plugins: [ |
||||
[ |
||||
require.resolve('@babel/plugin-transform-typescript'), |
||||
{ |
||||
isTSX: false, |
||||
}, |
||||
], |
||||
], |
||||
}); |
||||
const report = engine.executeOnText(code); |
||||
let output = report.results[0].output; |
||||
output = output ? output.trim() : output; |
||||
return output; |
||||
}; |
||||
|
||||
export default tsToJs; |
@ -0,0 +1,42 @@
|
||||
// types shared between server and client
|
||||
|
||||
export interface LocaleConfig { |
||||
lang: string; |
||||
title?: string; |
||||
description?: string; |
||||
head?: HeadConfig[]; |
||||
label?: string; |
||||
selectText?: string; |
||||
} |
||||
|
||||
export interface SiteData<ThemeConfig = any> { |
||||
base: string; |
||||
lang: string; |
||||
title: string; |
||||
description: string; |
||||
head: HeadConfig[]; |
||||
themeConfig: ThemeConfig; |
||||
locales: Record<string, LocaleConfig>; |
||||
} |
||||
|
||||
export type HeadConfig = |
||||
| [string, Record<string, string>] |
||||
| [string, Record<string, string>, string]; |
||||
|
||||
export interface PageData { |
||||
relativePath: string; |
||||
title: string; |
||||
description: string; |
||||
headers: Header[]; |
||||
frontmatter: Record<string, any>; |
||||
lastUpdated: number; |
||||
content?: string; |
||||
html?: string; |
||||
} |
||||
|
||||
export interface Header { |
||||
level: number; |
||||
title: string; |
||||
slug: string; |
||||
content: string; |
||||
} |
@ -0,0 +1,48 @@
|
||||
import path from 'path'; |
||||
import vue from '@vitejs/plugin-vue'; |
||||
import md from './plugin/md'; |
||||
import docs from './plugin/docs'; |
||||
import vueJsx from '@vitejs/plugin-vue-jsx'; |
||||
|
||||
/** |
||||
* @type {import('vite').UserConfig} |
||||
*/ |
||||
export default { |
||||
resolve: { |
||||
alias: { |
||||
// moment: 'moment/dist/moment.js',
|
||||
vue: 'vue/dist/vue.esm-bundler.js', |
||||
'ant-design-vue': path.resolve(__dirname, './components'), |
||||
}, |
||||
}, |
||||
plugins: [ |
||||
vueJsx({ |
||||
// options are passed on to @vue/babel-plugin-jsx
|
||||
mergeProps: false, |
||||
enableObjectSlots: false, |
||||
}), |
||||
docs(), |
||||
md(), |
||||
vue({ |
||||
include: [/\.vue$/, /\.md$/], |
||||
}), |
||||
], |
||||
optimizeDeps: { |
||||
include: [ |
||||
'fetch-jsonp', |
||||
'@ant-design/icons-vue', |
||||
'lodash-es', |
||||
'vue', |
||||
'vue-router', |
||||
'moment', |
||||
'async-validator', |
||||
], |
||||
}, |
||||
css: { |
||||
preprocessorOptions: { |
||||
less: { |
||||
javascriptEnabled: true, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
Loading…
Reference in new issue