一些参考网址:
markdown-it官网:markdown-it | markdown-it 中文文档 (docschina.org)markdown-it插件的分析和源码分析参考地址:https://zhuanlan.zhihu.com/p/64290806element-ui的源码地址:https://github.com/ElemeFE/element
一:先从package.json找到本地运行官网的指令是在scripts里的dev指令
"dev": "npm run build:file && cross-env NODE_ENV=development webpack-dev-server --config build/webpack.demo.js & node build/bin/template.js",
找到对应md解析的module的处理loader
处理markdown文件是先用build/md-loader/index.js处理了,然后再用vue-loader处理的;
md组件文件都在 examples/docs里,下面拿examples/docs/zh-cn/button.md组件的解析为例子
二: 进入build/md-loader/index.js里查看时如何解析的md文件的
//build/md-loader/index.js const { stripScript, stripTemplate, genInlineComponentText } = require('./util'); //提取script内容和纯内容部分,以及拼接好模板的方法 const md = require('./config'); //暴露出已经处理好markdownit插件的md module.exports = function(source) { const content = md.render(source); //config里对render的插件的处理 ... };
第一步是在build/md-loader/config.js里处理的
// build/md-loader/config.js
const Config = require('markdown-it-chain'); //支持链式调用 markdown-it const anchorPlugin = require('markdown-it-anchor'); //配置标题目录跳转的插件 const slugify = require('transliteration').slugify; //把中文翻译成拼音的插件 const containers = require('./containers'); //内容块容器化处理插件 const overWriteFenceRule = require('./fence'); //修改默认的fence规则的渲染函数,加上展开的源码的部分 const config = newConfig(); config .options.html(true).end() .plugin('anchor').use(anchorPlugin, [ { level: 2, slugify: slugify, permalink: true, permalinkBefore: true} ]).end() .plugin('containers').use(containers).end(); //主要是这个插件里处理md里写的elementui组件的渲染的代码 const md =config.toMd(); overWriteFenceRule(md); module.exports = md;
主要的md里编写的elementui组件处理插件是build/md-loader/containers.js里处理的,把:::demo 和 ::: 之间的非描述部分,放到了<!--element-demo: ${content}:element-demo-->,后面在build/md-loader/index.js里处理;
// build/md-loader/containers.js
const mdContainer = require('markdown-it-container'); //这个插件可以让你支持内容块,识别 markdown的::: module.exports = md =>{ //用在markdown-it里,识别 ::: demo md.use(mdContainer, 'demo', { validate(params) { return params.trim().match(/^demo\s*(.*)$/); //*是匹配0次以上 }, /** * tokens 符合上面规则的所有内容 * idx 某一条的id */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] : ''; //紧跟在demo后面的描述的内容 const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : ''; //html的匹配内容 //返回把内容放到已经写好的组件demo-block里;把html的内容放到!--element-demo里,便于后面处理抽取 //demo-block组件已经是在entry.js里作为全局组件注册过了可以直接使用 return `<demo-block>${description ? `<div>${md.render(description)}</div>` : ''} <!--element-demo: ${content}:element-demo-->`; } return '</demo-block>'; } }); md.use(mdContainer, 'tip'); md.use(mdContainer, 'warning'); };
在build/md-loader/index.js 里处理解析放入到<!--element-demo: ${content}:element-demo--> 里的内容,提取里面的template部分和script部分,放到
//build/md-loader/index.js const { stripScript, stripTemplate, genInlineComponentText } = require('./util'); const md = require('./config'); //暴露出已经处理好markdownit插件的md module.exports = function(source) { const content =md.render(source); //md插件对里面的内容放到了<!--element-demo里,在这里做提取处理 const startTag = '<!--element-demo:'; const startTagLen =startTag.length; const endTag = ':element-demo-->'; const endTagLen =endTag.length; let componenetsString = ''; let id = 0; //demo 的 id let output = []; //输出的内容 let start = 0; //字符串开始位置 let commentStart = content.indexOf(startTag); //html开始的下标值 let commentEnd = content.indexOf(endTag, commentStart + startTagLen); //html结束的位置,从开始的标识结束的后面计算 //循环处理html的内容,必须是有开始和结尾标识的 while (commentStart !== -1 && commentEnd !== -1) { output.push(content.slice(start, commentStart)); //拿到前面非html内容的描述等文字部分 const commentContent = content.slice(commentStart + startTagLen, commentEnd); //拿到除去前后标识的html内容部分 const html = stripTemplate(commentContent); //拿到非script和style的内容部分 const script = stripScript(commentContent); //拿到script部分 let demoComponentContent = genInlineComponentText(html, script); //把md提取的内容拼接成vue的template编译的js const demoComponentName = `element-demo${id}`; output.push(`<template slot="source"><${demoComponentName} /></template>`); //把内容放到demo-block的source的slot中 componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`; //把处理好的template都作为组件放到components里 //重新计算下一次的位置 id++; start = commentEnd +endTagLen; commentStart =content.indexOf(startTag, start); commentEnd = content.indexOf(endTag, commentStart +startTagLen); } //仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
// componentsString是在上面提取的所有的 组件demo作为组件拼接的字符串
//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)); return` <template> <section class="content element-doc">${output.join('')} </section> </template> ${pageScript} `; };
在 build/md-loader/fence.js里重写md.renderer.rules.fence 规则,把md里的elementui组件放大demo-block的highlight插槽里,用于展示组件源码;
//覆盖默认的 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) { //html的加上高亮标签 return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`; } returndefaultRender(tokens, idx, options, env, self); }; };
全局组件demo-block的组件展示和组件源码插槽代码如下:
// examples/components/demo-block.vue
<template> <div class="demo-block":class="[blockClass, { 'hover': hovering }]"@mouseenter="hovering = true"@mouseleave="hovering = false"> <div class="source"> <!--组件渲染展示的位置 --> <slot name="source"></slot> </div> <div class="meta"ref="meta"> <div class="description"v-if="$slots.default"> <slot></slot> </div> <div class="highlight"> <!--组件源码展示的位置 --> <slot name="highlight"></slot> </div> </div> <div 。。。。