background

Markdown is often used in the company’s weekly report writing. In order to solve the efficiency problem that everyone in the team needs to summarize after writing weekly reports, WE have developed a weekly report Markdown editor for internal use. Team members can write corresponding weekly reports according to the project. Finally, the team leader can directly export the weekly report which is automatically classified and summarized according to the project and personnel. Later, the editor was no longer maintained for various reasons. Until recently, when I was preparing to write a paper and needed to use formulas and Gantt charts, I realized that many open source Markdown editors did not support formulas and charts. As a programmer, you have to create what you don’t have, so be prepared to write your own editor that supports formulas and graphs.

To prepare

Determine the tools you need to use before you start writing code:

  1. Use create-React-app to quickly build front-end logic
  2. The editor uses monaco-Editor
  3. Markdown parsing uses markdown-it
  4. Implement the formula functionality using MathJax
  5. Implement flow charts, sequence charts, Gantt charts, pie charts and other graphics using Avi

Start code

Initialize the project

npx create-react-app mkdown
Copy the code

Install dependencies

cd mkdown
yarn add markdown-it mathjax@3 mermaid monaco-editor markdown-it-table-of-contents lodash antd
Copy the code

Lodash’s debounce function was used in the editor to reduce the number of renders required for markdown to parse the preview. In addition to the preview function, loDash’s debounce function was also intended to provide local storage. After local storage was successful, a message popup window was required, which is why antD library was introduced.

Implement the front-end UI framework

The page structure of the editor is shown in the figure. At the top of the page, the Menu component provides some Menu items, followed by the Main component. Place the editor Edtor component on the left and the preview Prview component on the right in the body part.

To split the components, first create three new component files in the SRC folder.

cd src && touch {Menu,Main,Editor,Preview}.js 
Copy the code

After creating the file respectively in the corresponding file to achieve the basic component code in order to render the structure of the page component in the page style adjustment.

Menu.js

import React from "react";

function Menu() {
  return (
    <div className="menu">menu</div>
  );
}

export default Menu;
Copy the code

Main.js

import React from "react";
import Preview from "./Preview";
import Editor from "./Editor";

function Main() {
  return (
    <div className="main">
      <Editor />
      <Preview />
    </div>
  );
}

export default Main;
Copy the code

Editor.js

import React from "react";

function Editor() {
  return (
    <div className="editor">editor</div>
  );
}

export default Editor;
Copy the code

Preview.js

import React from "react";

function Preview() {
  return (
    <div className="preview">preview</div>
  );
}

export default Preview;
Copy the code

Replace the contents of index.js with

import React from 'react';
import ReactDOM from 'react-dom';
import Main from './Main';
import Menu from './Menu';
import './index.css';

ReactDOM.render(
  <React.StrictMode>
    <Menu />
    <Main />
  </React.StrictMode>,
  document.getElementById('root')
);

Copy the code

Modify the contents of index.css to add some basic styles for the framework.

html{
  --menu-height: 32px;
  font-size: 12px;
}

html.body.#root{
  height: 100%;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.main{
  display: flex;
  flex-wrap: wrap;
  height: calc(100% - var(--menu-height));
  justify-content: space-between;
  position: fixed;
  top:var(--menu-height);
  width: 100%;
}
.menu{
  background: # 232323;
  border-bottom: 1px solid #2E2E2E;
  box-sizing: border-box;
  color: # 858585;
  font-size: 1rem;
  height: var(--menu-height);
  line-height: var(--menu-height);
  padding: 0 1.25 rem;
  overflow: visible;
}

.editor{
  box-sizing: border-box;
  flex: 1;
  height: 100%;
  overflow:hidden;
  background: gray;
}

.preview {
  all: initial;
  flex: 1;
  height: 100%;
  margin: 0;
  overflow: auto;
  padding: 0;
}

Copy the code

After yarn start is executed, the framework shown in the following figure is displayed in the browser

Access editor

When implementing the Markdown Editor, use the new React hooks function to replace the contents of editor.js to bring in the Editor

import React, { useRef, useEffect } from 'react';
import { editor } from "monaco-editor";
import { debounce } from 'lodash';



function Editor(props) {
  const container = useRef(null);
  const { value, onChange } = props;
  useEffect(() => {
    const _editor =  editor.create(container.current, {
      value: value,
      language: "markdown",
      renderLineHighLight: "none",
      lineDecorationWidth: 0,
      lineNumbersLeft: 0,
      lineNumbersWidth: 0,
      fontSize: 20,
      lineNumbers: "on",
      automaticLayout: false,
      quickSuggestions: false,
      occurrencesHighlight: false,
      colorDecorators: false,
      wordWrap: true,
      theme: "vs-dark",
      minimap: {
        enabled: false}}); const _model = _editor.getModel(); _model.onDidChangeContent(debounce(() => { onChange(_model.getValue()); }, 500)); }, [value, onChange])return (
    <div className="editor" ref={container} />
  );
}

Editor.defaultProps = {
  value: ' ',
  onChange:() => { }
};

export default Editor;
Copy the code

