2

I usually use Markdown for writing articles, but when I publish it, I will encounter some platforms that do not support Markdown Rearrangement is impossible, so I will use it. Some Markdown tools for converting rich text, such as markdown-nice , if you use it more, you will be curious about how it is implemented, so there is this article.

markdown-nice is a project based on React , let's take a look at its overall page:

A toolbar at the top with three side-by-side areas in the middle, namely the editing area, the preview area, and the custom theme area. The custom theme area is hidden by default.

Basically it is a Markdown editor, with some adaptations for each platform added.

editor

The editor uses CodeMirror , specifically a secondary encapsulated component React-CodeMirror :

 import CodeMirror from "@uiw/react-codemirror";

class App extends Component {
    render() {
        return (
            <CodeMirror
                  value={this.props.content.content}
                  options={{
                    theme: "md-mirror",// 主题
                    keyMap: "sublime",// 快捷键
                    mode: "markdown",// 模式,也就是语言类型
                    lineWrapping: true,// 开启超长换行
                    lineNumbers: false,// 不显示行号
                    extraKeys: {// 配置快捷键
                      ...bindHotkeys(this.props.content, this.props.dialog),
                      Tab: betterTab,
                      RightClick: rightClick,
                    },
                  }}
                  onChange={this.handleThrottleChange}
                  onScroll={this.handleScroll}
                  onFocus={this.handleFocus}
                  onBlur={this.handleBlur}
                  onDrop={this.handleDrop}
                  onPaste={this.handlePaste}
                  ref={this.getInstance}
                />
        )
    }
}

Shortcuts, Commands

markdown-nice Set some shortcut keys through the extraKeys option, and also add some shortcut buttons in the toolbar:

The logic of these shortcut keys or command buttons to operate the text content is basically the same, first get the content of the current selection:

 const selected = editor.getSelection()

Then make the processing modification:

 `**${selected}**`

Finally replace the content of the selection:

 editor.replaceSelection(`**${selected}**`)

In addition, you can also modify the position of the cursor to improve the experience. For example, after the bold operation, the cursor position will be behind the text, instead of * because markdown-nice will be modified after replacing the content of the selection the position of the cursor:

 export const bold = (editor, selection) => {
  editor.replaceSelection(`**${selection}**`);
  const cursor = editor.getCursor();
  cursor.ch -= 2;// 光标位置向前两个字符
  editor.setCursor(cursor);
};

sheet

The table syntax of Markdown is more troublesome to write by hand, markdown-nice For the table, it only provides the function of inserting table syntax symbols for you, you can enter the number of table rows and columns to be inserted:

After confirmation, the symbol will be inserted automatically:

The implementation is actually the splicing logic of a string:

 const text = this.buildFormFormat(this.state.rowNum, this.state.columnNum);

buildFormFormat = (rowNum, columnNum) => {
    let formFormat = "";
    // 最少会创建三行
    for (let i = 0; i < 3; i++) {
        formFormat += this.buildRow(i, columnNum);
    }
    // 超过三行
    for (let i = 3; i <= rowNum; i++) {
        formFormat += this.buildRow(i, columnNum);
    }
    return formFormat;
};

buildRow = (rowNum, columnNum) => {
    let appendText = "|";
    // 第一行为表头和内容的分隔
    if (rowNum === 1) {
        appendText += " --- |";
        for (let i = 0; i < columnNum - 1; i++) {
            appendText += " --- |";
        }
    } else {
        appendText += "     |";
        for (let i = 0; i < columnNum - 1; i++) {
            appendText += "     |";
        }
    }
    return appendText + (/windows|win32/i.test(navigator.userAgent) ? "\r\n" : "\n");
};

After the table characters are generated, the content of the current selection can be replaced:

 handleOk = () => {
    const {markdownEditor} = this.props.content;
    const cursor = markdownEditor.getCursor();

    const text = this.buildFormFormat(this.state.rowNum, this.state.columnNum);
    markdownEditor.replaceSelection(text);

    cursor.ch += 2;
    markdownEditor.setCursor(cursor);
    markdownEditor.focus();
};

