饿了么前端介绍的Markdown解析和扩展 地址
把 Markdown 当成 Vue 组件来使用。在文档中,我们还编写了许多示例去描述组件的用法。在编写示例代码时,还有这两个需求:
1、示例代码格与单文件组件保持一致
2、示例代码像组件一样在页面中渲染
原生的markdown并不具备这样的功能,要对进行特殊的定制化
markdown-it 原理
输入一串 markdown 代码,最后得到一串 html 代码,整体流程如下:
markdown代码 --> 解析器 --> token流 --> 渲染器 --> html
解析器最主要的两个block和inline两个规则
block规则 # 我是一个例子 得到 heading_open 、inline、 heading_close 三个 token
inline规则 我是一个例子 我们得到了 3 + 1 个 token: 内联 token 都有一个.children 属性,带有嵌套 token 流
渲染器(Renderer) 生成token流,传递给renderer,会遍历所有token,将每个token.type传递给定义相同名字规则,渲染器规则在md.renderer.rules[name]
md-loader流程图
md-loader配置
module:{
rules:{
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
},
{
loader: path.resolve(__dirname, './../src/mdLoader/index.js') // md文件加载进来后,要做些操作
}
]
},
}
}
创建test.md 并添加到路由
# 测试一个实例
:::demo //自定义的块级容器,显示在页面的例子
```html //编写html代码
<template>
<Button class='testClass' v-if='isShow'>
<span>default</span>
</Button>
</template>
<script>
export default{
data(){
return{
isShow:false
}
}
}
</script>
```
:::
创建一个demo-block组件 在全局引入组件
创建的demo-block,将生成的render函数插到slot的source,显示的代码插到slot的highlight。 可以自定义做效果,现在先简单实现点击显示和隐藏代码
<template>
<div>
<slot></slot>
<slot name="source"></slot>
<Button class="code_button" @click="codeSwitch">显示代码</Button>
<slot name="highlight" v-if="isCode"></slot>
</div>
</template>
<script>
export default {
name: "demoBlock",
data() {
return {
isCode: false
};
},
methods: {
codeSwitch() {
this.isCode = !this.isCode;
}
}
};
</script>
所需要用到的插件:
const Config = require('markdown-it-chain'); //支持markdown-it的链式操作
const anchorPlugin = require('markdown-it-anchor'); // markdown锚点
const mdContainer = require('markdown-it-container'); //此插件识别一块容器,并指定如何返回,扩展性要比自己写md.renderer.rules返回值好用
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
index.js文件
被loader加载的index文件
主要做了将经过规则处理过添加占位符,截取替换占位符的内容把template的内容用@vue/component-compiler-utils插件转换成render函数,和script一起合并成自执行函数的组件,最后返回vue格式代码
const {
stripScript,
stripTemplate,
genInlineComponentText
} = require('./util');
const md = require('./config');
module.exports = function (source) {
//source是test.md传过来的原始数据:# test.md
const content = md.render(source); //经过了containers,fence 规则的处理,添加了<!--ksc--demo demo-block等块级标识
//<!--ksc-demo: ${content} :ksc-demo--> 截取中间的content内容,转换成vue里template,script生成vue dom,做成组件引用
const startTag = '<!--ksc-demo:';
const startTagLen = startTag.length;
const endTag = ':ksc-demo-->';
const endTagLen = endTag.length;
let componenetsString = '';
let id = 0; // demo 的 id
let output = []; // 输出的内容
let start = 0; // 字符串开始位置
let commentStart = content.indexOf(startTag); // 开始查找<!--ksc-demo:的位置
let commentEnd = content.indexOf(endTag, commentStart + startTagLen); //从commentStart+startTag的length位置开始搜索:ksc-demo-->
console.log('commentStart',commentStart)
// commentStart !== -1开始循环, 条件不成立,循环结束
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart)); // 把<!--ksc-demo:之前的代码先存放在output里面
const commentContent = content.slice(commentStart + startTagLen, commentEnd); //截取内容 组件的代码集
const html = stripTemplate(commentContent); // 过滤返回template
const script = stripScript(commentContent); // 过滤返回script
let demoComponentContent = genInlineComponentText(html, script); //返回自执行函数
const demoComponentName = `kscDemo${id}`;
//output:['<demo-block>',<template slot="source"><ksc-demo0/></template>,</demo-block><demo-block>,<template slot="source"><ksc-demo1/></template>,</demo-block>]
output.push(`<template slot="source"><${demoComponentName} /></template>`); //插入到demo-block slot name='source' 里面
//后面会添加components:{} kscDemo:(function(){})()
componenetsString += `${demoComponentName}: ${demoComponentContent},`; //ksc-demo组件
// 重新计算下一次的位置
id++; //组件的id
start = commentEnd + endTagLen; //开始的位置
commentStart = content.indexOf(startTag, start); // 开始查找start之后是否还有<!--ksc-demo:
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
// 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
// todo: 优化这段逻辑
let pageScript = '';
if (componenetsString) {
pageScript = `<script>
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);
}
output.push(content.slice(start));
console.log('output',output)
//output = <demo-block><template slot="source"><ksc-demo0/></template></demo-block><demo-block><template slot="source"><ksc-demo1/></template></demo-block>
//组装成vue代码返回
return `
<template>
<section class="ksc_dome">
${output.join('')}
</section>
</template>
${pageScript}
`;
};
config.js
添加containers规则和fence规则,生成了markdown的实例,导出md
const Config = require('markdown-it-chain'); //支持markdown-it的链式操作
const containers = require('./containers'); // container-demo-open类型
const overWriteFenceRule = require('./fence'); //fence类型
const config = new Config();
config
.options.html(true).end()
.plugin('containers').use(containers).end(); //添加containers规则
const md = config.toMd(); //使用以上配置创建markdown-it实例
overWriteFenceRule(md); //修改fence
module.exports = md;
util.js
//过滤出template的内容,script的内容,使用 @vue/component-compiler-utils 与 vue-template-compiler对template 的编译,把render,staticRenderFns,script合并成一个对象返回
const { compileTemplate } = require('@vue/component-compiler-utils');// 编译Vue当个文件或者组件
const compiler = require('vue-template-compiler'); // 与vue-loader一起使用,被它引用 将模板预编译为渲染函数 插件解释://https://zhuanlan.zhihu.com/p/114239056
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/); //匹配script中间的内容,在result[2]
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();
}
// 暂时没找到类似写法, vue-loader源码中就是这样引入的,转换成组件
function genInlineComponentText(template, script) {
const finalOptions = {
source: `<div>${template}</div>`,
filename: 'inline-component', // TODO:这里有待调整
compiler //必须传递compiler,确定所使用的版本
};
//compiled的数据结构
// ast: {
// },
// code: 'var render = function() {\n' +
// ' var _vm = this\n' +
// ' var _h = _vm.$createElement\n' +
// ' var _c = _vm._self._c || _h\n' +
// ' return _c(\n' +
// tips: [],
// errors: []
const compiled = compileTemplate(finalOptions); // 将原始代码编译为JavaScript代码
let demoComponentContent = `${compiled.code}`; // compiled.code是原始代码html转的render函数
script = script.replace(/export\s+default/, 'const democomponentExport =');//export default 内容赋值给democomponentExport
// 没有绑定动态数据会生成静态render,保存到staticRenderFns数组中, 使用自执行函数返回组件的的结果
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
render,
staticRenderFns,
...democomponentExport
}
})()`;
return demoComponentContent;
}
module.exports = {
stripScript,
stripStyle,
stripTemplate,
genInlineComponentText
};
fence.js
配置type为fence规则,返回vue结构
// 覆盖默认的 fence 渲染策略
module.exports = md => {
const defaultRender = md.renderer.rules.fence;
//tokend的type和rules[fence]进行匹配,修改返回该规则的vue代码
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*(.*)$/);
//```后面是否为html,prevToken是上一个token判断是不是:::demo
if (token.info === 'html' && isInDemoContainer) {
//escapeHtml对字符串进行转义
return `<template slot="highlight"><pre v-highlight><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
}
//返回默认的渲染方式
return defaultRender(tokens, idx, options, env, self);
};
};
containers.js
用markdown-it-container插件是专门为:::来配置规则的,扩展性更强
const mdContainer = require('markdown-it-container'); //此插件识别一块容器,并指定如何返回
module.exports = md => {
md.use(mdContainer, 'demo', {
validate(params) {
//遇到:::demo 匹配进来 return返回验证是否通过, 返回true走render,否则直接跳过
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
// 匹配成功返回会生成一个数组
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
// 如果::: 后面有字符 nesting就会变成1,否则就是-1, 上面正则匹配所以:::后面一定是demo
if (tokens[idx].nesting === 1) {
//m[1]是:::后面的信息,比如'demo test'会映射到slot default里面
const description = m && m.length > 1 ? m[1] : '';
// 如果:::内部有``` tokens[index+1].type, 就一定是fence, content就是``` ```里的内容
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
//把截取到的md内容包装一层 description是:::demo 后面的内容
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--ksc-demo: ${content}:ksc-demo-->
`;
}
//nesting不等于1的时候,返回闭合标签
return '</demo-block>';
}
});
md.use(mdContainer, 'warning');
};