I believe that md will be used more or less in development. So a simple md editor is particularly important. If you want to add an md editor to your project, you might as well take a look at this article
I think there are two types of editors. One is to realize real-time rendering on the left and right sides; One is to write syntax first, and then realize rendering through buttons.
In fact, real-time rendering is not difficult. The common problem to consider is xss, because the rendering library can customize the xss filtering of a third party (it was previously implemented through settings, that is, it came with itself, but it was cancelled after a certain version), so xss uses dompurify, which is officially recommended. Real time rendering can be realized by monitoring text changes through the editor api. Another problem to be considered is the correspondence between the code and the rendering area. But because this is contrary to my needs, I won't introduce it here. I believe small bosses can easily realize it
Uniform practice, let's take a look at the renderings
The toolbar above is actually just adding events and inserting corresponding statements into the cursor. emoji has not been implemented yet. It seems that it needs the support of a third-party library.
On the whole, there are no difficulties, but for these things, either the documents are scattered and unclear, or no documents can be found. If you really don't have documents, or the official simple documents, you may really want to greet him, hahaha. At this time, a usable code is particularly important. Although it may not have any comments, I believe you can understand it. Don't say much. Let's code it~
<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 </a> </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 </a> </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"> Title 1 (Ctrl+Alt+1) </div> <div class="header2-btn" :class="isHeader2Active" @click="insertHeader2Code"> Title 2 (Ctrl+Alt+2) </div> <div class="header3-btn" :class="isHeader3Active" @click="insertHeader3Code"> Title 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/" 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' // You must import to use in a webpack environment 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 // 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/">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> return foo<br>```', or: '<b>with tab / 4 spaces</b><br>....def whatever(foo):<br>.... 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 the webpack resolver is not imported, the module path will report an error modePath: 'ace/mode/markdown', // ditto 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 style maxLines: 1000, // The maximum number of lines. If it exceeds, the scroll bar will appear automatically minLines: 22, // The minimum number of rows. When the maximum number of rows is not reached, the editor will automatically expand and shrink the size fontSize: 14, // Font size in editor theme: this.themePath, // Default theme mode: this.modePath, // Default language mode tabSize: 4, // Tab set to 4 spaces readOnly: false, //read-only wrap: true, highlightActiveLine: true, value: this.codeValue }); marked.setOptions({ renderer: renderer, // highlight: function (code) { // return highlight.highlightAuto(code).value; // }, gfm: true,//The default is true. Allow the markdown of Git Hub standard tables: true,//The default is true. Allow support for table syntax. This option requires gfm to be true. breaks: false,//The default is false. Carriage return is allowed. This option requires gfm to be true. pedantic: false,//The default is false. Be compatible with the obscure parts of markdown.pl as much as possible. Do not correct any bad behaviors and errors of the original model. // sanitize: false, / / filtering (cleaning) the output is not supported. Filter with sanitizer or when rendering directly 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 more fashionable punctuation, such as adding dashes to reference syntax. }); // 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 succeeded"); // } // }); // this.aceEditor.commands.addCommand({ // name: 'paste', // bindKey: {win: 'Ctrl-V', mac: 'Command-M'}, // exec: function(editor) { // that.$message.success("paste succeeded"); // } // }); }, watch: { value(newVal) { console.log(newVal); this.aceEditor.setValue(newVal); } } } </script> <style scoped lang="scss"> .toolbar { cursor: pointer;//Mouse hand } .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 } .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 background: 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 value when referencing, that is, the content in the edit box
<MarkdownEditor v-bind:value="''"></MarkdownEditor>
Oh, by the way, I forgot to say something. About code block highlighting and latex rendering.
highlight.js is used for highlighting, and marked supports this library. Just use it directly. It can automatically identify the language. If you don't want to call that function, you can also judge the language the user will use. To use the theme, you need to reference the css corresponding to the style under the package. Another important thing is that the rendered tag must have the attribute of class hljs, otherwise you can only see that the code is highlighted. As for how to add the class attribute, if you don't have letax requirements, you only need to set a layer of label when rendering, and its class attribute is this.
The rest is latex, because marked itself does not support latex, but it supports rewriting the render function. This method is used to support latex. Here I use katex. Interested small bosses can try mathjax. However, one bad thing is that mathematical formulas need to be wrapped in code blocks, that is, $a * b $. But it's not a big problem. It's the king to render it well.
Well, that's all for sharing this time, see you again~