Also modifies the cursor position and refocuses the editor.

upload picture

markdown-nice支持直接拖动图片到编辑区域进行上传和粘贴图片直接上传,这是通过监听CodeMirror编辑器的drop paste implementation:

 <CodeMirror 
    onDrop={this.handleDrop}
      onPaste={this.handlePaste}
/>
 handleDrop = (instance, e) => {
    if (!(e.dataTransfer && e.dataTransfer.files)) {
        return;
    }
    for (let i = 0; i < e.dataTransfer.files.length; i++) {
        uploadAdaptor({file: e.dataTransfer.files[i], content: this.props.content});
    }
};
handlePaste = (instance, e) => {
    if (e.clipboardData && e.clipboardData.files) {
      for (let i = 0; i < e.clipboardData.files.length; i++) {
        uploadAdaptor({file: e.clipboardData.files[i], content: this.props.content});
      }
    }
}

It is judged that if there is a file in the dragged or pasted data, the uploadAdaptor method will be called:

 export const uploadAdaptor = (...args) => {
    const type = localStorage.getItem(IMAGE_HOSTING_TYPE);
    if (type === IMAGE_HOSTING_NAMES.aliyun) {
        const config = JSON.parse(window.localStorage.getItem(ALIOSS_IMAGE_HOSTING));
        if (
            !config.region.length ||
            !config.accessKeyId.length ||
            !config.accessKeySecret.length ||
            !config.bucket.length
        ) {
            message.error("请先配置阿里云图床");
            return false;
        }
        return aliOSSUpload(...args);
    }
}

Other types of image beds are omitted. Taking Alibaba Cloud OSS as an example, it will first check whether the relevant configuration exists. If it exists, it will call the aliOSSUpload method:

 import OSS from "ali-oss";

export const aliOSSUpload = ({
  file = {},
  onSuccess = () => {},
  onError = () => {},
  images = [],
  content = null, // store content
}) => {
  const config = JSON.parse(window.localStorage.getItem(ALIOSS_IMAGE_HOSTING));
  // 将文件类型转成base64类型
  const base64Reader = new FileReader();
  base64Reader.readAsDataURL(file);
  base64Reader.onload = (e) => {
    const urlData = e.target.result;
    const base64 = urlData.split(",").pop();
    // 获取文件类型
    const fileType = urlData
      .split(";")
      .shift()
      .split(":")
      .pop();

    // base64转blob
    const blob = toBlob(base64, fileType);

    // blob转arrayBuffer
    const bufferReader = new FileReader();
    bufferReader.readAsArrayBuffer(blob);
    bufferReader.onload = (event) => {
      const buffer = new OSS.Buffer(event.target.result);
      aliOSSPutObject({config, file, buffer, onSuccess, onError, images, content});
    };
  };
};

This step is mainly to convert the file type to arrayBuffer type, and finally call aliOSSPutObject to upload the file:

 const aliOSSPutObject = ({config, file, buffer, onSuccess, onError, images, content}) => {
  let client = new OSS(config);

  // 上传文件名拼接上当前时间
  const OSSName = getOSSName(file.name);

  // 执行上传操作
  client
    .put(OSSName, buffer)
    .then((response) => {
      const names = file.name.split(".");
      names.pop();
      const filename = names.join(".");
      const image = {
        filename, // 名字不变并且去掉后缀
        url: response.url,
      };
      // 插入到文档
      if (content) {
        writeToEditor({content, image});
      }
    })
    .catch((error) => {
      console.log(error);
    });
};

