Skip to content

Commit

Permalink
feat: 完善文档功能
Browse files Browse the repository at this point in the history
  • Loading branch information
zhenyulei committed Nov 5, 2020
1 parent a44ba34 commit bc8b79b
Show file tree
Hide file tree
Showing 18 changed files with 603 additions and 250 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/src/packages/**/*.md
19 changes: 0 additions & 19 deletions loader/md-vue/card-wrapper.js

This file was deleted.

15 changes: 0 additions & 15 deletions loader/md-vue/code-wrapper.js

This file was deleted.

40 changes: 40 additions & 0 deletions loader/md-vue/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const Config = require('markdown-it-chain');
const anchorPlugin = require('markdown-it-anchor');
const slugify = require('transliteration').slugify;
const hljs = require('highlight.js');
const containers = require('./containers');
const overWriteFenceRule = require('./fence');

const config = new Config();

const highlight = (str, lang) => {
if (!lang || !hljs.getLanguage(lang)) {
return '<pre><code class="hljs">' + str + '</code></pre>';
}
const html = hljs.highlight(lang, str, true, undefined).value;
return `<pre><code class="hljs language-${lang}">${html}</code></pre>`;
};

config.options
.html(true)
.highlight(highlight)
.end()

// .plugin('anchor').use(anchorPlugin, [
// {
// level: 2,
// slugify: slugify,
// permalink: false,
// permalinkBefore: false,
// },
// ]).end()

.plugin('containers')
.use(containers)
.end();

const md = config.toMd();
overWriteFenceRule(md);

module.exports = md;
25 changes: 25 additions & 0 deletions loader/md-vue/containers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const mdContainer = require('markdown-it-container');

module.exports = md => {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : '';
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--nutui-demo: ${content}:nutui-demo-->
`;
}
return '</demo-block>';
}
});

md.use(mdContainer, 'tip');
md.use(mdContainer, 'warning');
};
14 changes: 14 additions & 0 deletions loader/md-vue/fence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 覆盖默认的 fence 渲染策略
module.exports = md => {
const defaultRender = md.renderer.rules.fence;
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx];
// 判断该 fence 是否在 :::demo 内
const prevToken = tokens[idx - 1];
const isInDemoContainer = prevToken && prevToken.nesting === 1 && prevToken.info.trim().match(/^demo\s*(.*)$/);
if (token.info === 'html' && isInDemoContainer) {
return `<template #highlight><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
}
return defaultRender(tokens, idx, options, env, self);
};
};
9 changes: 0 additions & 9 deletions loader/md-vue/highlight.js

This file was deleted.

124 changes: 56 additions & 68 deletions loader/md-vue/index.js
Original file line number Diff line number Diff line change
@@ -1,80 +1,68 @@
const loaderUtils = require('loader-utils'); //:loader-utils 是一个npm i loader-utils -D 安装的插件,便于获取webpack.config.js 中配置loader的options;
const MarkdownIt = require('markdown-it'); //渲染 markdown 基本语法
const markdownItAnchor = require('markdown-it-anchor'); //为各级标题添加锚点
const frontMatter = require('front-matter'); //就是md文档最上面的内容 类似于docz中的路由/标题的设置部分
const highlight = require('./highlight');
const linkOpen = require('./link-open');
const cardWrapper = require('./card-wrapper');
const codeWrapper = require('./code-wrapper');
/* eslint-disable @typescript-eslint/no-var-requires */
const { stripScript, stripTemplate, genInlineComponentText } = require('./util');
const md = require('./config');

const { slugify } = require('transliteration');

