diff --git a/.eslintrc b/.eslintrc index 58295097d..83f8ac1e6 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,11 +7,12 @@ "jest": true, "es6": true }, + "parser": "@typescript-eslint/parser", "parserOptions": { "parser": "babel-eslint" }, "extends": ["plugin:vue/vue3-recommended", "prettier"], - "plugins": ["markdown", "jest"], + "plugins": ["markdown", "jest", "@typescript-eslint", "eslint-plugin-no-explicit-type-exports"], "overrides": [ { "files": ["**/demo/*.md"], @@ -27,7 +28,11 @@ "@vue/prettier", "@vue/prettier/@typescript-eslint" ], + "parserOptions": { + "project": "./tsconfig.json" + }, "rules": { + "no-explicit-type-exports/no-explicit-type-exports": 2, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/ban-types": 0, "@typescript-eslint/consistent-type-imports": 1, diff --git a/examples/index.js b/examples/index.ts similarity index 84% rename from examples/index.js rename to examples/index.ts index 0273e0218..c469dbf27 100644 --- a/examples/index.js +++ b/examples/index.ts @@ -1,9 +1,8 @@ -import '@babel/polyfill'; -import 'ant-design-vue/style'; +import '../components/style'; import { createApp, version } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import App from './App.vue'; -import antd from 'ant-design-vue/index.ts'; +import antd from '../components'; // eslint-disable-next-line no-console console.log('Vue version: ', version); @@ -13,7 +12,6 @@ const basic = (_, { slots }) => { const router = createRouter({ history: createWebHistory(), - fallback: false, routes: [], }); const app = createApp(App); diff --git a/examples/typings.d.ts b/examples/typings.d.ts new file mode 100644 index 000000000..7852412a5 --- /dev/null +++ b/examples/typings.d.ts @@ -0,0 +1,8 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + // eslint-disable-next-line @typescript-eslint/ban-types + const component: DefineComponent & { readonly pageDate?: PageData }; + export default component; +} + +declare module '*.svg'; diff --git a/index.html b/index.html new file mode 100644 index 000000000..bbd8c2c00 --- /dev/null +++ b/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + Ant Design Vue + + + + + + + +
+ + diff --git a/package.json b/package.json index 9d98ddb88..2873b3133 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "vetur" ], "scripts": { - "dev": "webpack serve", + "dev": "vite", "test": "cross-env NODE_ENV=test WORKFLOW=true jest --config .jest.js", "test:dev": "cross-env NODE_ENV=test jest --config .jest.js", "compile": "node antd-tools/cli/run.js compile", @@ -82,11 +82,21 @@ "@commitlint/cli": "^12.0.0", "@commitlint/config-conventional": "^12.0.0", "@octokit/rest": "^18.0.0", + "@rollup/plugin-babel": "^5.3.0", + "@types/compression": "^1.7.0", "@types/fs-extra": "^9.0.8", + "@types/jest": "^26.0.15", + "@types/koa": "^2.11.6", "@types/lodash-es": "^4.17.3", + "@types/lru-cache": "^5.1.0", + "@types/markdown-it": "^10.0.2", + "@types/node": "^13.13.4", + "@types/postcss-load-config": "^2.0.1", "@types/raf": "^3.4.0", "@typescript-eslint/eslint-plugin": "^4.1.0", "@typescript-eslint/parser": "^4.1.0", + "@vitejs/plugin-vue": "^1.2.4", + "@vitejs/plugin-vue-jsx": "^1.1.6", "@vue/babel-plugin-jsx": "^1.0.0", "@vue/cli-plugin-eslint": "^5.0.0-0", "@vue/compiler-sfc": "^3.1.0", @@ -104,7 +114,7 @@ "babel-plugin-inline-import-data-uri": "^1.0.1", "babel-plugin-istanbul": "^6.0.0", "case-sensitive-paths-webpack-plugin": "^2.1.2", - "chalk": "^4.1.0", + "chalk": "^4.1.1", "cheerio": "^1.0.0-rc.2", "codecov": "^3.0.0", "colorful": "^2.1.0", @@ -114,18 +124,22 @@ "css-loader": "^5.0.0", "css-minimizer-webpack-plugin": "^3.0.0", "deep-assign": "^3.0.0", + "diacritics": "^1.3.0", "docsearch.js": "^2.6.3", "enquire-js": "^0.2.1", + "escape-html": "^1.0.3", "eslint": "^7.25.0", "eslint-config-prettier": "^8.0.0", "eslint-plugin-html": "^6.0.0", "eslint-plugin-jest": "^24.3.6", "eslint-plugin-markdown": "^2.0.0-alpha.0", + "eslint-plugin-no-explicit-type-exports": "^0.11.10", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-vue": "^7.1.0", "fetch-jsonp": "^1.1.3", "fs-extra": "^10.0.0", "glob": "^7.1.2", + "gray-matter": "^4.0.3", "gulp": "^4.0.1", "gulp-babel": "^8.0.0", "gulp-strip-code": "^0.1.4", @@ -145,6 +159,11 @@ "less-plugin-npm-import": "^2.1.0", "less-vars-to-js": "^1.3.0", "lint-staged": "^11.0.0", + "markdown-it": "^8.4.2", + "markdown-it-anchor": "^8.0.4", + "markdown-it-container": "^3.0.0", + "markdown-it-emoji": "^2.0.0", + "markdown-it-table-of-contents": "^0.5.2", "marked": "0.3.18", "merge2": "^1.2.1", "mini-css-extract-plugin": "^1.5.0", @@ -156,7 +175,7 @@ "postcss-loader": "^6.0.0", "prettier": "^2.2.0", "pretty-quick": "^3.0.0", - "prismjs": "^1.20.0", + "prismjs": "^1.23.0", "querystring": "^0.2.0", "raw-loader": "^4.0.2", "reqwest": "^2.0.5", @@ -165,6 +184,7 @@ "rucksack-css": "^1.0.2", "selenium-server": "^3.0.1", "semver": "^7.0.0", + "slash": "^2.0.0", "style-loader": "^3.0.0", "stylelint": "^13.0.0", "stylelint-config-prettier": "^8.0.0", @@ -180,6 +200,7 @@ "umi-mock-middleware": "^1.0.0", "umi-request": "^1.3.5", "url-loader": "^3.0.0", + "vite": "^2.3.8", "vue": "^3.1.0", "vue-antd-md-loader": "^1.2.1-beta.1", "vue-clipboard2": "0.3.1", diff --git a/plugin/docs/index.ts b/plugin/docs/index.ts new file mode 100644 index 000000000..48914c7d4 --- /dev/null +++ b/plugin/docs/index.ts @@ -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 }; + } + }, + }; +}; diff --git a/plugin/docs/vueToMarkdown.ts b/plugin/docs/vueToMarkdown.ts new file mode 100644 index 000000000..774b970f8 --- /dev/null +++ b/plugin/docs/vueToMarkdown.ts @@ -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({ 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; + }; +} diff --git a/plugin/md/index.ts b/plugin/md/index.ts new file mode 100644 index 000000000..94db87648 --- /dev/null +++ b/plugin/md/index.ts @@ -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 }; + } + }, + }; +}; diff --git a/plugin/md/markdown/markdown.ts b/plugin/md/markdown/markdown.ts new file mode 100644 index 000000000..f45787e64 --- /dev/null +++ b/plugin/md/markdown/markdown.ts @@ -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; +} + +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; +}; diff --git a/plugin/md/markdown/plugins/component.ts b/plugin/md/markdown/plugins/component.ts new file mode 100644 index 000000000..646b12717 --- /dev/null +++ b/plugin/md/markdown/plugins/component.ts @@ -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], + [/^/, true], + [/^/, true], + // PascalCase Components + [/^<[A-Z]/, />/, true], + // custom elements with hyphens + [/^<\w+\-/, />/, true], + [new RegExp('^|$))', '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; +}; diff --git a/plugin/md/markdown/plugins/containers.ts b/plugin/md/markdown/plugins/containers.ts new file mode 100644 index 000000000..9b28a5f7b --- /dev/null +++ b/plugin/md/markdown/plugins/containers.ts @@ -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 ? `
\n` : `
\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 `

${ + info || defaultTitle + }

\n`; + } else { + return `
\n`; + } + }, + }, + ]; +} diff --git a/plugin/md/markdown/plugins/header.ts b/plugin/md/markdown/plugins/header.ts new file mode 100644 index 000000000..f120f072d --- /dev/null +++ b/plugin/md/markdown/plugins/header.ts @@ -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); + }; +}; diff --git a/plugin/md/markdown/plugins/highlight.ts b/plugin/md/markdown/plugins/highlight.ts new file mode 100644 index 000000000..f4c6bc312 --- /dev/null +++ b/plugin/md/markdown/plugins/highlight.ts @@ -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 `
${code}
`; +} + +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'); +}; diff --git a/plugin/md/markdown/plugins/highlightLines.ts b/plugin/md/markdown/plugins/highlightLines.ts new file mode 100644 index 000000000..d72a55b72 --- /dev/null +++ b/plugin/md/markdown/plugins/highlightLines.ts @@ -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 = /^
/;
+
+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 `
 
`; + } + return '
'; + }) + .join(''); + + const highlightLinesWrapperCode = `
${highlightLinesCode}
`; + + return highlightLinesWrapperCode + code; + }; +}; diff --git a/plugin/md/markdown/plugins/hoist.ts b/plugin/md/markdown/plugins/hoist.ts new file mode 100644 index 000000000..da3b958d5 --- /dev/null +++ b/plugin/md/markdown/plugins/hoist.ts @@ -0,0 +1,20 @@ +import type MarkdownIt from 'markdown-it'; +import type { MarkdownParsedData } from '../markdown'; + +// hoist +${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.meta') + .replace(/process\.env/g, 'process.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 + ? `` + : ''; + const jsSourceCode = ` +${template} +${jsCode} +${style} + `.trim(); + let { html: jsVersion } = md.render(`\`\`\`vue +${jsSourceCode} +\`\`\``); + jsVersion = jsVersion + .replace(/import\.meta/g, 'import.meta') + .replace(/process\.env/g, 'process.env'); + + const jsfiddle = escapeHtml( + JSON.stringify({ + us, + cn, + docHtml: pageData.html.split('
')[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 = `
+