After the upload is successful, the image will be inserted into the document:

 function writeToEditor({content, image}) {
  const isContainImgName = window.localStorage.getItem(IS_CONTAIN_IMG_NAME) === "true";
  let text = "";
  // 是否带上文件名
  if (isContainImgName) {
    text = `\n![${image.filename}](${image.url})\n`;
  } else {
    text = `\n![](${image.url})\n`;
  }
  const {markdownEditor} = content;
  // 替换当前选区
  const cursor = markdownEditor.getCursor();
  markdownEditor.replaceSelection(text, cursor);
  content.setContent(markdownEditor.getValue());
}

The specific upload logic of other major platforms can refer to the source code: imageHosting.js .

Format Markdown

markdown-nice support the function of formatting Markdown , that is, beautification function, such as:

After beautification:

Formatting is using prettier :

 import prettier from "prettier/standalone";
import prettierMarkdown from "prettier/parser-markdown";

export const formatDoc = (content, store) => {
  content = handlePrettierDoc(content);
  // 给被中文包裹的`$`符号前后添加空格
  content = content.replace(/([\u4e00-\u9fa5])\$/g, "$1 $");
  content = content.replace(/\$([\u4e00-\u9fa5])/g, "$ $1");
  store.setContent(content);
  message.success("格式化文档完成!");
};

// 调用prettier进行格式化
const handlePrettierDoc = (content) => {
  const prettierRes = prettier.format(content, {
    parser: "markdown",
    plugins: [prettierMarkdown],
  });
  return prettierRes;
};

preview

The preview is to convert Markdown to html for display. The preview area only needs to provide a container element, such as div , and then convert the converted html Content use div.innerHTML = html method can be added.

Markdown转换为html ,比如markdown-itmarkedshowdownmarkdown-nice使用的是markdown-it .

Core code:

 const parseHtml = markdownParser.render(this.props.content.content);

return (
    <section
        dangerouslySetInnerHTML={{
            __html: parseHtml,
        }}
    />
)

markdownParser ie markdown-it Example:

 import MarkdownIt from "markdown-it";
export const markdownParser = new MarkdownIt({
  html: true,// 允许在源代码中存在HTML标签
  highlight: (str, lang) => {
    // 代码高亮逻辑,后面再看
  },
});

plugin

After creating an instance of MarkdownIt , many plugins were registered:

 markdownParser
  .use(markdownItSpan) // 在标题标签中添加span
  .use(markdownItTableContainer) // 在表格外部添加容器
  .use(markdownItMath) // 数学公式
  .use(markdownItLinkfoot) // 修改脚注
  .use(markdownItTableOfContents, {
    transformLink: () => "",
    includeLevel: [2, 3],
    markerPattern: /^\[toc\]/im,
  }) // TOC仅支持二级和三级标题
  .use(markdownItRuby) // 注音符号
  .use(markdownItImplicitFigures, {figcaption: true}) // 图示
  .use(markdownItDeflist) // 定义列表
  .use(markdownItLiReplacer) // li 标签中加入 p 标签
  .use(markdownItImageFlow) // 横屏移动插件
  .use(markdownItMultiquote) // 给多级引用加 class
  .use(markdownItImsize);

It is also reflected in the function comments of the plugin.

markdown-it Markdown字符串转成一个个tokentoken html A string, such as # 街角小林 will generate the following token list (with some fields removed):

 [
  {
    "type": "heading_open",
    "tag": "h1",
    "nesting": 1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "#",
    "info": "",
    "block": true,
  },
  {
    "type": "inline",
    "tag": "",
    "nesting": 0,
    "level": 1,
    "children": [
      {
        "type": "text",
        "tag": "",
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "街角小林",
        "markup": "",
        "info": "",
        "block": false,
      }
    ],
    "content": "街角小林",
    "markup": "",
    "info": "",
    "block": true,
  },
  {
    "type": "heading_close",
    "tag": "h1",
    "nesting": -1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "#",
    "info": "",
    "block": true
  }
]

markdown-it内部,完成各项工作的是一个个rules ,其实就是一个个函数,解析的rules分为三类: core , block , inline .