The Editor component receives a value parameter and the onChange callback, which uses debounce to reduce the frequency of onChange firing when the Editor content changes to reduce the rendering frequency of the preview (preview requires frequent DOM reads, as described below).

Implement preview function

The Main component needs to be modified to receive content from the Editor component Editor and pass it to the Preview component before the Preview function can be implemented. The modified contents are as follows:

import React, { useState } from "react";
import Preview from "./Preview";
import Editor from "./Editor";

function Main() {
  const [source, setSource] = useState(' ');
  function handleSourceChange(newSource) {
    setSource(newSource);
  }
  return (
    <div className="main">
      <Editor onChange={handleSourceChange}/>
      <Preview source={source}/>
    </div>
  );
}

export default Main;
Copy the code

To realize the basic preview function of Markdown, in addition to introducing Markdown-it, markdown-it-table-of-contents, a plug-in of Markdown-IT, should be introduced to realize the function of TOC, and initial configuration should be carried out after introduction

import MarkdownIt from "markdown-it";
import tocPlugin from "markdown-it-table-of-contents";
const md = new MarkdownIt({
  html: false.xhtmlOut: false.breaks: false.langPrefix: "language-".linkify: true.typographer: false.quotes: "" "" "
});
md.use(tocPlugin, { includeLevel: [2.3].markerPattern: /^\[toc\]/im });

Copy the code

The full preview.js parser previews the basic functionality of the code

import React, { useRef, useEffect} from "react";
import MarkdownIt from "markdown-it";
import tocPlugin from "markdown-it-table-of-contents";

const md = new MarkdownIt({
  html: false.xhtmlOut: false.breaks: false.langPrefix: "language-".linkify: true.typographer: false.quotes: "" "" "
});
md.use(tocPlugin, { includeLevel: [2.3].markerPattern: /^\[toc\]/im });


function Preview(props) {
  const { source } = props;
  const ele = useRef(null);
  useEffect((a)= > {
    ele.current.innerHTML = md.render(source || "");
  }, [source]);
  return (
    <div className="preview" ref={ele}/>
  );
}

export default Preview;
Copy the code

When this is done, a basic Markdown edit and preview editor is complete. You can test the following image in your browser.

Next we implement support for formulas, and to facilitate configuration, we create a separate MathJax configuration file with the following content

window.MathJax = {
  tex: {
    inlineMath: [["$"."$"],
      ["\\("."\\)"],
      ["?"."?"]],displayMath: [["?"."?"],
      ["\ \ ["."\ \]"]],processEscapes: true
  },
  options: {
    skipHtmlTags: ["script"."noscript"."style"."textarea"."pre"."code"."a"].ignoreHtmlClass: "editor".processHtmlClass: 'tex2jax_process'}};export default window.MathJax;
Copy the code

After creating the configuration items, introduce the configuration items in preview.js along with MathJax.

The configuration item must come before the MathJax library so that MathJax can properly initialize based on the configuration item. Since we need MathJax to only convert the content in our preview component, and our preview DOM is not initialized at the time of MathJax initialization, we need to update the MathJax configuration items after initialization. According to the documentation on the MathJax website, Once mathjax initialized again modified configuration items cannot update the generated objects, but you can through the window. The mathjax. Startup. GetComponents () generated in line with the new configuration object. Also, in order to reinitialize MathJax only once after component initialization, this is recorded in the preview component.

The complete code is as follows

import React, { useRef, useEffect, useState} from "react";
import MarkdownIt from "markdown-it";
import tocPlugin from "markdown-it-table-of-contents";
import "./mathjax";
import "mathjax/es5/tex-svg";

const md = new MarkdownIt({
  html: false.xhtmlOut: false.breaks: false.langPrefix: "language-".linkify: true.typographer: false.quotes: "" "" "
});
md.use(tocPlugin, { includeLevel: [2.3].markerPattern: /^\[toc\]/im });


function Preview(props) {
  const [init, setInit] = useState(null);
  const { source } = props;
  const ele = useRef(null);
  useEffect((a)= > {
    if(! init) {window.MathJax.startup.elements = ele.current;
      window.MathJax.startup.getComponents();
      setInit(true);
    }
    ele.current.innerHTML = md.render(source || "");
    window.MathJax.typeset();
  }, [source, init]);
  return (
    <div className="preview" ref={ele}/>
  );
}

export default Preview;
Copy the code

The mathjax parsed formula defaults to a single line centered element, which you can modify to be an in-line block-level element. Add it to index.css

.preview mjx-container[jax="SVG"][display="true"]{
  display: inline-block;
}
Copy the code

After completing the formula function support we will implement the graph function. According to the API of Meridian.avi, you need to assign a temporary DOM to the image while rendering, which can be used to cache the DOM node of the generated image. Markdown -it will be parsed

Parsed into

.language-flow

Finally complete the preview.js code

import React, { useRef, useEffect, useState} from "react";
import MarkdownIt from "markdown-it";
import tocPlugin from "markdown-it-table-of-contents";
import "./mathjax";
import "mathjax/es5/tex-svg";
import mermaid from "mermaid";

