“This is my fourth day of participating in the First Challenge 2022. For more details: First Challenge 2022.”

preface

In “A blog with VuePress + Github Pages”, we build a blog with VuePress and see what the final TypeScript Chinese document looks like.

In the process of building a blog, we explained how to write a markdown-it plug-in in “VuePress Blog Optimization extension Markdown Syntax”, and explained the implementation principle of Markdown-it in “Markdown-IT Principle Analysis”. In this article we will explain the actual code to help you better write plug-ins.

Parse

The markdown-it rendering process is divided into two parts, Parse and Render. If we want to implement a new Markdown syntax, for example, if we want to Parse @header as

header

, we can start with the Parse process.

The way to customize the parse rules can be found in markdown-it’s official documentation through the Ruler class:

var md = require('markdown-it') (); md.block.ruler.before('paragraph'.'my_rule'.function replace(state) {
  / /...
});
Copy the code

Paragraph rule my_rule = my_rule = my_rule = my_rule = my_rule

The first is Md.block. ruler. Besides, md.inline.ruler and MD.core. Ruler can customize the rules.

Then.before, look at the API related to Ruler, and methods like after, at, disable, enable. This is because rules are executed in order and changes to one rule may affect other rules.

And paragraph, how do I know which rule to insert before or after? This requires you to read the source code, there is no documentation to tell you about this…

If it’s md.block, look at parse_block.js, if it’s md.inline, look at parse_inline.js, if it’s md.core, look at parse_core.js.

var _rules = [
  // First 2 params - rule name & source. Secondary array - list of rules,
  // which can be terminated by this one.
  [ 'table'.require('./rules_block/table'),'paragraph'.'reference']], ['code'.require('./rules_block/code')], ['fence'.require('./rules_block/fence'),'paragraph'.'reference'.'blockquote'.'list']], ['blockquote'.require('./rules_block/blockquote'),'paragraph'.'reference'.'blockquote'.'list']], ['hr'.require('./rules_block/hr'),'paragraph'.'reference'.'blockquote'.'list']], ['list'.require('./rules_block/list'),'paragraph'.'reference'.'blockquote']], ['reference'.require('./rules_block/reference')], ['html_block'.require('./rules_block/html_block'),'paragraph'.'reference'.'blockquote']], ['heading'.require('./rules_block/heading'),'paragraph'.'reference'.'blockquote']], ['lheading'.require('./rules_block/lheading')], ['paragraph'.require('./rules_block/paragraph')]];Copy the code

Finally, function replace(state), where the function argument actually has more than state, looks at the parse code for any of the specific rules, such as heading. Js:

module.exports = function heading(state, startLine, endLine, silent) {
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
	
  // ...
};
Copy the code

It can be seen that in addition to state, there are also startLine, endLine and silent. In fact, the best way to write the code is to refer to the code that has been implemented.

Instance to explain

Let’s look at the code involved in parsing @header as

header

. This is what we want to render:

var md = window.markdownit();
// md.block.ruler.before(...)

var result = md.render(`@ header
contentTwo
`);

console.log(result);
Copy the code

Normally it renders as follows:

<p>@ header
contentTwo</p>
Copy the code

Now the expected render result is:

<h1>header</h1>
<p>contentTwo</p>
Copy the code

Let’s take a look at how this can be done, using the header.js code:

md.block.ruler.before('paragraph'.'@header'.function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
  
  / /...
})
Copy the code

Parse scans each line according to the newline character, so each line is matched by our custom function. The function supports four parameters: state, startLine, and endLine.

Let’s print state startLine, endLine and so on:

md.block.ruler.before('paragraph'.'@header'.function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
  
  console.log(JSON.parse(JSON.stringify(state)), startLine, endLine);
})
Copy the code

This is the result of the print:

The content of state is shown in a simplified way:

{
    "src": "@ header\ncontentTwo\n"."md": {... },"env": {... },"tokens": [...]. ."bMarks": [0.9.20]."eMarks": [8.19.20]."tShift": [0.0.0]."line": 0
}
Copy the code

For details on what these fields mean in state, see the state_block.js file, where:

  • BMarks represents the starting position of each line
  • EMarks indicates where each line ends
  • TShift represents the position of the first non-space character on each line

BMarks [startLine] + state. TShift [startLine] pos = 0 + 0 = 0

EMarks [startLine] Max = 8

From this we can also see that pos is the starting position of the line and Max is the end position of the line. Using pos and Max, we can intercept the string:

md.block.ruler.before('paragraph'.'@header'.function(state, startLine, endLine, silent){
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
  
  		console.log(JSON.parse(JSON.stringify(state)), startLine, endLine);
  		let text = state.src.substring(pos, max);
  		console.log(text);
  
		  state.line = startLine + 1;
			return true
})
Copy the code

The printed result is:

In the code we add state.line = startLine + 1; And return true, in order to enter the next line of traversal.

If we can extract the tokens each time, then we can make regular matches. If they match, customize tokens.

md.block.ruler.before('paragraph'.'myplugin'.function (state,startLine,endLine) {
  var ch, level, tmp, token,
      pos = state.bMarks[startLine] + state.tShift[startLine],
      max = state.eMarks[startLine];
      ch  = state.src.charCodeAt(pos);

      if(ch ! = =0x40/ * @ * / || pos >= max) { return false; }
      
      let text = state.src.substring(pos, max);
      let rg = /^@\s(.*)/;
      let match = text.match(rg);

      if (match && match.length) {
        let result = match[1];
        token = state.push('heading_open'.'h1'.1);
        token.markup = The '@';
        token.map = [ startLine, state.line ];

        token = state.push('inline'.' '.0);
        token.content = result;
        token.map = [ startLine, state.line ];
        token.children = [];

        token = state.push('heading_close'.'h1', -1);
        token.markup = The '@';

        state.line = startLine + 1;
        return true; }})Copy the code

At this point, the desired effect is achieved:

series

Blog building series is the only practical series I have written so far. It is estimated to be about 20 articles, explaining how to use VuePress to build and optimize a blog and deploy it to GitHub, Gitee, private server and other platforms. Full article address: github.com/mqyqingfeng…

Wechat: “MQyqingfeng”, add me Into Hu Yu’s only readership group.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.