Recently, the nuggets community launched an online platform for editing running code, which not only allows you to share code on the platform as you do with CodePen, but also allows you to publish code demos as part of your article content.

I was thinking that with Digger, we could provide some useful and interesting tools, such as the Slides tool, which allows users to embed their own presentations in digger articles. So, in my spare time, I spent about a week launching WebSlides. Md, which allows you to write your presentations in Markdown and HTML and use Playgroud platforms like Diggings on Code as your contribution sharing platform!

WebSlides. Md + code on nuggets -> online presentation

Technology selection

In order to run code on any Playground platform that supports HTML, CSS, and JavaScript, our presentation library must be purely front-end rendered and not dependent on any libraries that require an engineering (compile) environment.

There are many alternative solutions for Web presentations, mature ones such as Sanshui’s NodePPT or the recently popular Slidev, but they all require a local Node runtime environment and are not easy to edit and run directly on the simple Playground.

Pure front-end rendering presentation tool has WebSlides, but the original WebSlides does not support Markdown syntax and must be handwritten with pure native HTML, which is very troublesome to write and not friendly to non-front-end students.

Is it ok to add Markdown syntax to WebSlides? Conclusion is feasible, we just need to introduce a simple real-time compilation of Markdown library, such as marked. Therefore, the final technical selection of this project was to combine marked and WebSlides. Marked compiled the Markdown text into HTML, and then rendered it to WebSides.

Technical implementation details

We can inherit WebSlides’ classes and add the logic to parse Markdown.

const defaultOptions = {
  loop: false.autoslide: false.changeOnClick: false.showIndex: true.navigateOnScroll: true.minWheelDelta: 40.scrollWait: 450.slideOffset: 50.marked: {
    renderer: new Renderer(),
    highlight: function(code, lang) {
      const Prism = require('prismjs');
      const language = Prism.languages[lang];
      if(language) {
        return Prism.highlight(code, language, lang);
      }
      return code;
    },
    pedantic: false.gfm: true.breaks: false.sanitize: false.smartLists: true.smartypants: false.xhtml: false.headerIds: false,}};export default {
  CDN: '//cdn.jsdelivr.net/npm'.indent: true.codeTheme: "default"
};

window.WebSlides = class MDSlides extends WebSlides {
  static get marked() {
    return marked;
  }
  static get config() {
    return config;
  }
  constructor({marked: markedOptions = {}, ... options} = {}) {
    const container = document.querySelector('#webslides:not([done="done"])');
    const {marked: defaultMarkedOptions, ... defaultOpts} = defaultOptions; options =Object.assign({}, defaultOpts, options);
    if(container) {
      const sections = container.querySelectorAll('section');
      if(sections.length) {
        const markedOpts = Object.assign({}, defaultMarkedOptions, markedOptions);
        marked.setOptions(markedOpts);

        sections.forEach((section) = > {
          let content = htmlDecode(section.innerHTML);
          if(WebSlides.config.indent) {
            content = trimIndent(content);
          }

          section.innerHTML = marked.parse(content);
        });
      }
      container.setAttribute('done'.'done');
    }
    let {codeTheme} = config;
    if(codeTheme && codeTheme ! = ='default') {
      if(!/^http(s?) : \ \ / / /.test(codeTheme)) {
        codeTheme = `${WebSlides.config.CDN}/ [email protected] / themes /${codeTheme}.css`;
      }
      addCSS(codeTheme);
    }
    super(options); }};Copy the code

The above code is relatively simple. The core of the code is that we parse the Markdown syntax in each section fragment by marked, then throw the parsed HTML back into the section element and let WebSlides render it.

Syntax highlighting is marked. We use PrismJS to handle syntax highlighting. If we have a syntax highlighting theme configured, load it down by dynamically adding CSS.

export function addCSS(url) {
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.type = 'text/css';
  link.href = url;
  document.documentElement.appendChild(link);
}
Copy the code

Another detail is that we can indent the Markdown relative to the section container.

export function trimIndent(input) {
  const lines = input.split(/\n/g).filter(l= > l.trim());
  let spaces = Infinity;
  lines.forEach((line) = > {
    spaces = Math.min(spaces, line.match(/^\s*/) [0].length);
  });
  if(spaces > 0) {
    input = input.replace(new RegExp(`^[^\\S\\n]{${spaces}} `.'mg'), ' ');
  }
  return input;
}
Copy the code

