1
头图

foreword

In "An article that takes you to build a blog with VuePress + Github Pages" , we used VuePress to build a blog. Check the final effect: TypeScript Chinese document .

If we have browsed the official document , we will find a very useful function, that is, many code blocks, a Try button will appear when they are suspended:

image.png

Click will jump to the corresponding Playground, such as the illustrated button to jump is this link , we can modify the code and verify the effect this Playground.

If we want to achieve such a function, how to achieve it?

think

It's easy to think of writing a VuePress plugin to implement it. This effect looks a bit like a code duplication plugin, but when you think about it, it's not.

For the implementation of the code copy plug-in, refer to "Implementing a VuePress Plug-in from Scratch" , you can traverse each code block and insert a copy button after the page is rendered, and write the code to the clipboard when you click copy, but Code block jumping is different. Code jumping requires us to write a link address first, and then render the button. The question is where is the address of this link written? You know, all we can write is a normal markdown file...

So we thought, is it possible to expand the syntax of markdown? For example, the normal code block is written as:

```typescript
const message = "Hello World!";
```

To achieve this effect, can we write:

```typescript
// try-link: https://www.baidu.com
const message = 'Hello World!';
```

But when rendering, the try-link line of comments is not rendered, but becomes like this:

image.png

When clicking Try , jump to the corresponding link.

Try button can be displayed when the mouse hovers over the code block, similar to this effect:

1234.gif

markdown-it

at the official document , we can know: VuePress uses markdown-it to render Markdown, markdown-it what is 061ea842c01038? Check the Github repository 161ea842c01041 markdown-it, and you can see this introduction:

Markdown parser done right. Fast and easy to extend.

To put it simply, markdown-it is a markdown renderer, which can render markdown into html, etc., and markdown-it supports the extension function of writing plugins. In fact, why the markdown files in the VuePress project can support writing Vue components is because VuePress writes Since the plugin supports Vue syntax, can we also expand the syntax of markdown?

Fortunately, in the VuePress document , a configuration is provided to customize the markdown-it plugin:

VuePress uses markdown-it (opens new window) to render Markdown, and most of the above extensions are also implemented through custom plugins. If you want to go further, you can do some custom configuration for the current markdown-it instance through the markdown option in .vuepress/config.js:
module.exports = {
  markdown: {
    // markdown-it-anchor 的选项
    anchor: { permalink: false },
    // markdown-it-toc 的选项
    toc: { includeLevel: [1, 2] },
    extendMarkdown: md => {
      // 使用更多的 markdown-it 插件!
      md.use(require('markdown-it-xxx'))
    }
  }
}

The introduced method is known, but how to write this markdown-it plugin?

markdown-it plugin

markdown-it at the Github repository code and document , we can roughly understand markdown-it . The conversion process is similar to Babel, which is converted into an abstract syntax tree and then generates the corresponding code. and Render two processes.

We can also see this by looking at the source code

MarkdownIt.prototype.render = function (src, env) {
  env = env || {};
  return this.renderer.render(this.parse(src, env), this.options, env);
};

So here we have two ideas to solve the problem, a process is in Parse process, a process Render the process, for the sake of simplicity, I decided to deal directly with Render process, see Render source , where we can see Render In fact, default Rules (rendering rules) have been written according to some fixed types, such as code blocks:

default_rules.fence = function (tokens, idx, options, env, slf) {
  var token = tokens[idx],
      info = token.info ? unescapeAll(token.info).trim() : '',
      langName = '',
      langAttrs = '',
      highlighted, i, arr, tmpAttrs, tmpToken;

  if (info) {
    // ...
  }

  if (options.highlight) {
    highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content);
  } else {
    highlighted = escapeHtml(token.content);
  }

  if (highlighted.indexOf('<pre') === 0) {
    return highlighted + '\n';
  }

  if (info) {
    //...
  }


  return  '<pre><code' + slf.renderAttrs(token) + '>'
        + highlighted
        + '</code></pre>\n';
};

We can override this rule, referring to the plugin writing principle provided by markdown-it, we can write:

# 获取 md 实例后
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
    // ...
};

In order to save a little more trouble, I am going to directly get the final rendered HTML result, which is a string, and then match //try-link: xxx and replace it with a <a> link. Let's look at the HTML generated by the comment //try-link: xxx

image.png

Modify the config.js file:

module.exports = {
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

For the sake of brevity, I did not <a> link, but added a class. Where do I write the style of this class?

VuePress provides the docs/.vuepress/styles/index.styl file as a global style file that will be automatically applied, generated at the end of the final CSS file, with a higher priority than the default styles.

So we write the style under the index.styl

// 默认样式
.try-button {
    position: absolute;
    bottom: 1em;
    right: 1em;
    font-weight: 100;
    border: 1px solid #719af4;
    border-radius: 4px;
    color: #719af4;
    padding: 2px 8px;
    text-decoration: none;
    transition-timing-function: ease;
    transition: opacity .3s;
    opacity: 0;
}

// hover 样式
.content__default:not(.custom) a.try-button:hover {
    background-color: #719af4;
    color: #fff;
    text-decoration: none;
}

Sometimes, automatic compilation may not take effect, we can re-run yarn run docs:dev .

At this point, the button can be displayed normally (the default style transparency is 0, here is forcibly set the transparency to 1 for screenshots):

image.png

Next, we need to realize that this button is only displayed when the mouse hovers over the code block. Here we can use the "Implementing a VuePress Plugin from Scratch" to get all the code block elements on page mounted Then add the event, and then modify the config.js file:

module.exports = {
    plugins: [
      (options, ctx) => {
        return {
          name: 'vuepress-plugin-code-try',
          clientRootMixin: path.resolve(__dirname, 'vuepress-plugin-code-try/index.js')
        }
      }
    ],
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

Then create a new vuepress-plugin-code-try directory under the same level directory config.js , and then create a new index.js file:

export default {
  mounted () {
    setTimeout(() => {
        document.querySelectorAll('div[class*="language-"] pre').forEach(el => {
            if (el.querySelector('.try-button')) {
                el.addEventListener('mouseover', () => {
                    el.querySelector('.try-button').style.opacity = '1';
                })
                el.addEventListener('mouseout', () => {
                    el.querySelector('.try-button').style.opacity = '0';
                })
            }
        })
    }, 100)
  }
}

At this point, running the project again, we have achieved the effect we originally wanted:

1234.gif

series of articles

The blog building series is the only series of practical tutorials I have written so far, explaining how to use VuePress to build a blog and deploy it to GitHub, Gitee, personal servers and other platforms.

  1. An article that takes you to build a blog with VuePress + GitHub Pages
  2. An article teaches you how to synchronize GitHub and Gitee code
  3. will not use GitHub Actions yet? Check out this
  4. How does Gitee automatically deploy Pages? Or use GitHub Actions!
  5. A front-end Linux command
  6. A simple and sufficient Nginx Location configuration explanation
  7. A detailed tutorial from purchasing a server to deploying blog code
  8. detailed tutorial of domain name from purchase to filing to resolution
  9. How to set the last updated time of VuePress blog optimization
  10. VuePress blog optimization to add data statistics function
  11. VuePress blog optimization to open HTTPS
  12. VuePress blog optimization to open Gzip compression
  13. Implement a VuePress plugin from scratch

WeChat: "mqyqingfeng", add me to the only readership of Xianyu.

If there are any mistakes or inaccuracies, please be sure to correct me, thank you very much. If you like or have inspiration, welcome to star, which is also an encouragement to the author.


冴羽
9.3k 声望6.3k 粉丝