core normalizeblockinlinelinkifyreplacementssmartquotes These rules will execute the above rules in order on the incoming markdown string, which contains block and inlnie The execution process of the rules, block and inline related rules are used to generate a token , as the name suggests, a token responsible for generating block-level types token , such as title, code block, table, item list, etc., one responsible for generating inline types after block-level elements are generated token , such as text, links, pictures, etc.

block will scan line by line at runtime markdown string, all block levels will be executed for each line of string in turn rule function, parsing and generating block level tokenblock规则有tablecodefenceblockquotehr , list , heading , paragraph , etc.

block类型的规则处理完之后,可能会生成一种---22b077df8379b8ea41b06faf7878a9a7 type inlinetoken ,这种token属于未完全解析的tokeninline类型的token ,也就是对块级tokencontent解析生成内联tokeninline规则有textlink , image etc.

After these parsing rules are executed, an array of token will be output, and then a render related rule will be generated html string, so a markdown-it If the plugin wants to interfere with the generated token , then update, expand, and add different types of parsing rule , if you want to interfere with the generated html token ------ html , then by updating, extending, adding rendering rule .

The above is just a rough introduction. Those who are interested in in-depth understanding can read markdown-it source code or the following two series of articles:

markdown-it source code analysis 1-overall process , markdown-it series of articles

markdown-nice There are so many plugins used, some are from the community, some are written by yourself, let's take a look at two of them that are relatively simple.

1. markdownItMultiquote

 function makeRule() {
  return function addTableContainer(state) {
    let count = 0;
    let outerQuoteToekn;
    for (var i = 0; i < state.tokens.length; i++) {
      // 遍历所有token
      const curToken = state.tokens[i];
      // 遇到blockquote_open类型的token
      if (curToken.type === "blockquote_open") {
        if (count === 0) {
          // 最外层 blockquote 的 token
          outerQuoteToekn = curToken;
        }
        count++;
        continue;
      }
      if (count > 0) {
        // 给最外层的加一个类名
        outerQuoteToekn.attrs = [["class", "multiquote-" + count]];
        count = 0;
      }
    }
  };
}

export default (md) => {
  // 在核心规则下增加一个自定义规则
  md.core.ruler.push("blockquote-class", makeRule(md));
};

This plugin is very simple. When there are multiple nested blockquote , add a class name to the outermost blockquote token , the effect is as follows:

2. markdownItLiReplacer

 function makeRule(md) {
  return function replaceListItem() {
    // 覆盖了两个渲染规则
    md.renderer.rules.list_item_open = function replaceOpen() {
      return "<li><section>";
    };
    md.renderer.rules.list_item_close = function replaceClose() {
      return "</section></li>";
    };
  };
}

export default (md) => {
  md.core.ruler.push("replace-li", makeRule(md));
};

This plugin is even simpler, overwriting the built-in list_item rule, the effect is to add a ---6142f0a4fd4d8b234b148e9af527eeb9 li tag to the section tag.

External link to footnote

