Share a Markdown editor based on Ace

Share a Markdown editor based on Ace

I think editors are divided into two categories, one is divided into left and right sides to achieve instant rendering; the other is to write the syntax first and then achieve rendering through buttons.

In fact, real-time rendering is not difficult. The common problem that needs to be considered is XSS. Because the rendering library can customize third-party XSS filtering (previously it was achieved through settings, that is, it came with it, but it was cancelled after a certain version), so XSS uses the officially recommended dompurify. Real-time rendering can be achieved by using the editor's own API to monitor text changes. Another issue that needs to be considered is the correspondence between the code and the rendering area. But because this is contrary to my needs, I will not introduce it here. I believe that small bosses can easily achieve it.

Unified convention, let's take a look at the effect diagram

Figure 1
Figure 2

The toolbar above actually adds events and inserts corresponding statements into the cursor. Emoji is not implemented yet and seems to require support from a third-party library.

Overall, there is no difficulty, but for these things, either the documentation is scattered and unclear, or there is no documentation to be found. If there is really no documentation, or the official documentation is very simple, you may really want to say hello to him, hahahaha. At this time, a usable code is particularly important. Although it may not have many comments, I believe that you are smart enough to understand its meaning. Without further ado, let's get to the code~

<template>
  <div>
    <div class="section-ace">
      <el-row>
        <el-col :span="6">
          <el-row>
            <el-col :span="12">
              <a class="editor-tab-content" :class="isEditActive" @click="showEdit">
                <i class="fa fa-pencil-square-o" aria-hidden="true"></i>
                Edit
            </el-col>
            <el-col :span="12">
              <a class="preview-tab-content" :class="isPreviewActive" @click="showPreview">
                <i class="fa fa-eye" aria-hidden="true"></i>
                Preview
            </el-col>
          </el-row>
        </el-col>
        <el-col :push="8" :span="18">
          <el-row>
            <div class="toolbar">
              <el-col :span="1">
                <div>
                  <i @click="insertBoldCode" class="fa fa-bold" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertItalicCode" class="fa fa-italic" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertMinusCode" class="fa fa-minus" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <el-popover placement="bottom"
                            width="125"
                            transition="fade-in-linear"
                            trigger="click"
                            content="">
                  <i slot="reference" class="fa fa-header" aria-hidden="true"></i>
                  <div>
                    <div class="header1-btn" :class="isHeader1Active" @click="insertHeader1Code">
                      Heading 1 (Ctrl+Alt+1)
                    </div>
                    <div class="header2-btn" :class="isHeader2Active" @click="insertHeader2Code">
                      Heading 2 (Ctrl+Alt+2)
                    </div>
                    <div class="header3-btn" :class="isHeader3Active" @click="insertHeader3Code">
                      Heading 3 (Ctrl+Alt+3)
                    </div>
                  </div>
                </el-popover>
              </el-col>
              <el-col :span="1">
                <el-popover placement="bottom"
                            width="125"
                            transition="fade-in-linear"
                            trigger="click"
                            content="">
                  <i slot="reference" class="fa fa-code" aria-hidden="true"></i>
                  <div>
                    <div class="text-btn" :class="isTextActive" @click="insertText">
                      Text (Ctrl+Alt+P)
                    </div>
                    <div class="code-btn" :class="isCodeActive" @click="insertCode">
                      Code (Ctrl+Alt+C)
                    </div>
                  </div>
                </el-popover>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertQuoteCode" class="fa fa-quote-left" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertUlCode" class="fa fa-list-ul" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertOlCode" class="fa fa-list-ol" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertLinkCode" class="fa fa-link" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="insertImgCode" class="fa fa-picture-o" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <el-upload
                    class="upload-demo"
                    action="https://jsonplaceholder.typicode.com/posts/"
                    :limit="1">
                    <i class="fa fa-cloud-upload" aria-hidden="true"></i>
                  </el-upload>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="selectEmoji" class="fa fa-smile-o" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <div>
                  <i @click="toggleMaximize" class="fa fa-arrows-alt" aria-hidden="true"></i>
                </div>
              </el-col>
              <el-col :span="1">
                <i @click="toggleHelp" class="fa fa-question-circle" aria-hidden="true"></i>
                <el-dialog :visible.sync="dialogHelpVisible"
                           :show-close="false"
                           top="5vh"
                           width="60%"
                           :append-to-body="true"
                           :close-on-press-escape="true">
                  <el-card class="box-card" style="margin: -60px -20px -30px -20px">
                    <div slot="header" class="helpHeader">
                      <i class="fa fa-question-circle" aria-hidden="true"><span>Markdown Guide</span></i>
                    </div>
                    <p>This site is powered by Markdown. For full documentation,
                      <a href="http://commonmark.org/help/" rel="external nofollow" target="_blank">click here</a>
                    </p>
                    <el-table
                      :data="tableData"
                      stripe
                      border
                      :highlight-current-row="true"
                      style="width: 100%">
                      <el-table-column
                        prop="code"
                        label="Code"
                        width="150">
                        <template slot-scope="scope">
                          <p v-html='scope.row.code'></p>
                        </template>
                      </el-table-column>
                      <el-table-column
                        prop="or"
                        label="Or"
                        width="180">
                        <template slot-scope="scope">
                          <p v-html='scope.row.or'></p>
                        </template>
                      </el-table-column>
                      <el-table-column
                        prop="devices"
                        label="Linux/Windows">
                      </el-table-column>
                      <el-table-column
                        prop="device"
                        label="Mac OS"
                        width="180">
                      </el-table-column>
                      <el-table-column
                        prop="showOff"
                        label="... to Get"
                      width="200">
                        <template slot-scope="scope">
                          <p v-html='scope.row.showOff'></p>
                        </template>
                      </el-table-column>
                    </el-table>
                  </el-card>
                </el-dialog>
              </el-col>
            </div>
          </el-row>
        </el-col>
      </el-row>
    </div>
    <br>
    <div id="container">
      <div class="show-panel">
        <div ref="markdown" class="ace" v-show="!isShowPreview"></div>
        <div class="panel-preview" ref="preview" v-show="isShowPreview"></div>
      </div>
    </div>
  </div>
