@jx3box/jx3box-editor 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,6 +32,7 @@
32
32
  "js-base64": "^3.6.1",
33
33
  "katex": "^0.16.4",
34
34
  "lodash": "^4.17.21",
35
+ "lucide": "^0.577.0",
35
36
  "photoswipe": "^4.1.2",
36
37
  "sanitize-html": "^2.17.0",
37
38
  "sortablejs": "^1.15.0",
package/src/Article.vue CHANGED
@@ -8,12 +8,19 @@
8
8
  v-for="(text, i) in data"
9
9
  :key="i"
10
10
  v-html="text"
11
- :class="{ on: i == page - 1 || all == true }"
11
+ :class="{ on: i == page - 1 || all == true, 'markdown-body': isMarkdownMode }"
12
12
  :id="'c-article-part' + ~~(i + 1)"
13
13
  ></div>
14
14
  </div>
15
15
 
16
- <div id="c-article" class="c-article" ref="article" v-else-if="data && data.length" v-html="data[0]"></div>
16
+ <div
17
+ id="c-article"
18
+ class="c-article"
19
+ ref="article"
20
+ v-else-if="data && data.length"
21
+ v-html="data[0]"
22
+ :class="{ 'markdown-body': isMarkdownMode }"
23
+ ></div>
17
24
 
18
25
  <el-button class="c-article-all" type="primary" v-if="!all && hasPages" @click="showAll">加载全部</el-button>
19
26
 
@@ -48,6 +55,8 @@
48
55
 
49
56
  <script>
50
57
  import $ from "jquery";
58
+ import Vditor from "vditor";
59
+ import "github-markdown-css/github-markdown-light.css";
51
60
 
52
61
  // XSS
53
62
  import execFilterXSS from "./assets/js/xss";
@@ -57,6 +66,7 @@ import execLazyload from "./assets/js/img";
57
66
  import execFilterIframe from "./assets/js/iframe";
58
67
  import execFilterLink from "./assets/js/a";
59
68
  import execSplitPages from "./assets/js/nextpage";
69
+ import normalizeMarkdownForVditor from "./assets/js/normalizeMarkdownForVditor";
60
70
 
61
71
  // 扩展文本
62
72
  import renderFoldBlock from "./assets/js/fold";
