Element MD-loader source code address

Why copy MD-loader

Recently, I planned to rewrite the official website of my component library. I encountered a problem when I wrote the display component part. It would be very troublesome to write the function display of all components in a.vue file. Wouldn’t it be nice if you could turn it into the desired page with a simple MD with code highlighting, demo boxes, and page styles?

Transformation logic

Modify the WebPack configuration beforehand

module: {
        rules: [
            //.....
            {
                test: /\.md$/,
                use: [
                    {
                        loader: 'vue-loader',
                        options: {
                            compilerOptions: {
                                preserveWhitespace: false
                            }
                        }
                    },
                    {
                        loader: path.resolve(__dirname, './md-loader/index.js')}]},]},Copy the code

Element md – loader directory

directory General function
index.js Entrance to the file
config.js Markdown-it configuration file
containers.js Render Adds custom output configuration
fence Modify the fence rendering strategy
util Some functions that handle parsing MD data

This section describes the functions of the MD-loader

First take a look at the demo


//demo.md
# # Table formDisplay multiple data with similar structure, and sort, filter, compare, or perform other customized operations on the data.### Base tableBasic table presentation usage. :::demo After the 'data' object array is injected into the 'el-table' element, the 'el-table-column' can be filled with data using the 'prop' attribute corresponding to the key name of the object, and the 'label' attribute is used to define the column name of the table. You can use the 'width' attribute to define the column width. ```html <template> <el-table :data="tableData"
      style="width: 100%">
      <el-table-column
        prop="date"
        label="Date"
        width="180">
      </el-table-column>
      <el-table-column
        prop="name"
        label="Name"
        width="180">
      </el-table-column>
      <el-table-column
        prop="address"
        label="Address">
      </el-table-column>
    </el-table>
  </template>

  <script>
    export default {
      data() {
        return {
          tableData: [{
            date: '2016-05-02',
            name: 'Wang Xiaohu',
            address: Lane 1518, Jinshajiang Road, Putuo District, Shanghai
          }
        }
      }
    }
  </script>
 
:::
(```)
Copy the code
  • A set of codeTwo purposesOne is shown as an example, one is shown as an example code, all indemo-blockComponent, that is, just write a set of code is enough

demo-block

  • The anchor

Ok, so here’s how element’s MD-Loader does it step by step

The preparatory work

You need to install both dependencies first

yarn add markdown-it-chain markdown-it-anchor -D
Copy the code

Config.js configures markdown-it

//config.js


const Config = require('markdown-it-chain');
const anchorPlugin = require('markdown-it-anchor'); // Add anchor to header const config = new config (); config .options.html(true).end()

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


const md = config.toMd();

module.exports = md;

Copy the code

Add an anchor point to the header with markdown-it-anchor

Markdown-it-chain configuration reference document

Now add config.js to index.js


const md = require('./config');

module.exports = function(source) {
  const content = md.render(source) // Get parsed data from.md, remember this content //.... }Copy the code

Since it is packaged as.vue, the final output must be exactly the same as the usual output

<template> </template> <script>export default {
  
}
</script>
Copy the code

So modify index.js

module.exports = function(source) {
  const content = md.render(source// Get parsed data from.md //....let script = `
           <script>
      export default {
        name: 'component-doc'} </script> '// the script tag // outputs the combined string of template and scriptreturn `
    <template>
      <section class="content element-doc">  //template
      </section>
    </template>`
    ${pageScript};
           
}
Copy the code

Now we need to consider the problem that an MD has many similar demos, and the output must be a VUE object, so these demos must be packaged into components.

Create components using render functions

Obviously you can’t create a component from a template, so you need to use the form of a rendering function.

Plug-ins that you need to use

The name General function
vue-template-compiler Template to render function (as configuration item)
component-compiler-utils Tools for compiling Vue single file components

Vue-loader compiles vUE single-file components using component-compiler-utils tools

Now introduce them in util.js

const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
Copy the code

In the source code this is done in the util.js genInlineComponentText function. (This function is quite complex and verbose, and can only be broken down.)


function genInlineComponentText(template, script) {}
Copy the code

First, the function takes two arguments, template and script, which are processed by their respective handlers from the content that was parsed from the start

function stripScript(content) {
  const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
  return result && result[2] ? result[2].trim() : ' '; } // Only output with script tagsfunction stripTemplate(content) {
  content = content.trim();
  if(! content)return content;
  return content.replace(/<(script|style)[\s\S]+<\/\1>/g, ' ').trim(); }// filter out script and style, the output is naturally templateCopy the code

Please click the github documentation of the tool in the table above, corresponding to the options configuration inside, anyway, just follow the documentation

  const options = {
    source: `<div>${template}</div>`,
    filename: 'inline-component'// The compiler is vue-template-compiler}Copy the code

Compile with compileTemplate introduced above

const compiled = compileTemplate(options)
Copy the code

If so, errors and warnings during compilation are thrown

if (compiled.tips && compiled.tips.length) {
    compiled.tips.forEach(tip => {
      console.warn(tip);
    });
  }
  // errors
  if(compiled.errors && compiled.errors.length) { console.error( //..... ) ; }Copy the code

And then finally get the compiled code

  let demoComponentContent = `${compiled.code}`
Copy the code

Now work with script

Script = script.trim() // Trim the whitespaceif (script) {
    script = script.replace(/export\s+default/, 'const democomponentExport =')}else {
    script = 'const democomponentExport = {}';
  }
Copy the code

This part is to replace the string export default with const democomponentExport = in order to facilitate the later deconstruction assignment

Finally return out

demoComponentContent = `(function() {
    ${demoComponentContent}// Run it once${script}
    return{render, // staticRenderFns, // no need to pay attention to this... DemocomponentExport // Deconstruct the attributes of the object above}})() 'return demoComponentContent;
Copy the code

${demoComponentContent} is already assigned to render when it runs once, and compiled

var render = function() {
  var _vm = this
  var _h = _vm.$createElementVar _c = _vm. Love. _c | | _h / / can be put belowreturnOmega of alpha c is treated as omega$createElement
  return _c(
    "div", // an HTML tag name [//.....] // The child node array is actually also caused by$createElement} var staticRenderFns = [] rendertrue
Copy the code

So now it’s clear. Go back to the index.js

let script = ` <script> export default {
        name: 'component-doc',
        components: {
          'demo-components': (function() {
               var render = function() {/ /...return{ render, staticRenderFns, ... democomponentExport } })() } } </script> `Copy the code

About render._withStripped = true

If undefined, it will not be intercepted by GET, meaning that no errors will be thrown after accessing a value that does not exist. The @component-compiler-utils is used here, so it is added automatically and can be ignored.

So what did we do up here

  • withmarkdown-itParse the data in.md
  • Pull awaytemplateandscript, compiled by plug-insrender Functioon, using it to create components.

Now the problem is that one content contains all demo data, how to distinguish and do the above operations for each demo, and the components in the components name cannot be the same, how to solve this problem.

‘Tag’ each Demo

First you need to download the dependencies

yarn add markdown-it-container -D
Copy the code

Markdown – it – address of the container

Look directly at the source code

Sample documents

Element source

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>` : ' '} <! --element-demo:${content}:element-demo-->
        `;
      }
      return '</demo-block>'; }})};Copy the code

To understand, the following is a block-level container, wrapped in the (::: : symbol), this plug-in can customize what the block-level renders

Description (1) *demo* (2) ::Copy the code
  • tokensIs an array containing all md codes in a block-level container, partitioned according to certain rules, for example

Tokens [IDX]. Type === ‘fence

  • By default, block-level containers return ::: wrapped content, which means that even if it is written on the same line as description, it is not included in the content by default. Render returns the content before or after the content, depending on the beginning or end of the page. Nesting – tokens[idx]. Nesting is equal to 1. See the Examples in this document.


  • is added at the beginning of the page and
    is added at the end of the page to form a
    component.
    is a globally registered component that is used for demonstration purposes and will be mentioned later.

There are three things in this Demo-block

${description}  //(1) description即为demo的开头说明,请返回demo.md查看
 <!--element-demo: ${content}: elemental-demo --> //(2) The element before and after the display component is the tag that will be found in index.js${content}// (3) this thing will render overlay in fence.js file, modify its tag contentCopy the code

Locate and assemble components based on tags

 const startTag = '<! --element-demo:';
  const startTagLen = startTag.length;
  const endTag = ':element-demo-->';
  const endTagLen = endTag.length;

  let componenetsString = ' ';
  letid = 0; / / the demo idletoutput = []; // The output contentletstart = 0; // Start position of stringlet commentStart = content.indexOf(startTag);
  let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
  while(commentStart ! == -1 && commentEnd ! == -1) { output.push(content.slice(start, commentStart)); const commentContent = content.slice(commentStart + startTagLen, commentEnd); const html = stripTemplate(commentContent); const script = stripScript(commentContent);let demoComponentContent = genInlineComponentText(html, script);
    const demoComponentName = `element-demo${id}`;
    output.push(`<template slot="source"><${demoComponentName} /></template>`);
    componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent}`; // start will be the end of the previous tag, which is generally the beginning of the previous demo code presentation id++; //id used to concatenate names start = commentEnd + endTagLen; commentStart = content.indexOf(startTag, start); commentEnd = content.indexOf(endTag, commentStart + startTagLen); } output.push(content.slice(start)) // concatenate the rest of the outputCopy the code

Source code this paragraph is a bit much, first need to understand the role of each variable

  • DemoComponentName and ID are used to name each demo component. The ID will be +1 for each loop in the while, so that components will not have the same name from the first to the last.

  • Output is an array that is eventually concatenated into the template of the output vue file, which is the HTML for the page. As you can see from the code above, there are only three places to operate on output. The start of the while loop pushes description, then the display component string,

 output.push(`<template slot="source"><${demoComponentName} /></template>`)
Copy the code

The demoComponentName is locally registered in the final output of the vUe-like object string, as mentioned above.

Why output.push(content.slice(start)) at the end

letContent = 'Description1' //description Component1 // show Description2 componentCode2 // show code Description2Copy the code

Content is the structure above, so output actually goes through the following process

1· First cycle

output.push(Description1)

output.push(Component1)

2. Second loop

output.push(componentCode1)

output.push(Description2)

output.push(Component2)

3. The loop ends

componentCode2

  • demoComponentContentWe’ve seen that it’s returnedrender Function.componenetsStringThe structure is similar to the following code
`componentName1:(renderFn1)(),componentName2:(renderFn2)()`
Copy the code

The code that ends up in script is

script = `<script>
      export default {
        name: 'component-doc',
        components: {
          component1:(function() {*render1* })(),
          component2:(function() {*render2* })(),
          component3:(function() {*render3* })(),
        }
      }
    </script>`;
Copy the code

Then index.js will return this,

 return `
    <template>
      <section class="content element-doc">
        ${output.join('')}
      </section>
    </template>
    ${script}
  `;
Copy the code

The resulting rough code is

<h3> I am demo1</h3> <template slot="source"><demo1/></template> 
<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(content)}</code></pre></template>Copy the code

Fence.js operates roughly the same as the comments I wrote above, with the main code as follows

if (token.info === 'html' && isInDemoContainer) {
      return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
    }
Copy the code

Modify the default output by overwriting, adding

Distribute content using named slots

<template slot="source"><${demoComponentName}/></template> // display the componentCopy the code

Open Element’s source code and find demo-block.vue in example=>components

   <div class="source">
      <slot name="source"></slot>
    </div>

Copy the code

Using the special slot feature on the template, you can pass content from parent to named slot.

Add anchor point

yarn add markdown-it-anchor
Copy the code

use

const Config = require('markdown-it-chain')
const anchor = require('markdown-it-anchor')
const containers = require('./containers')
const config = new Config()

config
    .options.html(true).end()
    .plugin('anchor').use(anchor, [
    {
        permalinkSymbol:The '#', // Change the pattern of the default link, which defaults to ¶ Permalink:true// Add the link permalinkBefore:true,// Place the link to the left of the title}]).end()Copy the code

After the configuration is complete, you need to add a basic route to link hops in mounted hooks

 renderAnchorHref() {
        if (/changelog/g.test(location.href)) return;
        const anchors = document.querySelectorAll('h2 a,h3 a,h4 a,h5 a');
        const basePath = location.href.split(The '#').splice(0, 2).join(The '#');

        [].slice.call(anchors).forEach(a => {
          const href = a.getAttribute('href');
          a.href = basePath + href;
        });
      },
Copy the code

By now, most of the code and functions of Element mD-Loader have been reviewed. Thanks to the source code contributed by Element team, I benefited a lot.

Hope to help you understand the source code, if there is anything wrong, welcome to criticize.