Add extensions

Next, we can add some useful extensions.

Because WebSlides implement layout and effects by adding semantic styles to elements, and standard Markdown does not support custom styles, it was necessary to implement some handy extensions.

First, we can preserve the native HTML capabilities of WebSlides. Here we implement an extended syntax embedded in native HTML by writing an extension to Marked. I define this syntax as the syntax of @ HTML with indented code paragraphs.

Such as:

:@html
< H1 > Title 
       

Body

## Other content
Copy the code

To implement this syntax, we simply write the following extension to marked.

// Override function
const tokenizer = {
  html(src) {
    const match = src.match(/^:\@html\s*? ((? :\n(? :[^\S\n]+[^\n]+)?) +)/i);
    if (match) {
      return {
        type: 'html'.raw: match[0].text: match[1].trim()
      };
    }

    // return false to use original codespan tokenizer
    return false; }};export default { tokenizer };
Copy the code

Implementing extensions in marked is not complicated, and we can override the default handling of existing tokens by overriding the Tokenizer and Renderer, which is described in detail in marked’s official documentation.

In the preceding code, we use regular expressions to match paragraphs and pass them to Parser as HTML tokens.

Second, we can add syntax to the container of HTML elements attached to the Markdown fragment, which I define as :tag? .class? [attr]? The syntactic form of indented code paragraphs.

Such as:

Wrap ## Markdown ## other contentCopy the code

The code above adds a

container to the indented fragment.

The implementation of this extension is also not complicated, the code is as follows:

export default {
  name: 'wrapper'.level: 'block'.tokenizer(src) {
    const match = src.match(/^:([\w-_]*)(\.[^\[\]\s]+)? ((? :\[[^\[\]]+\])*)[^\S\n]*((? :[^\S\n]*[^\s@][^\n]*)?) \s*? ((? :\n(? :[^\S\n]+[^\n]+)?) *)/i);
    if(match) {
      if(match[0= = =':') return; // none match
      return {
        type: 'wrapper'.raw: match[0].tagName: match[1]? match[1] : 'div'.className: match[2]? match[2].replace(/\./g.' ').trim() : null.attributes: match[3]? match[3].replace(/[\[\]]+/g.' ').trim() : null.text: match[4].body: trimIndent(match[5]).trim(), }; Apply colours to a drawing of}},renderer(token) {
    const {tagName, text, body} = token;
    const attrs = getAttrs(token);

    return ` <${tagName}${attrs}>${text}${marked.parse(body)}</${tagName}>\n`; }};Copy the code

By defining the corresponding Tokenizer and Renderer for the extension object, we can enable Marked to transform and render. This is also covered in detail in marked official documentation.

Third, we add attributes and style extensions to the Markdown content. We define {^. Class [attrs]} and {$. Class [attrs]} syntax, the former to add attributes and styles to the next sibling element, the latter to add attributes and styles to the previous sibling element or parent element.

So we can use it as follows:

{^.flexblock}
-The list of  - [The child columns] (https://juejin.cn){$[target="blank"]}
  - [The child columns] (https://juejin.cn){$[target="blank"]}
-The list of  - [The child columns] (https://juejin.cn){$[target="blank"]}
  - [The child columns] (https://juejin.cn){$[target="blank"]} <! -- equivalent to the following HTML code --><ul class="flexblock">
  <ul>
< li > < a href = "https://juejin.cn" target = "blank" > Denver < / a > < / li > < li > < a href = "https://juejin.cn" target = "blank" > Denver < / a > < / li > < / ul > < ul > < li > < a href = "https://juejin.cn" target = "blank" > Denver < / a > < / li > < li > < a href = "https://juejin.cn" Target ="blank">  Copy the code

The implementation of this extension is as follows:

export default {
  name: 'attr'.level: 'inline'.start(src) {
    const match = src.match(/ {[\ ^ \ $] / [^ \ ^ \ $]);
    if(match) return match.index;
  },
  tokenizer(src) {
    match = src.match(/^{([\^\$])(\.[^\[\]\s]+)? ((? :\[[^\[\]\n]+\])*)}/i);
    if (match) {
      const [b, c, d] = match.slice(1);
      const className = c ? c.replace(/\./g.' ').trim() : null;
      const attrsJson = {};
      if(className) attrsJson.className = className;
      d.split(/[\[\]]+/g).forEach((f) = > {
        if(f) {
          const [k, v] = f.split('=');
          attrsJson[k] = v.replace(/^\s*"(.*)"$/i."$1"); }});const attrs = JSON.stringify(attrsJson);
      return {
        type: 'attr'.raw: match[0].text: `<script type="text/webslides-attrs" position="${b}">${attrs}</script>`}; }},renderer(token) {
    returntoken.text; }};Copy the code

The parsing of this code was a bit tricky, we actually parsed it into a bit of JSON data and put it in a

const preattrs = section.querySelectorAll('script[type="text/webslides-attrs"]');
preattrs.forEach((el) = > {
    const parent = el.parentElement;
    if(parent && parent.tagName.toLowerCase() === 'p' && parent.childNodes.length === 1) {
      parent.setAttribute('position', el.getAttribute('position'));
      el = parent;
    }
    const node = findSibling(el);
    if(node) {
      const attrs = JSON.parse(el.textContent);
      for(const [k, v] of Object.entries(attrs)) {
        if(k === 'className') {
          node.className = node.className ? `${node.className} ${v}` : v;
        }
        elsenode.setAttribute(k, v); } el.remove(); }});Copy the code

The above three extensions address the issue of compatibility between HTML and Markdown. With these extensions, you can use Markdown to document WebSlides quickly and easily.

Other extensions

In order to improve WebSlides’ presentation capability, we can also write other extensions to support more functions. WebSlides. Md implements three additional extensions, namely the svgicon extension that supports SVG ICONS. The Katex extension supports mathematical formulas and the Mermaid extension supports rendering flowcharts.

I won’t go into details here, but look at the code:

svgicon

// https://github.com/simple-icons/simple-icons

export default {
  name: 'icon'.level: 'inline'.start(src) {
    const match = src.match(/{@[^@\n]/);
    if(match) return match.index;
  },
  tokenizer(src) {
    const match = src.match(/^{@\s*([\w_][\w-_]*)(? : \? ([^\s]+))? \s*? }/i);
    if(match) {
      return {
        type: 'icon'.raw: match[0].file: match[1].query: match[2]}; }},renderer(token) {
    const {file, query} = token;
    let className = "svgicon";
    let attrs = ' ';
    if(query) {
      const {searchParams} = new URL(`svgicon://svgicon?${query}`);
      for(let [key, value] of searchParams.entries()) {
        attrs = `${attrs} ${key}="${value}"`;
        if(key === 'style') {
          attrs = `${attrs} data-style=${value}`; }}}else {
      className = `${className} small`;
    }
    return `<img class="${className}" src="${WebSlides.config.CDN}/bootstrap-icons/icons/${file}.svg"${attrs}> `; }};Copy the code

katex

import katex from 'katex';
import {trimIndent} from '.. /utils';

function renderer(token) {
  const {text:code, macros} = token;
  let ret = code;
  try {
    return katex.renderToString(code, {
      macros
    });
  } catch(ex) {
    console.error(ex.message);
    returnret; }}export default [{
  name: 'katex'.level: 'block'.tokenizer(src) {
    const match = src.match(/^:@katex\s*? ((? :\n(? :[^\S\n]+[^\n]+)?) +)/i);
    if (match) {
      const body = trimIndent(match[1]).trim();
      const m = body.match(/^(\{[\s\S]*? The \})? ([\s\S]*)/i);

      let macros = m[1];

      if(macros) {
        try {
          macros = JSON.parse(m[1]);
        } catch(ex) {
          console.error(ex.message); }}return {
        type: 'katex'.raw: match[0],
        macros,
        text: m[2].trim()
      };
    }
  },
  renderer,
}, {
  name: 'katex-inline'.level: 'inline'.tokenizer(src) {
    const match = src.match(/^\$\$([^\n]+?) $/ $\ \);
    if (match) {
      return {
        type: 'katex'.raw: match[0].text: match[1].trim()
      };
    }
  },
  renderer,
}];
Copy the code

mermaid

const state = {};

import {trimIndent} from '.. /utils';

export default {
  name: 'mermaid'.level: 'block'.tokenizer(src) {
    const match = src.match(/^:\@mermaid\s*? ((? :\n(? :[^\S\n]+[^\n]+)?) +)/i);
    if (match) {
      return {
        type: 'mermaid'.raw: match[0].text: trimIndent(match[1]).trim(), }; }},renderer(token) {
    let code = `<div class="mermaid aligncenter">
${token.text}
</div>`;
    if(! state.hasMermaid) { state.hasMermaid =true;
      const scriptEl = document.createElement('script');
      scriptEl.src = `${WebSlides.config.CDN}/mermaid/dist/mermaid.min.js`;
      scriptEl.crossorigin = "anonymous";
      document.documentElement.appendChild(scriptEl);
      scriptEl.onload = () = > {
        mermaid.startOnLoad = false;
        mermaid.initialize({});
        mermaid.parseError = function(err){
          console.error(err);
        };
        const mermaidGraphs = document.querySelectorAll('.slide.current .mermaid');
        mermaid.init(mermaidGraphs);
      };
    }
    return code;
  },
  state,
};
Copy the code

Stomping pits and details

WebSlides. Md is no exception. So we need to do some detail processing for the pits we step on.

First, if Markdown is mixed with HTML tags, marked won’t add carriage returns to HTML block-level tags. This will affect Markdown parsing and processing. For example:

< h1 > title1< / h1 > # # title2
Copy the code

In the above code, the HTML tag on the first line affects the parsing of the Markdown code on the second line by adding a carriage return after all the HTML block-level tags.

const blockTags = 'address|article|aside|base|basefont|blockquote|body|caption'
+ '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption'
+ '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe'
+ '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option'
+ '|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr'
+ '|track|ul';

const blockReg = new RegExp(`(<\\s*(? : (? :\\/\\s*(? :${blockTags})\\s*)|(? : (? :${blockTags})\\s*\\/\\s*)|hr)>\\s*?) \\n`.'ig');

sections.forEach((section) = > {
  let content = htmlDecode(section.innerHTML);
  if(WebSlides.config.indent) {
    content = trimIndent(content);
  }

  content = content
    .replace(blockReg,(a) = > {
      return `${a}\n`;
    }); // A carriage return is required after the Block element, otherwise parsing will be problematic. });Copy the code

However, this code causes another problem. If there are any HTML block-level tags in a section of code within the fragment, the carriage returns will also be inserted, so we need to remove the carriage returns in this section by inherited the marked Renderer object. Let’s deal with carriage returns in the code fragment.

class Renderer extends marked.Renderer {
  code(code, infostring, escaped) {
    code = code.replace(blockReg, "$1"); // Remove carriage returns after Block elements
    return super.code(code, infostring, escaped); }}Copy the code

So that takes care of the carriage return problem.

And then we can do svgicon and savi in the constructor, especially savi, because inside the plugin we just render sAVI as a

tag, and that’s sAVI, When our Slides were not on the current page, while avi was processing, the real width and height of the container could not be obtained because the section display: None was displayed. As a result, the page rendered an invisible flowchart with width and height of 0. The problem with Svgicon is that by default we use img tags to load SVG files for rendering. There is no way to specify the color of the icon in this way. If we want to change the color, we can re-download and render the SVG tags.

Both avi and Svgicon can be handled while the current page is displayed, so we registered a WS: Slide-change event with the container and handled within the event.

container.addEventListener('ws:slide-change'.() = > {
    const section = document.querySelector('#webslides section.current');
    // load svgicon
    const svgicons = section.querySelectorAll('img.svgicon[fill],img.svgicon[stroke]');
    svgicons.forEach(async (el) => {
      if(el.clientHeight > 0) {
        loadSvgIcon(el);
      } else {
        el.onload = loadSvgIcon.bind(null, el); }});if(window.mermaid && window.mermaid.init) {
      const mermaidGraphs = document.querySelectorAll('.slide.current .mermaid');
      window.mermaid.init(mermaidGraphs); }});Copy the code

In this way, we basically solved all the details of the problem, the complete code can be seen in the rare Earth mining code repository, the specific use of the code can refer to the [code mining] code demo.

If you have any questions, please feel free to discuss them in the comments section. You are also welcome to make requests or contribute code to WebSlides.