</template>
<script>
import ace from 'ace-builds'
// To use it in a webpack environment, you must import import 'ace-builds/webpack-resolver';
import marked from 'marked'
import highlight from "highlight.js";
import "highlight.js/styles/foundation.css";
import katex from 'katex'
import 'katex/dist/katex.css'
import DOMPurify from 'dompurify';

const renderer = new marked.Renderer();
function toHtml(text){
  let temp = document.createElement("div");
  temp.innerHTML = text;
  let output = temp.innerText || temp.textContent;
  temp = null;
  return output;
}

function mathsExpression(expr) {
  if (expr.match(/^\$\$[\s\S]*\$\$$/)) {
    expr = expr.substr(2, expr.length - 4);
    return katex.renderToString(expr, { displayMode: true });
  } else if (expr.match(/^\$[\s\S]*\$$/)) {
    expr = toHtml(expr); // temp solution
    expr = expr.substr(1, expr.length - 2);
    //Does that mean your text is getting dynamically added to the page? If so, someone must be calling KaTeX to render
    // it, and that call needs to have the strict flag set to false as well. That is, console warnings, such as % is escaped or Chinese // link: https://katex.org/docs/options.html
    return katex.renderToString(expr, { displayMode: false , strict: false});
  }
}

const unchanged = new marked.Renderer()
renderer.code = function(code, language, escaped) {
  console.log(language);
  const isMarkup = ['c++', 'cpp', 'golang', 'java', 'js', 'javascript', 'python'].includes(language);
  let hled = '';
  if (isMarkup) {
    const math = mathsExpression(code);
    if (math) {
      return math;
    } else {
      console.log("highlight");
      hled = highlight.highlight(language, code).value;
    }
  } else {
    console.log("highlightAuto");
    hled = highlight.highlightAuto(code).value;
  }
  return `<pre class="hljs ${language}"><code class="${language}">${hled}</code></pre>`;
  // return unchanged.code(code, language, escaped);
};
renderer.codespan = function(text) {
  const math = mathsExpression(text);
  if (math) {
    return math;
  }
  return unchanged.codespan(text);
};

