我认为的编辑器分成两类,一种是分为左右两边实现即时渲染;一种是先写语法,然后通过按钮实现渲染。
其实即时渲染也不难,共同需要考虑的问题就是xss,因为渲染库能自定义第三方的xss过滤(之前是通过设置来实现,也就是本身自带,不过在某个版本后被取消了),所以xss就用官方推荐的dompurify。即时渲染可以通过编辑器本身api实现文本变动监听来实现,还有一个需要考虑的问题就是代码与渲染区域的对应。但因为这与我的需求相悖,在这里就不介绍了,相信小老板们都能轻松实现
统一惯例,我们来看看效果图
上面的工具栏其实就是添加事件然后往光标插入对应的语句而已,emoji暂时没有实现,貌似需要第三方库支持。
整体来说并没有难点,只不过对于这些东西来说,要么是文档分散讲得不清楚,要么就是找不到什么文档。要是真没有文档的话,或者官方简陋的文档,你可能真的想问候一下他,哈哈哈。这个时候一个能用的代码就显得尤为重要,尽管它可能没什么注释,但相信聪明的你肯定能理解其中的意思。话不多说,上代码吧~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603<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> 编辑 </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> 预览 </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"> 标题 1 (Ctrl+Alt+1) </div> <div class="header2-btn" :class="isHeader2Active" @click="insertHeader2Code"> 标题 2 (Ctrl+Alt+2) </div> <div class="header3-btn" :class="isHeader3Active" @click="insertHeader3Code"> 标题 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"> 文本 (Ctrl+Alt+P) </div> <div class="code-btn" :class="isCodeActive" @click="insertCode"> 代码 (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' // 在 webpack 环境中使用必须要导入 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(/^$$[sS]*$$$/)) { expr = expr.substr(2, expr.length - 4); return katex.renderToString(expr, { displayMode: true }); } else if (expr.match(/^$[sS]*$$/)) { 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/" rel="external nofollow" >Link</a>' },{ code: '', 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', // 不导入 webpack-resolver,该模块路径会报错 modePath: 'ace/mode/markdown', // 同上 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("nn"); this.aceEditor.insert("----------"); this.aceEditor.insert("nn"); this.aceEditor.gotoLine(cursorPosition.row + 5, cursorPosition.column,true); }, insertHeader1Code() { this.isHeader2Active = this.isHeader3Active = ''; this.isHeader1Active = 'active'; this.aceEditor.insert("nn"); this.aceEditor.insert("#"); }, insertHeader2Code() { this.isHeader1Active = this.isHeader3Active = ''; this.isHeader2Active = 'active'; this.aceEditor.insert("nn"); this.aceEditor.insert("##"); }, insertHeader3Code() { this.isHeader1Active = this.isHeader2Active = ''; this.isHeader3Active = 'active'; this.aceEditor.insert("nn"); this.aceEditor.insert("###"); }, insertText() { let cursorPosition = this.aceEditor.getCursorPosition(); this.isCodeActive = ''; this.isTextActive = 'active'; this.aceEditor.insert("```nn```"); 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', //选中样式 maxLines: 1000, // 最大行数,超过会自动出现滚动条 minLines: 22, // 最小行数,还未到最大行数时,编辑器会自动伸缩大小 fontSize: 14, // 编辑器内字体大小 theme: this.themePath, // 默认设置的主题 mode: this.modePath, // 默认设置的语言模式 tabSize: 4, // 制表符设置为 4 个空格大小 readOnly: false, //只读 wrap: true, highlightActiveLine: true, value: this.codeValue }); marked.setOptions({ renderer: renderer, // highlight: function (code) { // return highlight.highlightAuto(code).value; // }, gfm: true,//默认为true。 允许 Git Hub标准的markdown. tables: true,//默认为true。 允许支持表格语法。该选项要求 gfm 为true。 breaks: false,//默认为false。 允许回车换行。该选项要求 gfm 为true。 pedantic: false,//默认为false。 尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。 // sanitize: false,//对输出进行过滤(清理) 不支持了,用sanitizer 或者直接渲染的时候过滤 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//使用更为时髦的标点,比如在引用语法中加入破折号。 }); // this.aceEditor.session.on('change', this.show); // let that = this; // this.aceEditor.commands.addCommand({ // name: '复制', // bindKey: {win: 'Ctrl-C', mac: 'Command-M'}, // exec: function(editor) { // that.$message.success("复制成功"); // } // }); // this.aceEditor.commands.addCommand({ // name: '粘贴', // bindKey: {win: 'Ctrl-V', mac: 'Command-M'}, // exec: function(editor) { // that.$message.success("粘贴成功"); // } // }); }, watch: { value(newVal) { console.log(newVal); this.aceEditor.setValue(newVal); } } } </script> <style scoped lang="scss"> .toolbar { cursor: pointer;//鼠标手型 } .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;//鼠标手型 } .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;//鼠标手型 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>
这次的代码同样需要在引用时绑定value,也就是编辑框里的内容
1<MarkdownEditor v-bind:value="''"></MarkdownEditor>
哦,对了,忘记讲一些东西了。关于代码块高亮以及latex渲染的问题。
高亮使用的是highlight.js,marked是支持这个库的,直接使用就行,它能自动识别语言,要是不想调用那个函数,你也可以自行判断用户会使用到的语言。主题的使用,需要引用包下style对应的css。还有一个最重要的就是渲染的标签必须要有class为hljs的属性,不然你只能看到代码是高亮的。至于class属性怎么添加,如果你没有letax需求,那么只需要在渲染的时候套一层标签,它的class属性是这个即可。
剩下的就是latex了,因为marked本身是不支持latex的,但是它支持重写render函数,通过这一方法来实现对latex的支持,在这里我使用的是katex,感兴趣的小老板可以试试mathjax。不过有一个不太好的地方就是数学公式需要被代码块包住,即$a * b$
。不过这都不是大问题,能好好渲染才是王道。
好了,本次的分享就到此为止吧,see you again~
到此这篇关于基于Ace的Markdown编辑器的文章就介绍到这了,更多相关Ace Markdown编辑器内容请搜索靠谱客以前的文章或继续浏览下面的相关文章希望大家以后多多支持靠谱客!
最后
以上就是开放云朵最近收集整理的关于分享一个基于Ace的Markdown编辑器的全部内容,更多相关分享一个基于Ace内容请搜索靠谱客的其他文章。
发表评论 取消回复