@@ -85,8 +95,7 @@ import renderJx3Element from "./assets/js/jx3_element";
85
95
  export default {
86
96
  name: "Article",
87
97
  props: {
88
-
89
- post_mode : {
98
+ post_mode: {
90
99
  type: String,
91
100
  default: "tinymce",
92
101
  },
@@ -134,6 +143,7 @@ export default {
134
143
  page: 1,
135
144
  data: [],
136
145
  mode: "",
146
+ renderVersion: 0,
137
147
 
138
148
  // 画廊
139
149
  gallery_index: null,
@@ -180,14 +190,17 @@ export default {
180
190
  },
181
191
  computed: {
182
192
  total: function () {
183
- return this.chunks.length;
193
+ return this.data.length;
184
194
  },
185
195
  hasPages: function () {
186
- return this.chunks.length > 1;
196
+ return this.data.length > 1;
187
197
  },
188
198
  origin: function () {
189
199
  return this.content;
190
200
  },
201
+ isMarkdownMode: function () {
202
+ return ["markdown", "md", "vditor"].includes(String(this.post_mode || "").toLowerCase());
203
+ },
191
204
  chunks: function () {
192
205
  return this.pageable ? execSplitPages(this.origin) : [this.origin];
193
206
  },
@@ -195,9 +208,7 @@ export default {
195
208
  methods: {
196
209
  getHeaderHeight: function () {
197
210
  // 页面上可能没有这些元素,取存在的第一个:.c-header 优先,其次 .c-breadcrumb
198
- const el =
199
- document.querySelector(".c-header") ||
200
- document.querySelector(".c-breadcrumb");
211
+ const el = document.querySelector(".c-header") || document.querySelector(".c-breadcrumb");
201
212
  if (!el) return 0;
202
213
  const rect = el.getBoundingClientRect && el.getBoundingClientRect();
203
214
  const h = (rect && rect.height) || el.offsetHeight || 0;
@@ -232,6 +243,34 @@ export default {
232
243
  return "";
233
244
  }
234
245
  },
246
+ renderMarkdownChunk: async function (chunk) {
247
+ const temp = document.createElement("div");
248
+ const normalizedChunk = normalizeMarkdownForVditor(chunk);
249
+
250
+ await Vditor.preview(temp, normalizedChunk, {
251
+ mode: "light",
252
+ lang: "zh_CN",
253
+ hljs: {
254
+ enable: true,
255
+ lineNumber: false,
256
+ style: "github",
257
+ },
258
+ markdown: {
259
+ mark: true,
260
+ sanitize: true,
261
+ toc: true,
262
+ },
263
+ icon: "ant",
264
+ });
265
+
266
+ return temp.innerHTML;
267
+ },
268
+ renderMarkdown: async function () {
269
+ const html = await this.renderMarkdownChunk(this.origin);
270
+ const chunks = this.pageable ? execSplitPages(html) : [html];
271
+
272
+ return chunks.map((chunk) => this.doReg(chunk));
273
+ },
235
274
  doDOM: function ($root) {
236
275
  // 折叠块
237
276
  renderFoldBlock($root);
@@ -305,16 +344,27 @@ export default {
305
344
  this.doDir();
306
345
  });
307
346
  },
308
- render: function () {
347
+ render: async function () {
348
+ const version = ++this.renderVersion;
309
349
  let result = [];
310
- for (let chunk of this.chunks) {
311
- let _chunk = this.doReg(chunk);
312
- result.push(_chunk);
350
+
351
+ if (this.isMarkdownMode) {
352
+ result = await this.renderMarkdown();
353
+ } else {
354
+ for (let chunk of this.chunks) {
355
+ let _chunk = this.doReg(chunk);
356
+ result.push(_chunk);
357
+ }
313
358
  }
359
+
360
+ if (version !== this.renderVersion) return;
314
361
  this.data = result;
315
362
  },
316
- run: function () {
317
- this.render();
363
+ run: async function () {
364
+ this.page = 1;
365
+ this.all = false;
366
+ await this.render();
367
+ if (!this.data.length) return;
318
368
 
319
369
  // 等待html加载完毕后
320
370
  this.$nextTick(() => {
@@ -334,6 +384,9 @@ export default {
334
384
  content: function () {
335
385
  this.run();
336
386
  },
387
+ post_mode: function () {
388
+ this.run();
389
+ },
337
390
  },
338
391
  mounted: function () {
339
392
  const params = new URLSearchParams(location.search);
package/src/Markdown.vue CHANGED
@@ -1,19 +1,7 @@
1
1
  <template>
2
- <div class="c-editor-markdown">
2
+ <div class="c-editor-markdown" :class="{ 'c-editor-markdown--preview': previewVisible }">
3
3
  <slot name="prepend"></slot>
4
4
 
5
- <div class="c-editor-markdown__toolbar">
6
- <div class="c-editor-markdown__toolbar-left">
7
- <span class="c-editor-markdown__toolbar-label">编辑模式</span>
8
- <el-radio-group v-model="editorMode" size="small">
9
- <el-radio-button v-for="item in modeOptions" :key="item.value" :value="item.value">
10
- {{ item.label }}
11
- </el-radio-button>
12
- </el-radio-group>
13
- </div>
14
- <div class="c-editor-markdown__toolbar-tip">GitHub 样式预览,支持 Ctrl+V 粘贴图片上传</div>
15
- </div>
16
-
17
5
  <slot></slot>
18
6
 
19
7
  <div ref="editorHost" class="c-editor-markdown__host"></div>
@@ -25,17 +13,82 @@
25
13
  <script>
26
14
  import axios from "axios";
27
15
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
16
+ import {
17
+ Bold,
18
+ Code2,
19
+ Expand,
20
+ Eye,
21
+ EyeOff,
22
+ Heading,
23
+ Italic,
24
+ Link2,
25
+ List,
26
+ ListChecks,
27
+ ListOrdered,
28
+ Quote,
29
+ Redo2,
30
+ SquareCode,
31
+ Strikethrough,
32
+ TableProperties,
33
+ Undo2,
34
+ } from "lucide";
28
35
  import Vditor from "vditor";
29
36
  import "vditor/dist/index.css";
30
- import "github-markdown-css/github-markdown.css";
37
+ import "github-markdown-css/github-markdown-light.css";
38
+ import normalizeMarkdownForVditor from "./assets/js/normalizeMarkdownForVditor";
31
39
 
32
40
  const { __cms } = JX3BOX;
33
41
  const UPLOAD_API = `${__cms}api/cms/upload`;
34
- const MODE_OPTIONS = [
35
- { label: "所见即所得", value: "wysiwyg" },
36
- { label: "即时渲染", value: "ir" },
37
- { label: "分屏预览", value: "sv" },
38
- ];
42
+ const TOOLBAR_ICON_SIZE = 16;
43
+ const STATIC_TOOLBAR_ICONS = {
44
+ headings: Heading,
45
+ bold: Bold,
46
+ italic: Italic,
47
+ strike: Strikethrough,
48
+ link: Link2,
49
+ list: List,
50
+ "ordered-list": ListOrdered,
51
+ check: ListChecks,
52
+ quote: Quote,
53
+ code: Code2,
54
+ "inline-code": SquareCode,
55
+ table: TableProperties,
56
+ undo: Undo2,
57
+ redo: Redo2,
58
+ fullscreen: Expand,
59
+ };
60
+
61
+ function renderLucideIcon(iconNode) {
62
+ if (!iconNode) return "";
63
+
64
+ const attrs = {
65
+ xmlns: "http://www.w3.org/2000/svg",
66
+ width: TOOLBAR_ICON_SIZE,
67
+ height: TOOLBAR_ICON_SIZE,
68
+ viewBox: "0 0 24 24",
69
+ fill: "none",
70
+ stroke: "currentColor",
71
+ "stroke-width": "2",
72
+ "stroke-linecap": "round",
73
+ "stroke-linejoin": "round",
74
+ class: "c-editor-markdown__icon",
75
+ "aria-hidden": "true",
76
+ };
77
+ const svgAttrs = Object.entries(attrs)
78
+ .map(([key, value]) => `${key}="${value}"`)
79
+ .join(" ");
80
+ const content = iconNode
81
+ .map(([tag, tagAttrs]) => {
82
+ const elementAttrs = Object.entries(tagAttrs)
83
+ .map(([key, value]) => `${key}="${value}"`)
84
+ .join(" ");
85
+
86
+ return `<${tag} ${elementAttrs}></${tag}>`;
87
+ })
88
+ .join("");
89
+
90
+ return `<svg ${svgAttrs}>${content}</svg>`;
91
+ }
39
92
 
40
93
  export default {
41
94
  name: "Markdown",
@@ -74,10 +127,15 @@ export default {
74
127
  data: this.modelValue ?? this.content ?? "",
75
128
  editor: null,
76
129
  editorReady: false,
77
- editorMode: "ir",
78
- modeOptions: MODE_OPTIONS,
130
+ editorMode: "sv",
131
+ previewVisible: false,
79
132
  isRebuildingEditor: false,
80
133
  isUploadingImage: false,
134
+ previewRenderVersion: 0,
135
+ counterElement: null,
136
+ counterClickListener: null,
137
+ counterClickTimestamps: [],
138
+ pendingTip: "",
81
139
  };
82
140
  },
83
141
  watch: {
@@ -105,13 +163,6 @@ export default {
105
163
  this.applyEditableState();
106
164
  },
107
165
  },
108
- editorMode(nextMode, prevMode) {
109
- if (!this.editorReady || this.isRebuildingEditor || nextMode === prevMode) {
110
- return;
111
- }
112
-
113
- this.rebuildEditor(nextMode);
114
- },
115
166
  },
116
167
  mounted() {
117
168
  this.initEditor();
@@ -120,11 +171,28 @@ export default {
120
171
  this.destroyEditor();
121
172
  },
122
173
  methods: {
123
- getPreviewMode(mode) {
124
- return mode === "sv" ? "both" : "editor";
174
+ getPreviewMode() {
175
+ return this.previewVisible ? "both" : "editor";
176
+ },
177
+ getPreviewTip() {
178
+ return this.previewVisible ? "返回编辑" : "查看预览";
179
+ },
180
+ getToolbarIconMap() {
181
+ return {
182
+ ...STATIC_TOOLBAR_ICONS,
183
+ "toggle-preview": this.previewVisible ? EyeOff : Eye,
184
+ };
125
185
  },
126
186
  getToolbar() {
127
187
  return [
188
+ {
189
+ name: "toggle-preview",
190
+ icon: renderLucideIcon(Eye),
191
+ tip: this.getPreviewTip(),
192
+ tipPosition: "ne",
193
+ click: () => this.togglePreview(),
194
+ },
195
+ "|",
128
196
  "headings",
129
197
  "bold",
130
198
  "italic",
@@ -169,9 +237,11 @@ export default {
169
237
  style: "github",
170
238
  },
171
239
  markdown: {
240
+ mark: true,
172
241
  sanitize: true,
242
+ toc: true,
173
243
  },
174
- mode: this.getPreviewMode(this.editorMode),
244
+ mode: this.getPreviewMode(),
175
245
  },
176
246
  customWysiwygToolbar() {},
177
247
  toolbar: this.getToolbar(),
@@ -190,6 +260,10 @@ export default {
190
260
  this.editorReady = true;
191
261
  this.syncEditorValue(this.data);
192
262
  this.applyEditableState();
263
+ this.applyPreviewState();
264
+ this.syncToolbarState();
265
+ this.bindCounterShortcut();
266
+ this.showPendingTip();
193
267
  },
194
268
  input: (value) => {
195
269
  this.handleEditorInput(value);
@@ -204,6 +278,7 @@ export default {
204
278
  this.editor = new Vditor(host, this.buildEditorOptions(initialValue));
205
279
  },
206
280
  destroyEditor() {
281
+ this.unbindCounterShortcut();
207
282
  if (!this.editor) return;
208
283
 
209
284
  this.editor.destroy();
@@ -212,9 +287,11 @@ export default {
212
287
  },
213
288
  rebuildEditor() {
214
289
  const currentValue = this.editor?.getValue?.() ?? this.data ?? "";
290
+
215
291
  this.data = currentValue;
216
292
  this.isRebuildingEditor = true;
217
293
  this.destroyEditor();
294
+
218
295
  this.$nextTick(() => {
219
296
  this.initEditor(currentValue);
220
297
  this.isRebuildingEditor = false;
@@ -226,6 +303,141 @@ export default {
226
303
  const nextValue = value ?? "";
227
304
  if (this.editor.getValue() === nextValue) return;
228
305
  this.editor.setValue(nextValue);
306
+ if (this.previewVisible) {
307
+ this.renderPreviewContent();
308
+ }
309
+ },
310
+ getToolbarButton(name) {
311
+ return this.$refs.editorHost?.querySelector?.(`.vditor-toolbar button[data-type="${name}"]`) || null;
312
+ },
313
+ syncToolbarState() {
314
+ const iconMap = this.getToolbarIconMap();
315
+
316
+ Object.entries(iconMap).forEach(([name, iconNode]) => {
317
+ const button = this.getToolbarButton(name);
318
+ if (!button) return;
319
+
320
+ button.innerHTML = renderLucideIcon(iconNode);
321
+ });
322
+
323
+ const previewButton = this.getToolbarButton("toggle-preview");
324
+ if (previewButton) {
325
+ previewButton.setAttribute("aria-label", this.getPreviewTip());
326
+ previewButton.classList.toggle("vditor-menu--current", this.previewVisible);
327
+ }
328
+
329
+ this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar button[data-type]").forEach((button) => {
330
+ const isVisibleInPreview = ["toggle-preview", "fullscreen"].includes(button.dataset.type || "");
331
+
332
+ button.parentElement.style.display = this.previewVisible && !isVisibleInPreview ? "none" : "";
333
+ });
334
+ this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar .vditor-toolbar__divider").forEach((divider) => {
335
+ divider.style.display = this.previewVisible ? "none" : "";
336
+ });
337
+ },
338
+ syncEditorPanels() {
339
+ const host = this.$refs.editorHost;
340
+ const preview = host?.querySelector?.(".vditor-preview");
341
+ const sv = host?.querySelector?.(".vditor-sv");
342
+ const irWrapper = host?.querySelector?.(".vditor-ir");
343
+ const wysiwygWrapper = host?.querySelector?.(".vditor-wysiwyg");
344
+
345
+ if (preview) {
346
+ preview.style.display = this.previewVisible ? "block" : "none";
347
+ }
348
+ if (sv) {
349
+ sv.style.display = this.editorMode === "sv" && !this.previewVisible ? "block" : "none";
350
+ }
351
+ if (irWrapper) {
352
+ irWrapper.style.display = "none";
353
+ }
354
+ if (wysiwygWrapper) {
355
+ wysiwygWrapper.style.display = this.editorMode === "wysiwyg" && !this.previewVisible ? "block" : "none";
356
+ }
357
+ },
358
+ applyPreviewState() {
359
+ if (!this.editorReady || !this.editor) return;
360
+
361
+ this.syncEditorPanels();
362
+ if (this.previewVisible) {
363
+ this.renderPreviewContent();
364
+ }
365
+ },
366
+ renderPreviewContent: async function () {
367
+ if (!this.editorReady || !this.editor) return;
368
+
369
+ const previewRoot = this.$refs.editorHost?.querySelector?.(".vditor-preview");
370
+ const previewBody = previewRoot?.querySelector?.(".vditor-reset") || previewRoot;
371
+ if (!previewBody) return;
372
+
373
+ const version = ++this.previewRenderVersion;
374
+ const normalizedMarkdown = normalizeMarkdownForVditor(this.editor.getValue());
375
+
376
+ await Vditor.preview(previewBody, normalizedMarkdown, {
377
+ mode: "light",
378
+ lang: "zh_CN",
379
+ hljs: {
380
+ enable: true,
381
+ lineNumber: false,
382
+ style: "github",
383
+ },
384
+ markdown: {
385
+ mark: true,
386
+ sanitize: true,
387
+ toc: true,
388
+ },
389
+ icon: "ant",
390
+ });
391
+
392
+ if (version !== this.previewRenderVersion) return;
393
+ previewBody.classList.add("markdown-body");
394
+ },
395
+ togglePreview() {
396
+ this.previewVisible = !this.previewVisible;
397
+
398
+ if (!this.editorReady || !this.editor) return;
399
+
400
+ this.applyPreviewState();
401
+ this.syncToolbarState();
402
+ },
403
+ toggleSecretEditorMode() {
404
+ if (this.isRebuildingEditor) return;
405
+
406
+ this.editorMode = this.editorMode === "wysiwyg" ? "sv" : "wysiwyg";
407
+ this.pendingTip = this.editorMode === "wysiwyg" ? "已切换到所见即所得模式" : "已切换到 Markdown 源码模式";
408
+ this.rebuildEditor();
409
+ },
410
+ bindCounterShortcut() {
411
+ const counter = this.$refs.editorHost?.querySelector?.(".vditor-counter");
412
+ if (!counter || counter === this.counterElement) return;
413
+
414
+ this.unbindCounterShortcut();
415
+ this.counterClickListener = this.counterClickListener || (() => this.handleCounterClick());
416
+ this.counterElement = counter;
417
+ this.counterElement.addEventListener("click", this.counterClickListener);
418
+ },
419
+ unbindCounterShortcut() {
420
+ if (!this.counterElement) return;
421
+
422
+ this.counterElement.removeEventListener("click", this.counterClickListener);
423
+ this.counterElement = null;
424
+ },
425
+ handleCounterClick() {
426
+ const now = Date.now();
427
+
428
+ this.counterClickTimestamps = this.counterClickTimestamps.filter((time) => now - time <= 2000);
429
+ this.counterClickTimestamps.push(now);
430
+
431
+ if (this.counterClickTimestamps.length < 6) return;
432
+
433
+ this.counterClickTimestamps = [];
434
+ this.toggleSecretEditorMode();
435
+ },
436
+ showPendingTip() {
437
+ if (!this.pendingTip || !this.editor?.tip) return;
438
+
439
+ this.editor.tip(this.pendingTip, 1500);
440
+ this.pendingTip = "";
229
441
  },
230
442
  applyEditableState() {
231
443
  if (!this.editorReady || !this.editor) return;
@@ -329,28 +541,6 @@ export default {
329
541
  flex-direction: column;
330
542
  gap: 12px;
331
543
 
332
- &__toolbar {
333
- display: flex;
334
- align-items: center;
335
- justify-content: space-between;
336
- gap: 12px;
337
- flex-wrap: wrap;
338
- }
339
-
340
- &__toolbar-left {
341
- display: flex;
342
- align-items: center;
343
- gap: 12px;
344
- flex-wrap: wrap;
345
- }
346
-
347
- &__toolbar-label,
348
- &__toolbar-tip {
349
- font-size: 13px;
350
- color: #57606a;
351
- line-height: 1.4;
352
- }
353
-
354
544
  &__host {
355
545
  overflow: visible;
356
546
  border: 1px solid #dcdfe6;
@@ -358,8 +548,10 @@ export default {
358
548
  background-color: #ffffff;
359
549
  }
360
550
 
361
- .el-radio-group {
362
- box-shadow: none;
551
+ &__icon {
552
+ display: block;
553
+ width: 16px;
554
+ height: 16px;
363
555
  }
364
556
 
365
557
  .vditor {
@@ -369,10 +561,74 @@ export default {
369
561
  }
370
562
 
371
563
  .vditor-toolbar {
564
+ display: flex;
565
+ align-items: center;
372
566
  padding: 10px 12px;
373
567
  background: #f6f8fa;
374
568
  border-bottom: 1px solid #d8dee4;
375
569
  overflow: visible;
570
+
571
+ .vditor-toolbar__item {
572
+ margin-right: 2px;
573
+ }
574
+
575
+ .vditor-toolbar__divider {
576
+ margin: 0 4px;
577
+ }
578
+
579
+ .vditor-toolbar__item > .vditor-tooltipped {
580
+ display: inline-flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: 30px;
584
+ height: 30px;
585
+ padding: 0;
586
+ color: #4b5563;
587
+ }
588
+
589
+ .vditor-toolbar__item > .vditor-tooltipped svg {
590
+ fill: none !important;
591
+ stroke: currentColor !important;
592
+ stroke-width: 2 !important;
593
+ stroke-linecap: round;
594
+ stroke-linejoin: round;
595
+ width: 16px;
596
+ height: 16px;
597
+ overflow: visible;
598
+ }
599
+
600
+ .vditor-toolbar__item > .vditor-tooltipped svg * {
601
+ fill: none !important;
602
+ stroke: currentColor !important;
603
+ stroke-width: 2 !important;
604
+ stroke-linecap: round;
605
+ stroke-linejoin: round;
606
+ }
607
+
608
+ .vditor-panel button {
609
+ width: auto;
610
+ height: auto;
611
+ padding: 4px 8px;
612
+ color: inherit;
613
+ display: block;
614
+ line-height: 1.5;
615
+ text-align: left;
616
+ white-space: nowrap;
617
+ }
618
+ }
619
+
620
+ &--preview {
621
+ .vditor-sv,
622
+ .vditor-ir,
623
+ .vditor-wysiwyg,
624
+ .vditor-counter {
625
+ display: none !important;
626
+ }
627
+
628
+ .vditor-preview {
629
+ display: block !important;
630
+ width: 100%;
631
+ }
376
632
  }
377
633
 
378
634
  .vditor-reset {
@@ -400,9 +656,20 @@ export default {
400
656
  }
401
657
 
402
658
  .vditor-counter {
403
- padding: 8px 12px;
404
- border-top: 1px solid #d8dee4;
405
- background: #f6f8fa;
659
+ padding: 3px 8px;
660
+ border-top: 0;
661
+ background: rgba(27, 31, 35, 0.05);
662
+ border-radius: 3px;
663
+ line-height: 1.4;
664
+ margin: 0;
665
+ position: absolute;
666
+ right: 12px;
667
+
668
+ &::before,
669
+ &::after {
670
+ display: none !important;
671
+ content: none !important;
672
+ }
406
673
  }
407
674
 
408
675
  .vditor-preview__action {
@@ -414,11 +681,12 @@ export default {
414
681
  z-index: 30;
415
682
  }
416
683
 
417
- @media (max-width: 768px) {
418
- &__toolbar {
419
- align-items: flex-start;
420
- }
684
+ .vditor-menu--current {
685
+ color: #0969da;
686
+ background: #eaf2ff;
687
+ }
421
688
 
689
+ @media (max-width: 768px) {
422
690
  .vditor-reset.markdown-body,
423
691
  .vditor-sv__editor,
424
692
  .vditor-ir__marker,
@@ -0,0 +1,39 @@
1
+ const PROTECTED_SEGMENT_REGEXP = /(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`|\$\$[\s\S]*?\$\$|\$[^$\n]+\$)/g;
2
+
3
+ function replaceUnderline(text) {
4
+ return text.replace(/\+\+([^\n+][^\n]*?)\+\+/g, "<u>$1</u>");
5
+ }
6
+
7
+ function replaceSuperscript(text) {
8
+ return text.replace(
9
+ /(^|[^\w\\])([A-Za-z0-9)\]}\u4e00-\u9fa5]+)\^([^^\s][^^]*?)\^/g,
10
+ (match, prefix, base, value) => {
11
+ return `${prefix}${base}<sup>${value}</sup>`;
12
+ }
13
+ );
14
+ }
15
+
16
+ function replaceSubscript(text) {
17
+ return text.replace(/(^|[^\w\\])([A-Za-z0-9)\]}\u4e00-\u9fa5]+)~([^~\s][^~]*?)~/g, (match, prefix, base, value) => {
18
+ return `${prefix}${base}<sub>${value}</sub>`;
19
+ });
20
+ }
21
+
22
+ function normalizePlainMarkdown(text) {
23
+ return [replaceUnderline, replaceSuperscript, replaceSubscript].reduce((result, transform) => {
24
+ return transform(result);
25
+ }, text);
26
+ }
27
+
28
+ export default function normalizeMarkdownForVditor(markdown) {
29
+ const source = String(markdown || "");
30
+ if (!source) return "";
31
+
32
+ const segments = source.split(PROTECTED_SEGMENT_REGEXP);
33
+
34
+ return segments
35
+ .map((segment, index) => {
36
+ return index % 2 === 1 ? segment : normalizePlainMarkdown(segment);
37
+ })
38
+ .join("");
39
+ }
@@ -7,12 +7,28 @@ const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "
7
7
 
8
8
  const EXTRA_TAGS = [
9
9
  "img",
10
- "h1", "h2", "h3", "h4", "h5", "h6",
11
- "table", "thead", "tbody", "tr", "th", "td",
12
- "blockquote", "pre", "code", "hr",
13
- "video", "source",
14
- "iframe", "style",
15
- "colgroup", "col",
10
+ "h1",
11
+ "h2",
12
+ "h3",
13
+ "h4",
14
+ "h5",
15
+ "h6",
16
+ "table",
17
+ "thead",
18
+ "tbody",
19
+ "tr",
20
+ "th",
21
+ "td",
22
+ "blockquote",
23
+ "pre",
24
+ "code",
25
+ "hr",
26
+ "video",
27
+ "source",
28
+ "iframe",
29
+ "style",
30
+ "colgroup",
31
+ "col",
16
32
  ];
17
33
 
18
34
  // 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
@@ -116,6 +132,7 @@ export default function sanitizeRichText(html) {
116
132
  .filter((t) => !FORBID.has(t));
117
133
 
118
134
  return sanitizeHtml(html, {
135
+ allowVulnerableTags: true,
119
136
  disallowedTagsMode: "discard",
120
137
  allowedTags,
121
138
 
@@ -125,7 +142,18 @@ export default function sanitizeRichText(html) {
125
142
  img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
126
143
  video: ["controls", "width", "height", "class", "style"],
127
144
  source: ["src", "type"],
128
- iframe: ["src", "width", "height", "frameborder", "scrolling", "allowfullscreen", "sandbox", "referrerpolicy", "class", "style"],
145
+ iframe: [
146
+ "src",
147
+ "width",
148
+ "height",
149
+ "frameborder",
150
+ "scrolling",
151
+ "allowfullscreen",
152
+ "sandbox",
153
+ "referrerpolicy",
154
+ "class",
155
+ "style",
156
+ ],
129
157
  td: ["colspan", "rowspan", "align", "valign", "class", "style"],
130
158
  th: ["colspan", "rowspan", "align", "valign", "class", "style"],
131
159
  col: ["span", "width", "class", "style"],
@@ -4,7 +4,7 @@
4
4
  <el-segmented v-model="group" :options="groupOptions" />
5
5
  </div>
6
6
  <div class="container">
7
- <Article class="article" :content="content"></Article>
7
+ <Article class="article" :content="content" :post_mode="postMode"></Article>
8
8
  <div id="directory"></div>
9
9
  </div>
10
10
  </template>
@@ -29,6 +29,11 @@ export default {
29
29
  ],
30
30
  };
31
31
  },
32
+ computed: {
33
+ postMode() {
34
+ return this.group === "markdown" ? "markdown" : "tinymce";
35
+ },
36
+ },
32
37
  watch: {
33
38
  group: {
34
39
  immediate: true,
@@ -53,20 +58,19 @@ export default {
53
58
  .flex(x);
54
59
  margin-bottom: 1rem;
55
60
  }
56
- .container{
57
- #directory{
61
+ .container {
62
+ #directory {
58
63
  .w(200px);
59
64
  position: fixed;
60
- right:20px;
61
- top:20px;
65
+ right: 20px;
66
+ top: 20px;
62
67
  }
63
- .article{
68
+ .article {
64
69
  margin-right: 320px;
65
70
  }
66
-
67
71
  }
68
- body{
69
- min-height:2000px;
70
- padding:0 20px;
72
+ body {
73
+ min-height: 2000px;
74
+ padding: 0 20px;
71
75
  }
72
76
  </style>