function wrapper(content) {
content = cardWrapper(content);
content = codeWrapper(content);
content = escape(content);

return `
import { h } from 'vue';
module.exports = function(source) {
const content = md.render(source);

const content = unescape(\`${content}\`);
const startTag = '<!--nutui-demo:';
const startTagLen = startTag.length;
const endTag = ':nutui-demo-->';
const endTagLen = endTag.length;

export default {
mounted() {
const anchors = [].slice.call(this.$el.querySelectorAll('h2, h3, h4, h5'));
let componenetsString = '';
let id = 0; // demo 的 id
let output = []; // 输出的内容
let start = 0; // 字符串开始位置

anchors.forEach(anchor => {
anchor.addEventListener('click', this.scrollToAnchor);
});
},
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart));

methods: {
scrollToAnchor(event) {
if (event.target.id) {
this.$router.push({
path: this.$route.path,
hash: '#'+event.target.id
})
}
}
},
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
const html = stripTemplate(commentContent);
const script = stripScript(commentContent);
let demoComponentContent = genInlineComponentText(html, script);
const demoComponentName = `nutui-demo${id}`;
output.push(`<template #source><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;

render() {
return h('section', { innerHTML: content });
// 重新计算下一次的位置
id++;
start = commentEnd + endTagLen;
commentStart = content.indexOf(startTag, start);
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
};
`;
}

const parser = new MarkdownIt({
html: true,
linkify: true,
highlight
}).use(markdownItAnchor, {
level: 2, // 添加超链接锚点的最小标题级别, 如: #标题 不会添加锚点
slugify // 自定义slugify, 我们使用的是将中文转为汉语拼音,最终生成为标题id属性
});

module.exports = function(source) {
let options = loaderUtils.getOptions(this) || {}; // 获取loader的参数
this.cacheable && this.cacheable();

options = {
wrapper,
linkOpen: true,
...options
};

let fm;
// 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
// todo: 优化这段逻辑

if (options.enableMetaData) {
fm = frontMatter(source);
source = fm.body;
}

if (options.linkOpen) {
linkOpen(parser);
let pageScript = '';
if (componenetsString) {
pageScript = `<script lang="ts">
import * as Vue from 'vue';
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`;
} else if (content.indexOf('<script>') === 0) {
// 硬编码,有待改善
start = content.indexOf('</script>') + '</script>'.length;
pageScript = content.slice(0, start);
}

return options.wrapper(parser.render(source), fm);
output.push(content.slice(start));
const result = `
<template>
<section class="content nutui-doc">
${output.join('')}
</section>
</template>
${pageScript}
`;
return result;
};
18 changes: 0 additions & 18 deletions loader/md-vue/link-open.js

This file was deleted.

82 changes: 82 additions & 0 deletions loader/md-vue/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { compileTemplate, TemplateCompiler } = require('@vue/compiler-sfc');

function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
}

function stripStyle(content) {
const result = content.match(/<(style)\s*>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
}

// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
content = content.trim();
if (!content) {
return content;
}
return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}

function pad(source) {
return source
.split(/\r?\n/)
.map(line => ` ${line}`)
.join('\n');
}

const templateReplaceRegex = /<template>([\s\S]+)<\/template>/g;
function genInlineComponentText(template, script) {
// https://github.com/vuejs/vue-loader/blob/423b8341ab368c2117931e909e2da9af74503635/lib/loaders/templateLoader.js#L46
let source = template;
if (templateReplaceRegex.test(source)) {
source = source.replace(templateReplaceRegex, '$1');
}
const finalOptions = {
source: `<div>${source}</div>`,
filename: 'inline-component', // TODO:这里有待调整
compiler: TemplateCompiler,
compilerOptions: {
mode: 'function'
}
};
const compiled = compileTemplate(finalOptions);
// tips
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach(tip => {
console.warn(tip);
});
}
// errors
if (compiled.errors && compiled.errors.length) {
console.error(`\n Error compiling template:\n${pad(compiled.source)}\n` + compiled.errors.map(e => ` - ${e}`).join('\n') + '\n');
}
let demoComponentContent = `
${compiled.code.replace('return function render', 'function render')}
`;
// todo: 这里采用了硬编码有待改进
script = script.trim();
if (script) {
script = script.replace(/export\s+default/, 'const democomponentExport =').replace(/import ({.*}) from 'vue'/g, (s, s1) => `const ${s1} = Vue`);
} else {
script = 'const democomponentExport = {}';
}
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
render,
...democomponentExport
}
})()`;
return demoComponentContent;
}

module.exports = {
stripScript,
stripStyle,
stripTemplate,
genInlineComponentText
};
Loading

0 comments on commit bc8b79b

Please sign in to comment.