We all know that the biggest limitation of the official account is that hyperlinks are only allowed in the whitelist, and others will be filtered out, so if we don’t do anything, our hyperlinks will be gone. The solution is generally to convert them into footnotes and display At the end of the article, markdown-nice The logic to achieve this is more complicated, and will first change the content of Markdown to:

 [理想青年实验室](http://lxqnsys.com/)

formatted as:

 [理想青年实验室](http://lxqnsys.com/ "理想青年实验室")

That is to add the title, and then pass the markdown-it plugin processing token to generate a footnote:

 markdownParser
    .use(markdownItLinkfoot) // 修改脚注

The implementation of this plug-in is also more complicated. Those who are interested can read the source code: markdown-it-linkfoot.js .

In fact, we can choose another relatively simple idea, we can cover the markdown-it internal link token rendering rules, and collect all the link data at the same time, and finally we generate it ourselves html string spliced to markdown-it output html string.

For example, we create a markdownItLinkfoot2 plugin, register:

 // 用来收集所有的链接
export const linkList = []

markdownParser
    .use(markdownItLinkfoot2, linkList)

Pass the array of collection links to the plugin through options, followed by the plugin's code:

 function makeRule(md, linkList) {
  return function() {
    // 每次重新解析前都清空数组和计数器
    linkList.splice(0, linkList.length)
    let index = 0
    let isWeChatLink = false
    // 覆盖a标签的开标签token渲染规则
    md.renderer.rules.link_open = function(tokens, idx) {
      // 获取当前token
      let token = tokens[idx]
      // 获取链接的url
      let href = token.attrs[0] ? token.attrs[0][1] : ''
      // 如果是微信域名则不需要转换
      if (/^https:\/\/mp.weixin.qq.com\//.test(href)) {
        isWeChatLink = true
        return `<a href="${href}">`
      }
      // 后面跟着的是链接内的其他token,我们可以遍历查找文本类型的token作为链接标题
      token = tokens[++idx]
      let title = ''
      while(token.type !== 'link_close') {
        if (token.type === 'text') {
          title = token.content
          break
        }
        token = tokens[++idx]
      }
      // 将链接添加到数组里
      linkList.push({
        href,
        title
      })
      // 同时我们把a标签替换成span标签
      return "<span>";
    };
    // 覆盖a标签的闭标签token渲染规则
    md.renderer.rules.link_close = function() {
      if (isWeChatLink) {
        return "</a>"
      }
      // 我们会在链接名称后面加上一个上标,代表它存在脚注,上标就是索引
      index++
      return `<sup>[${index}]</sup></span>`;
    };
  };
}

export default (md, linkList) => {
  // 在核心的规则链上添加我们的自定义规则
  md.core.ruler.push("change-link", makeRule(md, linkList));
};

Then we generate the footnote html string by ourselves, and splicing it to the markdown-it output after parsing html string:

 let parseHtml = markdownParser.render(this.props.content.content);
if (linkList.length > 0) {
    let linkFootStr = '<div>引用链接:</div>'
    linkList.forEach((item, index) => {
        linkFootStr += `<div>[${index + 1}]&nbsp;&nbsp;&nbsp;${item.title}:${item.href}</div>`
    })
    parseHtml += linkFootStr
}

The effect is as follows:

Just refine the style.

Synchronized scrolling

The synchronous scrolling of the editing area and the preview area is a basic function. First, bind the mouse move-in event, so that you can determine which area the mouse is in which the scroll is triggered:

 // 编辑器
<div id="nice-md-editor" onMouseOver={(e) => this.setCurrentIndex(1, e)}></div>
// 预览区域
<div id="nice-rich-text" onMouseOver={(e) => this.setCurrentIndex(2, e)}></div>
    
setCurrentIndex(index) {
    this.index = index;
}

Then bind the scroll event:

 // 编辑器
<CodeMirror onScroll={this.handleScroll}></CodeMirror>
// 预览区域容器
<div 
    id={BOX_ID} 
    onScroll={this.handleScroll} 
    ref={(node) => {
        this.previewContainer = node;
    }}
>
    // 预览区域
    <section 
        id={LAYOUT_ID} 
        dangerouslySetInnerHTML={{
            __html: parseHtml,
        }}
        ref={(node) => {
            this.previewWrap = node;
        }}
    </section>
</div>
 handleScroll = () => {
    if (this.props.navbar.isSyncScroll) {
        const {markdownEditor} = this.props.content;
        const cmData = markdownEditor.getScrollInfo();
        // 编辑器的滚动距离
        const editorToTop = cmData.top;
        // 编辑器的可滚动高度
        const editorScrollHeight = cmData.height - cmData.clientHeight;
        // scale = 预览区域的可滚动高度 / 编辑器的可滚动高度
        this.scale = (this.previewWrap.offsetHeight - this.previewContainer.offsetHeight + 55) / editorScrollHeight;
        // scale = 预览区域的滚动距离 / 编辑器的滚动距离 = this.previewContainer.scrollTop / editorToTop
        if (this.index === 1) {
            // 鼠标在编辑器上触发滚动,预览区域跟随滚动
            this.previewContainer.scrollTop = editorToTop * this.scale;
        } else {
            // 鼠标在预览区域触发滚动,编辑器跟随滚动
            this.editorTop = this.previewContainer.scrollTop / this.scale;
            markdownEditor.scrollTo(null, this.editorTop);
        }
    }
};

The calculation is very simple. According to the ratio of the scrollable distances of the two areas equal to the ratio of the scrollable distances of the two areas, the scrolling distance of one of the areas is calculated, but this calculation is actually not very accurate, especially when there are a large number of When pictured:

It can be seen that the editor in the above figure has scrolled to section 4.2, and the preview area of section 4.2 is still invisible.

To solve this problem, it is not enough to simply calculate the height. It is necessary to be able to correspond the elements on both sides, and to predict the details, you can refer to another article of the author: How to implement a Markdown editor that can scroll accurately and synchronously .

theme

The theme is essentially the css style, and the markdown is converted into html and there are not many tags involved, as long as all of them are listed to customize the style.

markdown-nice Four style tags are created first:

1. basic-theme

The basic theme defines a set of default styles. The style content can be viewed in the basic.js file.

2. markdown-theme

Used to insert the selected theme style, which is used to override the style of basic-theme . The custom theme style will also be inserted into this tag:

3. font-theme

Used to specifically insert font styles, corresponding to this function:

 // 衬线字体 和 非衬线字体 切换
toggleFont = () => {
    const {isSerif} = this.state;
    const serif = `#nice { 
font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
}`;
    const sansSerif = `#nice { 
font-family: Roboto, Oxygen, Ubuntu, Cantarell, PingFangSC-light, PingFangTC-light, 'Open Sans', 'Helvetica Neue', sans-serif;
}`;
    const choosen = isSerif ? serif : sansSerif;
    replaceStyle(FONT_THEME_ID, choosen);
    message.success("字体切换成功!");
    this.setState({isSerif: !isSerif});
};

4. code-theme

As the name suggests, it corresponds to the style used to insert code blocks, markdown-it provides a highlight option to configure code block highlighting, provides a function to receive code characters and language types, Returns a html fragment, or wraps the pre tag and returns it, so that markdown-it will not be processed internally.

markdown-nice Use highlight.js to achieve code highlighting:

 export const markdownParser = new MarkdownIt({
  html: true,
  highlight: (str, lang) => {
    if (lang === undefined || lang === "") {
      lang = "bash";
    }
    // 加上custom则表示自定义样式,而非微信专属,避免被remove pre
    if (lang && highlightjs.getLanguage(lang)) {
      try {
        const formatted = highlightjs
          .highlight(lang, str, true)
          .value.replace(/\n/g, "<br/>") // 换行用br表示
          .replace(/\s/g, "&nbsp;") // 用nbsp替换空格
          .replace(/span&nbsp;/g, "span "); // span标签修复
        return '<pre class="custom"><code class="hljs">' + formatted + "</code></pre>";
      } catch (e) {
        console.log(e);
      }
    }
    // escapeHtml方法会转义html种的 &<>" 字符
    return '<pre class="custom"><code class="hljs">' + markdownParser.utils.escapeHtml(str) + "</code></pre>";
  },
});

highlight.js There are many built-in themes: styles , markdown-nice 6 from them:

And also supports the mac style, the difference is that the mac style adds the following styles:

One-click copy

markdown-nice有三个一键复制的按钮,分别是公众号知乎掘金 ,掘金现在本身编辑器就是markdown , so we ignore it directly.

the public:

 copyWechat = () => {
    const layout = document.getElementById(LAYOUT_ID); // 保护现场
    const html = layout.innerHTML;
    solveWeChatMath();
    this.html = solveHtml();
    copySafari(this.html);
    message.success("已复制,请到微信公众平台粘贴");
    layout.innerHTML = html; // 恢复现场
};

Know almost:

 copyZhihu = () => {
    const layout = document.getElementById(LAYOUT_ID); // 保护现场
    const html = layout.innerHTML;
    solveZhihuMath();
    this.html = solveHtml();
    copySafari(this.html);
    message.success("已复制,请到知乎粘贴");
    layout.innerHTML = html; // 恢复现场
};

The main difference is actually the solveWeChatMath and solveZhihuMath methods, which are used to solve the problem of formulas. markdown-nice Use MathJax to render the formula (see for yourself, the author is not familiar with MathJax , it is true that I can't understand~):

 try {
    window.MathJax = {
        tex: {
            inlineMath: [["\$", "\$"]],// 行内公式的开始/结束分隔符
            displayMath: [["\$\$", "\$\$"]],// 块级公式的开始/结束分隔符
            tags: "ams",
        },
        svg: {
            fontCache: "none",// 不缓存svg路径,不进行复用
        },
        options: {
            renderActions: {
                addMenu: [0, "", ""],
                addContainer: [
                    190,
                    (doc) => {
                        for (const math of doc.math) {
                            this.addContainer(math, doc);
                        }
                    },
                    this.addContainer,
                ],
            },
        },
    };
    require("mathjax/es5/tex-svg-full");
} catch (e) {
    console.log(e);
}

addContainer(math, doc) {
    const tag = "span";
    const spanClass = math.display ? "span-block-equation" : "span-inline-equation";
    const cls = math.display ? "block-equation" : "inline-equation";
    math.typesetRoot.className = cls;
    math.typesetRoot.setAttribute(MJX_DATA_FORMULA, math.math);
    math.typesetRoot.setAttribute(MJX_DATA_FORMULA_TYPE, cls);
    math.typesetRoot = doc.adaptor.node(tag, {class: spanClass, style: "cursor:pointer"}, [math.typesetRoot]);
}

// 内容更新后调用下列方法重新渲染公式
export const updateMathjax = () => {
  window.MathJax.texReset();
  window.MathJax.typesetClear();
  window.MathJax.typesetPromise();
};

The converted formula html structure is as follows:

The official account editor does not support formulas, so by directly inserting svg :

 export const solveWeChatMath = () => {
  const layout = document.getElementById(LAYOUT_ID);
  // 获取到所有公式标签
  const mjxs = layout.getElementsByTagName("mjx-container");
  for (let i = 0; i < mjxs.length; i++) {
    const mjx = mjxs[i];
    if (!mjx.hasAttribute("jax")) {
      break;
    }
    // 移除mjx-container标签上的一些属性
    mjx.removeAttribute("jax");
    mjx.removeAttribute("display");
    mjx.removeAttribute("tabindex");
    mjx.removeAttribute("ctxtmenu_counter");
    // 第一个节点为svg节点
    const svg = mjx.firstChild;
    // 将svg通过属性设置的宽高改成通过样式进行设置
    const width = svg.getAttribute("width");
    const height = svg.getAttribute("height");
    svg.removeAttribute("width");
    svg.removeAttribute("height");
    svg.style.width = width;
    svg.style.height = height;
  }
};

The Zhihu editor supports formulas, so it will directly replace the formula-related html with img tags:

 export const solveZhihuMath = () => {
  const layout = document.getElementById(LAYOUT_ID);
  const mjxs = layout.getElementsByTagName("mjx-container");
  while (mjxs.length > 0) {
    const mjx = mjxs[0];
    let data = mjx.getAttribute(MJX_DATA_FORMULA);
    if (!data) {
      continue;
    }

    if (mjx.hasAttribute("display") && data.indexOf("\\tag") === -1) {
      data += "\\\\";
    }
    // 替换整个公式标签
    mjx.outerHTML = '<img class="Formula-image" data-eeimg="true" src="" alt="' + data + '">';
  }
};

After processing the formula, it will execute the solveHtml method:

 import juice from "juice";

export const solveHtml = () => {
  const element = document.getElementById(BOX_ID);
  let html = element.innerHTML;
  // 将公式的容器标签替换成span
  html = html.replace(/<mjx-container (class="inline.+?)<\/mjx-container>/g, "<span $1</span>");
  // 将空格替换成&nbsp;
  html = html.replace(/\s<span class="inline/g, '&nbsp;<span class="inline');
  // 同上
  html = html.replace(/svg><\/span>\s/g, "svg></span>&nbsp;");
  // 这个标签上面已经替换过了,这里为什么还要再替换一遍
  html = html.replace(/mjx-container/g, "section");
  html = html.replace(/class="mjx-solid"/g, 'fill="none" stroke-width="70"');
  // 去掉公式的mjx-assistive-mml标签
  html = html.replace(/<mjx-assistive-mml.+?<\/mjx-assistive-mml>/g, "");
  // 获取四个样式标签内的样式
  const basicStyle = document.getElementById(BASIC_THEME_ID).innerText;
  const markdownStyle = document.getElementById(MARKDOWN_THEME_ID).innerText;
  const codeStyle = document.getElementById(CODE_THEME_ID).innerText;
  const fontStyle = document.getElementById(FONT_THEME_ID).innerText;
  let res = "";
  try {
    // 使用juice库将样式内联到html标签上
    res = juice.inlineContent(html, basicStyle + markdownStyle + codeStyle + fontStyle, {
      inlinePseudoElements: true,// 插入伪元素,做法是转换成span标签
      preserveImportant: true,// 保持!import
    });
  } catch (e) {
    message.error("请检查 CSS 文件是否编写正确!");
  }

  return res;
};

This step is mainly to replace the relevant tags of the formula, and then obtain the styles in the four style tags. The most critical step is to use juice to inline the styles into the html tag, so the style is previewed. is separated, but in the end the data we copy out is styled:

html After processing, the operation of copying to the clipboard will be performed at the end copySafari

 export const copySafari = (text) => {
  // 获取 input
  let input = document.getElementById("copy-input");
  if (!input) {
    // input 不能用 CSS 隐藏,必须在页面内存在。
    input = document.createElement("input");
    input.id = "copy-input";
    input.style.position = "absolute";
    input.style.left = "-1000px";
    input.style.zIndex = "-1000";
    document.body.appendChild(input);
  }
  // 让 input 选中一个字符,无所谓那个字符
  input.value = "NOTHING";
  input.setSelectionRange(0, 1);
  input.focus();

  // 复制触发
  document.addEventListener("copy", function copyCall(e) {
    e.preventDefault();
    e.clipboardData.setData("text/html", text);
    e.clipboardData.setData("text/plain", text);
    document.removeEventListener("copy", copyCall);
  });
  document.execCommand("copy");
};

Export to PDF

Exported as PDF function is actually implemented by the print function, which is to call:

 window.print();

You can see that the printed content is only the preview area. How is this achieved? It is very simple. Through media query, you can hide other elements that do not need to be printed in print mode:

 @media print {
  .nice-md-editing {
    display: none;
  }
  .nice-navbar {
    display: none;
  }
  .nice-sidebar {
    display: none;
  }
  .nice-wx-box {
    overflow: visible;
    box-shadow: none;
    width: 100%;
  }
  .nice-style-editing {
    display: none;
  }
  #nice-rich-text {
    padding: 0 !important;
  }
  .nice-footer-container {
    display: none;
  }
}

The effect is this:

Summarize

This article briefly understands the implementation principle of markdown-nice from the perspective of source code. The overall logic is relatively simple, and the implementation of some details is still a bit troublesome, such as extension markdown-it , support for mathematical formulas Wait. There are still many scenarios for extending markdown-it . For example, a large number of VuePress functions are implemented by writing markdown-it plug-ins, so if you have relevant development requirements, you can refer to these excellent open source projects. accomplish.


街角小林
886 声望773 粉丝