export default {
  name: "abc",
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      tableData: [{
        code: ':emoji_name:',
        or: 'β€”',
        devices: 'β€”',
        device: 'β€”',
        showOff: '🧑'
      },{
        code: '*Italic*',
        or: '_Italic_',
        devices: 'Ctrl+I',
        device: 'Command+I',
        showOff: '<em>Italic</em>'
      },{
        code: '**Bold**',
        or: '__Bold__',
        devices: 'Ctrl+B',
        device: 'Command+B',
        showOff: '<em>Bold</em>'
      },{
        code: '++Underscores++',
        or: 'β€”',
        devices: 'Shift+U',
        device: 'Option+U',
        showOff: '<ins>Underscores</ins>'
      },{
        code: '~~Strikethrough~~',
        or: 'β€”',
        devices: 'Shift+S',
        device: 'Option+S',
        showOff: '<del>Strikethrough</del>'
      },{
        code: '# Heading 1',
        or: 'Heading 1<br>=========',
        devices: 'Ctrl+Alt+1',
        device: 'Command+Option+1',
        showOff: '<h1>Heading 1</h1>'
      },{
        code: '## Heading 2',
        or: 'Heading 2<br>-----------',
        devices: 'Ctrl+Alt+2',
        device: 'Command+Option+2',
        showOff: '<h2>Heading 1</h2>'
      },{
        code: '[Link](https://a.com)',
        or: '[Link][1]<br>⁝<br>[1]: https://b.org',
        devices: 'Ctrl+L',
        device: 'Command+L',
        showOff: '<a href="https://commonmark.org/" rel="external nofollow" >Link</a>'
      },{
        code: '![Image](http://url/a.png)',
        or: '![Image][1]<br>⁝<br>[1]: http://url/b.jpg',
        devices: 'Ctrl+Shift+I',
        device: 'Command+Option+I',
        showOff: '<img src="https://cdn.acwing.com/static/plugins/images/commonmark.png" width="36" height="36" alt="Markdown">'
      },{
        code: '> Blockquote',
        or: 'β€”',
        devices: 'Ctrl+Q',
        device: 'Command+Q',
        showOff: '<blockquote><p>Blockquote</p></blockquote>'
      },{
        code: 'A paragraph.<br><br>A paragraph after 1 blank line.',
        or: 'β€”',
        devices: 'β€”',
        device: 'β€”',
        showOff: '<p>A paragraph.</p><p>A paragraph after 1 blank line.</p>'
      },{
        code: '<p>* List<br> * List<br> * List</p>',
        or: '<p> - List<br> - List<br> - List<br></p>',
        devices: 'Ctrl+U',
        device: 'Command+U',
        showOff: '<ul><li>List</li><li>List</li><li>List</li></ul>'
      },{
        code: '<p> 1. One<br> 2. Two<br> 3. Three</p>',
        or: '<p> 1) One<br> 2) Two<br> 3) Three</p>',
        devices: 'Ctrl+Shift+O',
        device: 'Command+Option+O',
        showOff: '<ol><li>One</li><li>Two</li><li>Three</li></ol>'
      },{
        code: 'Horizontal Rule<br><br>-----------',
        or: 'Horizontal Rule<br><br>***********',
        devices: 'Ctrl+H',
        device: 'Command+H',
        showOff: 'Horizontal Rule<hr>'
      },{
        code: '`Inline code` with backticks',
        or: 'β€”',
        devices: 'Ctrl+Alt+C',
        device: 'Command+Option+C',
        showOff: '<code>Inline code</code> with backticks'
      },{
        code: '```<br> def whatever(foo):<br>&nbsp;&nbsp;&nbsp;&nbsp;return foo<br>```',
        or: '<b>with tab / 4 spaces</b><br>....def whatever(foo):<br>....&nbsp;&nbsp;&nbsp;&nbsp;return foo',
        devices: 'Ctrl+Alt+P',
        device: 'Command+Option+P',
        showOff: '<pre class="hljs"><code class=""><span class="hljs-function"><span class="hljs-keyword">def</span>' +
          '<span class="hljs-title">whatever</span><span class="hljs-params">(foo)</span></span>:\n' +
          ' <span class="hljs-keyword">return</span> foo</code></pre>'
      }],
      dialogHelpVisible: false,
      isTextActive: '',
      isCodeActive: '',
      isHeader1Active: '',
      isHeader2Active: '',
      isHeader3Active: '',
      isShowPreview: false,
      isEditActive: "active",
      isPreviewActive: "",
      aceEditor: null,
      themePath: 'ace/theme/crimson_editor', // If webpack-resolver is not imported, the module path will report an error modePath: 'ace/mode/markdown', // Same as above codeValue: this.value || '',
    };
  },
  methods: {
    insertBoldCode() {
      this.aceEditor.insert("****");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column - 2);
    },
    insertItalicCode() {
      this.aceEditor.insert("__");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column - 1);
    },
    insertMinusCode() {
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.insert("\n\n");
      this.aceEditor.insert("----------");
      this.aceEditor.insert("\n\n");
      this.aceEditor.gotoLine(cursorPosition.row + 5, cursorPosition.column,true);
    },
    insertHeader1Code() {
      this.isHeader2Active = this.isHeader3Active = '';
      this.isHeader1Active = 'active';
      this.aceEditor.insert("\n\n");
      this.aceEditor.insert("#");
    },
    insertHeader2Code() {
      this.isHeader1Active = this.isHeader3Active = '';
      this.isHeader2Active = 'active';
      this.aceEditor.insert("\n\n");
      this.aceEditor.insert("##");
    },
    insertHeader3Code() {
      this.isHeader1Active = this.isHeader2Active = '';
      this.isHeader3Active = 'active';
      this.aceEditor.insert("\n\n");
      this.aceEditor.insert("###");
    },
    insertText() {
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.isCodeActive = '';
      this.isTextActive = 'active';
      this.aceEditor.insert("```\n\n```");
      this.aceEditor.gotoLine(cursorPosition.row + 2, cursorPosition.column,true);
    },
    insertCode() {
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.isTextActive = '';
      this.isCodeActive = 'active';
      this.aceEditor.insert("``");
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column + 1);
    },
    insertQuoteCode() {
      this.aceEditor.insert("\n>");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column + 1);
    },
    insertUlCode() {
      this.aceEditor.insert("\n*");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column + 1);
    },
    insertOlCode() {
      this.aceEditor.insert("\n1.");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column + 1);
    },
    insertLinkCode() {
      this.aceEditor.insert("[]()");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column - 3);
    },
    insertImgCode() {
      this.aceEditor.insert("![]()");
      let cursorPosition = this.aceEditor.getCursorPosition();
      this.aceEditor.moveCursorTo(cursorPosition.row, cursorPosition.column - 3);
    },
    uploadImg() {
      this.aceEditor.insert("![]()");
    },
    selectEmoji() {
      this.aceEditor.insert("****");
    },
    toggleMaximize() {
      this.aceEditor.insert("****");
    },
    toggleHelp() {
      this.dialogHelpVisible = !this.dialogHelpVisible;
    },
    showEdit() {
      this.$refs.preview.innerHTML = '';
      this.isEditActive = 'active';
      this.isPreviewActive = '';
      this.isShowPreview = false;
    },
    showPreview() {
      this.show();
      this.isEditActive = '';
      this.isPreviewActive = 'active';
      this.isShowPreview = true;
    },
    show(data) {
      let value = this.aceEditor.session.getValue();
      this.$refs.preview.innerHTML = DOMPurify.sanitize(marked(value));
      console.log(DOMPurify.sanitize(marked(value)));
    },
  },
  mounted() {
    this.aceEditor = ace.edit(this.$refs.markdown,{
      selectionStyle: 'line', //Selected stylemaxLines: 1000, //Maximum number of lines, scroll bar will appear automatically if exceededminLines: 22, //Minimum number of lines, the editor will automatically expand and contract when the maximum number of lines is not reachedfontSize: 14, //Font size in editortheme: this.themePath, //Default thememode: this.modePath, //Default language modetabSize: 4, //Tab is set to 4 spacesreadOnly: false, //Read-onlywrap: true,
      highlightActiveLine: true,
      value: this.codeValue
    });
    marked.setOptions({
      renderer: renderer,
      // highlight: function (code) {
      // return highlight.highlightAuto(code).value;
      // },
      gfm: true, //The default is true. Allows Git Hub standard markdown.
      tables: true, //The default is true. Enables support for table syntax. This option requires gfm to be true.
      breaks: false, //The default is false. Allow carriage return and line feed. This option requires gfm to be true.
      pedantic: false, //The default is false. Try to be as compatible with obscure parts of markdown.pl as possible. Does not correct any bad behaviors or errors of the original model.
      // sanitize: false, // Filter (clean) the output. Not supported. Use sanitizer or filter directly when rendering. xhtml: true, // If true, emit self-closing HTML tags for void elements (<br/>, <img/>, etc.) with a "/" as required by XHTML.
      silent: true, //If true, the parser does not throw any exception.
      smartLists: true,
      smartypants: false // Use smarter punctuation, such as dashes in quotations.
    });
    // this.aceEditor.session.on('change', this.show);
    // let that = this;
    // this.aceEditor.commands.addCommand({
    // name: 'Copy',
    // bindKey: {win: 'Ctrl-C', mac: 'Command-M'},
    // exec: function(editor) {
    // that.$message.success("Copy successful");
    // }
    // });
    // this.aceEditor.commands.addCommand({
    // name: 'Paste',
    // bindKey: {win: 'Ctrl-V', mac: 'Command-M'},
    // exec: function(editor) {
    // that.$message.success("Paste successful");
    // }
    // });
  },
  watch:
    value(newVal) {
      console.log(newVal);
      this.aceEditor.setValue(newVal);
    }
  }
}
</script>

