parent
64f7cbc423
commit
23697ff192
@ -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