mermaid.initialize({ startOnLoad: true });

const md = new MarkdownIt({
  html: false.xhtmlOut: false.breaks: false.langPrefix: "language-".linkify: true.typographer: false.quotes: "" "" "
});
md.use(tocPlugin, { includeLevel: [2.3].markerPattern: /^\[toc\]/im });


function Preview(props) {
  const [init, setInit] = useState(null);
  const { source } = props;
  const ele = useRef(null);
  let offcanvas = document.querySelector('#offcanvas');
  if(! offcanvas){ offcanvas =document.createElement('div');
    offcanvas.setAttribute('id'.'offcanvas');
    document.body.appendChild(offcanvas);
  }
  useEffect((a)= > {
    if(! init) {window.MathJax.startup.elements = ele.current;
      window.MathJax.startup.getComponents();
      setInit(true);
    }
    ele.current.innerHTML = md.render(source || "");
    ele.current.querySelectorAll(".language-flow").forEach(($el, idx) = > {
      mermaid.mermaidAPI.render(
        `chart-${idx}`,
        $el.textContent,
        (svgCode) => {
          $el.innerHTML = svgCode;
        },
        offcanvas
      );
    });
    window.MathJax.typeset();
  }, [source, init]);
  return (
    <div className="preview" ref={ele}/>
  );
}

export default Preview;
Copy the code

Realize the function of the graph

So far, the basic formulas and graphs and markdown parsing functions have been implemented, and some optimization points and save functions need to be implemented.

To optimize the

Fixed page crash caused by preview parsing failure when updating graphs and formulas

To solve this problem, put the rendering logic of the graph and formula in a try-catch.

Implement save function

  1. CMD+S will be implemented using localStorage combined with The Monaco custom shortcut. In order to be able to display successfully saved messages to the user after saving, ANTD’s Message function also needs to be introduced. The implementation of the saved functional logic code is mainly inEditor.jsIn the. The shortcut defined in Monaco is passededitor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, callback)The implementation.
import React, { useRef, useEffect } from 'react';
import { editor, KeyMod, KeyCode  } from "monaco-editor";
import { debounce } from 'lodash';
import message from 'antd/lib/message';
import 'antd/lib/message/style/index.css';

message.config({ top: 20.duration: 2.maxCount: 1 });

function Editor(props) {
  const container = useRef(null);
  const { value, onChange } = props;
  useEffect((a)= > {
    const _editor =  editor.create(container.current, {
      value: value,
      language: "markdown".renderLineHighLight: "none".lineDecorationWidth: 0.lineNumbersLeft: 0.lineNumbersWidth: 0.fontSize: 20.lineNumbers: "on".automaticLayout: false.quickSuggestions: false.occurrencesHighlight: false.colorDecorators: false.wordWrap: true.theme: "vs-dark".minimap: {
        enabled: false}});const _model = _editor.getModel();
    _model.onDidChangeContent(debounce((a)= > {
      onChange(_model.getValue());
    }, 500));
    _editor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, saveCache);
    function saveCache() {
      window.localStorage.setItem('cached', _model.getValue());
      message.info('Saved');
    }
  }, [value, onChange])
  return (
    <div className="editor" ref={container} />
  );
}

Editor.defaultProps = {
  value: '',
  onChange:() => { }
};

export default Editor;
Copy the code

In order to reduce the amount of compiled code, ANTD’s Message component and style were directly introduced when the Message component was introduced. To save, in addition to modifying editor.js, you need to modify main.js to ensure that you can restore the last saved content after refreshing the page.

  1. Menu save function

The logic of the Menu is realized in menu. js. In order to make the logic of each component independent, the Menu only uses postMessage to send command messages when clicking on the Menu in menu. js, and the components that need to process messages are subscribed to process them. When implementing the save function, when the user clicks the Save function in the Menu, the Menu component will send a save message, and the save function is handled by the “Editor” component of the subscription message.

Menu.js

index.css

Editor.js

Modified menu style effect

Add print menu

Menu.js is implemented in a similar way to save the Menu. You need to add a Menu item to menu.js and then send the corresponding message when clicking the Menu item.

Preview.js prints mainly Preview content, so put the printing processing logic in preview.js. When executing window.print(), if you click on the menu and execute immediately, you will find that the print preview includes the menu. To solve this problem, you need to wait some time after clicking on the menu.

After completing the interactive logic of JS, index.css needs to add the style of print to hide the page content that does not need to be printed, such as editor and menu.

Fixed editor size mismatch when browser zooming

So far, the basic functions of the editor have been completed, but when the browser zooming, it will be found that the size of the editor does not change, resulting in an abnormal interface. In order to solve this problem, it is necessary to monitor the browser’s resize event and timely modify the size of the editor.

The end of the

So far, the markdown editor I want to implement has been completed. In the future, I plan to implement functions such as pasting and uploading pictures and quickly inserting Markdown syntax. At present, the tool is mainly for solving the needs of individuals to write markdown. If you need or are interested in the online version of the portal, you can also welcome feedback if you find any problems. The code has been uploaded to github address