<style scoped lang="scss">
.toolbar {
  cursor: pointer; //mouse hand shape}
.show-panel {
  padding: 5px;
  border: 1px solid lightgray;
  .ace {
    position: relative !important;
    border-top: 1px solid lightgray;
    display: block;
    margin: auto;
    height: auto;
    width: 100%;
  }
  .panel-preview {
    padding: 1rem;
    margin: 0 0 0 0;
    width: auto;
    background-color: white;
  }
}

.editor-tab-content, .preview-tab-content, .header1-btn, .header2-btn, .header3-btn, .text-btn, .code-btn{
  border-bottom-color: transparent;
  border-bottom-style: solid;
  border-radius: 0;
  padding: .85714286em 1.14285714em 1.29999714em 1.14285714em;
  border-bottom-width: 2px;
  transition: color .1s ease;
  cursor: pointer; //mouse hand shape}

.header1-btn, .header2-btn, .header3-btn, .code-btn, .text-btn {
  font-size: 5px;
  padding: .78571429em 1.14285714em!important;
}

.active {
  background-color: transparent;
  box-shadow: none;
  border-color: #1B1C1D;
  font-weight: 700;
  color: rgba(0,0,0,.95);
}

.header1-btn:hover, .header2-btn:hover, .header3-btn:hover, .text-btn:hover, .code-btn:hover {
  cursor: pointer; //mouse hand shapebackground: rgba(0,0,0,.05)!important;
  color: rgba(0,0,0,.95)!important;
}

.helpHeader {
  font-size: 1.228571rem;
  line-height: 1.2857em;
  font-weight: 700;
  border-top-left-radius: .28571429rem;
  border-top-right-radius: .28571429rem;
  display: block;
  font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
  background: #FFF;
  box-shadow: none;
  color: rgba(0,0,0,.85);
}
</style>

This time the code also needs to bind the value when referencing, that is, the content in the edit box

<MarkdownEditor v-bind:value="''"></MarkdownEditor>

Oh, by the way, I forgot to mention something. Regarding code block highlighting and latex rendering.

Highlight uses highlight.js. Marked supports this library and can be used directly. It can automatically identify the language. If you don’t want to call that function, you can also judge the language that the user will use. To use the theme, you need to reference the css corresponding to the style in the package. Another important thing is that the rendered tag must have the attribute of class hljs, otherwise you can only see the code is highlighted. As for how to add the class attribute, if you don’t have a letax requirement, you only need to add a layer of tags when rendering, and its class attribute is this.

The only thing left is latex, because marked itself does not support latex, but it supports rewriting the render function to achieve support for latex. Here I use katex, and those who are interested can try mathjax. However, one disadvantage is that the mathematical formula needs to be surrounded by a code block, i.e. $a * b$ . But this is not a big problem. What matters most is the ability to render well.

Well, this sharing ends here, see you again~

This is the end of this article about the Ace-based Markdown editor. For more information about the Ace Markdown editor, please search for previous articles on 123WORDPRESS.COM or continue to browse the following related articles. I hope you will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • How to use the markdown editor component in Vue3
  • How to use Electron to simply create a Markdown editor
  • Using Vue to implement a markdown editor example code
  • Using simplemde to implement markdown editor in vue (adding image upload function)

<<:  How to operate Docker and images

>>:  HTML iframe usage summary collection

Recommend

Pure CSS custom multi-line ellipsis problem (from principle to implementation)

How to display text overflow? What are your needs...

In-depth understanding of MySQL global locks and table locks

Preface According to the scope of locking, locks ...

Build nginx virtual host based on domain name, port and IP

There are three types of virtual hosts supported ...

Detailed tutorial on deploying Django project under CentOS

Basic Environment Pagoda installation service [Py...

AsyncHooks asynchronous life cycle in Node8

Async Hooks is a new feature of Node8. It provide...

Analysis of MySQL lock mechanism and usage

This article uses examples to illustrate the MySQ...

A designer complains about Hammer's official website again

Last year, the open letter was a huge hit, even a...

Example of using Dockerfile to build an nginx image

Introduction to Dockerfile Docker can automatical...

9 Tips for MySQL Database Optimization

Table of contents 1. Choose the most appropriate ...

Solution to the problem that order by is not effective in MySQL subquery

By chance, I discovered that a SQL statement prod...

How to configure Jupyter notebook in Docker container

Jupyter notebook is configured under the docker c...

6 solutions to IDEA's inability to connect to the MySQL database

This article mainly introduces 6 solutions to the...