vditor 3.11.2 → 3.11.3

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.
Files changed (47) hide show
  1. package/README.md +11 -1
  2. package/dist/css/content-theme/ant-design.css +10 -0
  3. package/dist/css/content-theme/dark.css +10 -0
  4. package/dist/css/content-theme/light.css +13 -0
  5. package/dist/css/content-theme/wechat.css +10 -0
  6. package/dist/index.css +133 -4
  7. package/dist/index.js +587 -36
  8. package/dist/index.min.js +1 -1
  9. package/dist/js/i18n/ja_JP.js +2 -2
  10. package/dist/js/i18n/zh_TW.js +8 -8
  11. package/dist/js/lute/lute.min.js +75 -60
  12. package/dist/js/mermaid/mermaid.min.js +2027 -1047
  13. package/dist/js/wavedrom/LICENSE.WaveDrom +21 -0
  14. package/dist/js/wavedrom/wavedrom.min.js +9 -0
  15. package/dist/method.d.ts +2 -0
  16. package/dist/method.js +311 -6
  17. package/dist/method.min.js +1 -1
  18. package/dist/ts/constants.d.ts +4 -0
  19. package/dist/ts/markdown/adapterRender.d.ts +4 -0
  20. package/dist/ts/markdown/imageCaptionRender.d.ts +2 -0
  21. package/dist/ts/markdown/wavedromRender.d.ts +1 -0
  22. package/dist/ts/util/fixBrowserBehavior.d.ts +1 -0
  23. package/dist/types/index.d.ts +15 -0
  24. package/package.json +7 -4
  25. package/src/assets/less/_reset.less +103 -2
  26. package/src/assets/less/index.less +20 -0
  27. package/src/index.ts +3 -0
  28. package/src/method.ts +3 -0
  29. package/src/ts/constants.ts +5 -1
  30. package/src/ts/export/index.ts +2 -0
  31. package/src/ts/ir/index.ts +26 -0
  32. package/src/ts/ir/process.ts +24 -1
  33. package/src/ts/markdown/adapterRender.ts +4 -0
  34. package/src/ts/markdown/getHTML.ts +6 -3
  35. package/src/ts/markdown/imageCaptionRender.ts +175 -0
  36. package/src/ts/markdown/mermaidRender.ts +1 -1
  37. package/src/ts/markdown/previewRender.ts +7 -1
  38. package/src/ts/markdown/setLute.ts +3 -0
  39. package/src/ts/markdown/wavedromRender.ts +54 -0
  40. package/src/ts/preview/index.ts +5 -0
  41. package/src/ts/util/Options.ts +1 -1
  42. package/src/ts/util/fixBrowserBehavior.ts +91 -15
  43. package/src/ts/util/processCode.ts +3 -0
  44. package/src/ts/wysiwyg/afterRenderEvent.ts +2 -0
  45. package/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts +5 -1
  46. package/src/ts/wysiwyg/input.ts +5 -0
  47. package/src/ts/wysiwyg/setHeading.ts +110 -6
@@ -453,6 +453,41 @@ export const execAfterRender = (vditor: IVditor, options = {
453
453
  }
454
454
  };
455
455
 
456
+ const isEmptyListItem = (liElement: HTMLElement) => {
457
+ if (liElement.textContent.split(Constants.ZWSP).join("").trim() !== "") {
458
+ return false;
459
+ }
460
+ return !liElement.querySelector([
461
+ "audio", "canvas", "embed", "hr", "iframe", "img", "object", "ol", "pre", "svg", "table", "ul", "video",
462
+ "input:not([type='checkbox'])",
463
+ ].join(", "));
464
+ };
465
+
466
+ export const exitEmptyListItem = (liElement: HTMLElement) => {
467
+ const listElement = liElement.parentElement;
468
+ const listParent = listElement.parentElement;
469
+ const paragraphElement = document.createElement("p");
470
+ paragraphElement.setAttribute("data-block", "0");
471
+ paragraphElement.innerHTML = "<wbr>";
472
+
473
+ let trailingListElement: HTMLElement | undefined;
474
+ if (liElement.nextElementSibling) {
475
+ trailingListElement = listElement.cloneNode(false) as HTMLElement;
476
+ while (liElement.nextElementSibling) {
477
+ trailingListElement.appendChild(liElement.nextElementSibling);
478
+ }
479
+ listParent.insertBefore(trailingListElement, listElement.nextSibling);
480
+ }
481
+
482
+ liElement.remove();
483
+ if (listElement.childElementCount === 0) {
484
+ listParent.replaceChild(paragraphElement, listElement);
485
+ } else {
486
+ listParent.insertBefore(paragraphElement, trailingListElement || listElement.nextSibling);
487
+ }
488
+ return paragraphElement;
489
+ };
490
+
456
491
  export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | false, event: KeyboardEvent) => {
457
492
  const startContainer = range.startContainer;
458
493
  const liElement = hasClosestByMatchTag(startContainer, "LI");
@@ -471,6 +506,28 @@ export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | f
471
506
  return true;
472
507
  }
473
508
 
509
+ // 嵌套列表末尾的空列表项回车后转为父列表项中的段落
510
+ // https://github.com/Vanessa219/vditor/issues/939
511
+ if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && range.collapsed &&
512
+ !liElement.nextElementSibling && liElement.parentElement.parentElement.tagName === "LI" &&
513
+ isEmptyListItem(liElement)) {
514
+ const paragraphElement = exitEmptyListItem(liElement);
515
+ setRangeByWbr(paragraphElement, range);
516
+ execAfterRender(vditor);
517
+ event.preventDefault();
518
+ return true;
519
+ }
520
+
521
+ // 空列表项删除标记后退出当前列表
522
+ if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
523
+ liElement.textContent.trim().replace(Constants.ZWSP, "") === "" && range.toString() === "") {
524
+ const paragraphElement = exitEmptyListItem(liElement);
525
+ setRangeByWbr(paragraphElement, range);
526
+ execAfterRender(vditor);
527
+ event.preventDefault();
528
+ return true;
529
+ }
530
+
474
531
  if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
475
532
  !liElement.previousElementSibling && range.toString() === "" &&
476
533
  getSelectPosition(liElement, vditor[vditor.currentMode].element, range).start === 0) {
@@ -488,20 +545,6 @@ export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | f
488
545
  return true;
489
546
  }
490
547
 
491
- // 空列表删除后与上一级段落对齐
492
- if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
493
- liElement.textContent.trim().replace(Constants.ZWSP, "") === "" &&
494
- range.toString() === "" && liElement.previousElementSibling?.tagName === "LI") {
495
- liElement.previousElementSibling.insertAdjacentText("beforeend", "\n\n");
496
- range.selectNodeContents(liElement.previousElementSibling);
497
- range.collapse(false);
498
- liElement.remove();
499
- setRangeByWbr(vditor[vditor.currentMode].element, range);
500
- execAfterRender(vditor);
501
- event.preventDefault();
502
- return true;
503
- }
504
-
505
548
  if (!isCtrl(event) && !event.altKey && event.key === "Tab") {
506
549
  // 光标位于第一/零字符时,tab 用于列表的缩进
507
550
  let isFirst = false;
@@ -1021,6 +1064,27 @@ export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEven
1021
1064
  const startContainer = range.startContainer;
1022
1065
  const blockquoteElement = hasClosestByMatchTag(startContainer, "BLOCKQUOTE");
1023
1066
  if (blockquoteElement && range.toString() === "") {
1067
+ if (pElement && event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey &&
1068
+ pElement.parentElement.isSameNode(blockquoteElement) &&
1069
+ hasClosestByMatchTag(blockquoteElement.parentElement, "LI") &&
1070
+ (range.collapsed || !range.cloneContents().firstElementChild) &&
1071
+ pElement.innerHTML.replace(Constants.ZWSP, "") !== "\n" &&
1072
+ pElement.innerHTML.replace(Constants.ZWSP, "") !== "") {
1073
+ // 列表内的引用由浏览器处理回车时会新建列表项 https://github.com/Vanessa219/vditor/issues/1925
1074
+ const trailingRange = range.cloneRange();
1075
+ trailingRange.setEnd(pElement, pElement.childNodes.length);
1076
+ const paragraphElement = document.createElement("p");
1077
+ paragraphElement.setAttribute("data-block", "0");
1078
+ paragraphElement.appendChild(trailingRange.extractContents());
1079
+ pElement.insertAdjacentElement("afterend", paragraphElement);
1080
+ range.setStart(paragraphElement, 0);
1081
+ range.collapse(true);
1082
+ setSelectionFocus(range);
1083
+ execAfterRender(vditor);
1084
+ event.preventDefault();
1085
+ return true;
1086
+ }
1087
+
1024
1088
  if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey &&
1025
1089
  getSelectPosition(blockquoteElement, vditor[vditor.currentMode].element, range).start === 0) {
1026
1090
  // Backspace: 光标位于引用中的第零个字符,仅删除引用标签
@@ -1068,9 +1132,15 @@ export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEven
1068
1132
  return true;
1069
1133
  }
1070
1134
 
1071
- if (insertAfterBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
1135
+ const itemElement = blockquoteElement.parentElement.tagName === "LI" ? blockquoteElement.parentElement : null;
1136
+ const afterBlockElement = !blockquoteElement.nextElementSibling && itemElement?.nextElementSibling?.tagName === "LI" ?
1137
+ itemElement : blockquoteElement;
1138
+ if (insertAfterBlock(vditor, event, range, blockquoteElement, afterBlockElement)) {
1072
1139
  return true;
1073
1140
  }
1141
+ if (event.key === "ArrowUp" && itemElement && !blockquoteElement.previousElementSibling) {
1142
+ return false;
1143
+ }
1074
1144
  if (insertBeforeBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
1075
1145
  return true;
1076
1146
  }
@@ -1290,6 +1360,12 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1290
1360
  return ["", Lute.WalkContinue];
1291
1361
  }
1292
1362
 
1363
+ const parent = node.__internal_object__.Parent;
1364
+ // 链接引用的目标地址由定义节点提供,不在引用位置重复渲染
1365
+ if (parent.Type === 33 && parent.LinkType === 3) {
1366
+ return ["", Lute.WalkContinue];
1367
+ }
1368
+
1293
1369
  if (vditor.options.upload.renderLinkDest) {
1294
1370
  return vditor.options.upload.renderLinkDest(vditor, node, entering);
1295
1371
  }
@@ -10,6 +10,7 @@ import {markmapRender} from "../markdown/markmapRender";
10
10
  import {mindmapRender} from "../markdown/mindmapRender";
11
11
  import {plantumlRender} from "../markdown/plantumlRender";
12
12
  import {SMILESRender} from "../markdown/SMILESRender";
13
+ import {wavedromRender} from "../markdown/wavedromRender";
13
14
 
14
15
  export const processPasteCode = (html: string, text: string, type = "sv") => {
15
16
  const tempElement = document.createElement("div");
@@ -82,6 +83,8 @@ export const processCodeRender = (previewPanel: HTMLElement, vditor: IVditor) =>
82
83
  plantumlRender(previewPanel, vditor.options.cdn);
83
84
  } else if (language === "graphviz") {
84
85
  graphvizRender(previewPanel, vditor.options.cdn);
86
+ } else if (language === "wavedrom") {
87
+ wavedromRender(previewPanel, vditor.options.cdn);
85
88
  } else if (language === "math") {
86
89
  mathRender(previewPanel, {cdn: vditor.options.cdn, math: vditor.options.preview.math});
87
90
  } else {
@@ -1,4 +1,5 @@
1
1
  import {getMarkdown} from "../markdown/getMarkdown";
2
+ import {renderImageCaptions} from "../markdown/imageCaptionRender";
2
3
  import {accessLocalStorage} from "../util/compatibility";
3
4
 
4
5
  export const afterRenderEvent = (vditor: IVditor, options = {
@@ -6,6 +7,7 @@ export const afterRenderEvent = (vditor: IVditor, options = {
6
7
  enableHint: false,
7
8
  enableInput: true,
8
9
  }) => {
10
+ renderImageCaptions(vditor.wysiwyg.element, "wysiwyg", vditor.options.preview.markdown.imageCaption);
9
11
  if (options.enableHint) {
10
12
  vditor.hint.render(vditor);
11
13
  }
@@ -33,6 +33,7 @@ import {afterRenderEvent} from "./afterRenderEvent";
33
33
  import {removeBlockElement} from "./processKeydown";
34
34
  import {renderToc} from "../util/toc";
35
35
  import {getMarkdown} from "../markdown/getMarkdown";
36
+ import {renderImageCaptions} from "../markdown/imageCaptionRender";
36
37
 
37
38
  export const highlightToolbarWYSIWYG = (vditor: IVditor) => {
38
39
  clearTimeout(vditor.wysiwyg.hlToolbarTimeoutId);
@@ -1079,6 +1080,7 @@ export const genImagePopover = (event: Event, vditor: IVditor) => {
1079
1080
  imgElement.setAttribute("src", inputElement.value);
1080
1081
  imgElement.setAttribute("alt", alt.value);
1081
1082
  imgElement.setAttribute("title", title.value);
1083
+ renderImageCaptions(vditor.wysiwyg.element, "wysiwyg", vditor.options.preview.markdown.imageCaption);
1082
1084
  if (typeof vditor.options.input === "function") {
1083
1085
  vditor.options.input(getMarkdown(vditor));
1084
1086
  }
@@ -1150,5 +1152,7 @@ const focusToElement = (event: KeyboardEvent, range: Range) => {
1150
1152
  };
1151
1153
 
1152
1154
  const customWysiwygToolbar = (vditor: IVditor, type: TWYSISYGToolbar) => {
1153
- vditor.options.customWysiwygToolbar(type, vditor.wysiwyg.popover);
1155
+ if (vditor.options.customWysiwygToolbar) {
1156
+ vditor.options.customWysiwygToolbar(type, vditor.wysiwyg.popover);
1157
+ }
1154
1158
  };
@@ -71,6 +71,7 @@ export const input = (vditor: IVditor, range: Range, event?: InputEvent) => {
71
71
 
72
72
  const isWYSIWYGElement = blockElement.isEqualNode(vditor.wysiwyg.element);
73
73
  const footnoteElement = hasClosestByAttribute(blockElement, "data-type", "footnotes-block");
74
+ const calloutElement = hasClosestByAttribute(range.startContainer, "data-type", "callout");
74
75
 
75
76
  if (!isWYSIWYGElement) {
76
77
  // 列表需要到最顶层
@@ -90,6 +91,10 @@ export const input = (vditor: IVditor, range: Range, event?: InputEvent) => {
90
91
  blockElement = footnoteElement;
91
92
  }
92
93
 
94
+ if (calloutElement) {
95
+ blockElement = calloutElement;
96
+ }
97
+
93
98
  html = blockElement.outerHTML;
94
99
 
95
100
  if (blockElement.tagName === "UL" || blockElement.tagName === "OL") {
@@ -1,7 +1,94 @@
1
- import {hasClosestBlock} from "../util/hasClosest";
1
+ import {hasClosestBlock, hasClosestByMatchTag} from "../util/hasClosest";
2
2
  import {getEditorRange, setRangeByWbr} from "../util/selection";
3
3
  import {renderToc} from "../util/toc";
4
4
 
5
+ const createBlockElement = (element: HTMLElement, tagName: string) => {
6
+ const blockElement = element.ownerDocument.createElement(tagName);
7
+ blockElement.setAttribute("data-block", "0");
8
+ return blockElement;
9
+ };
10
+
11
+ const replaceBlockElement = (element: HTMLElement, tagName: string) => {
12
+ const blockElement = createBlockElement(element, tagName);
13
+ while (element.firstChild) {
14
+ blockElement.appendChild(element.firstChild);
15
+ }
16
+ element.replaceWith(blockElement);
17
+ return blockElement;
18
+ };
19
+
20
+ const isListItemBlock = (node: Node) => {
21
+ if (node.nodeType !== 1) {
22
+ return false;
23
+ }
24
+ const element = node as HTMLElement;
25
+ return element.getAttribute("data-block") === "0" || element.tagName === "INPUT";
26
+ };
27
+
28
+ const getListItemInlineNodes = (itemElement: HTMLElement, wbrElement: HTMLElement) => {
29
+ let anchorNode: Node = wbrElement;
30
+ while (anchorNode.parentNode && !anchorNode.parentNode.isSameNode(itemElement)) {
31
+ anchorNode = anchorNode.parentNode;
32
+ }
33
+ if (!anchorNode.parentNode?.isSameNode(itemElement)) {
34
+ return [];
35
+ }
36
+
37
+ let firstNode = anchorNode;
38
+ let lastNode = anchorNode;
39
+ while (firstNode.previousSibling && !isListItemBlock(firstNode.previousSibling)) {
40
+ firstNode = firstNode.previousSibling;
41
+ }
42
+ while (lastNode.nextSibling && !isListItemBlock(lastNode.nextSibling)) {
43
+ lastNode = lastNode.nextSibling;
44
+ }
45
+
46
+ // 任务列表的复选框和正文之间需要保留分隔空格。
47
+ if (firstNode.previousSibling instanceof HTMLInputElement && firstNode.nodeType === 3) {
48
+ const leadingWhitespace = firstNode.textContent.match(/^\s+/)?.[0];
49
+ if (leadingWhitespace) {
50
+ firstNode.textContent = firstNode.textContent.substring(leadingWhitespace.length);
51
+ itemElement.insertBefore(itemElement.ownerDocument.createTextNode(leadingWhitespace), firstNode);
52
+ }
53
+ }
54
+
55
+ const nodes: Node[] = [];
56
+ let node: Node = firstNode;
57
+ while (node) {
58
+ nodes.push(node);
59
+ if (node.isSameNode(lastNode)) {
60
+ break;
61
+ }
62
+ node = node.nextSibling;
63
+ }
64
+ return nodes;
65
+ };
66
+
67
+ const setListItemHeading = (itemElement: HTMLElement, wbrElement: HTMLElement, tagName: string) => {
68
+ const inlineNodes = getListItemInlineNodes(itemElement, wbrElement);
69
+ if (inlineNodes.length === 0) {
70
+ return false;
71
+ }
72
+
73
+ const headingElement = createBlockElement(itemElement, tagName);
74
+ itemElement.insertBefore(headingElement, inlineNodes[0]);
75
+ inlineNodes.forEach((node) => {
76
+ headingElement.appendChild(node);
77
+ });
78
+ if (headingElement.innerHTML.trim() === "<wbr>") {
79
+ headingElement.appendChild(itemElement.ownerDocument.createElement("br"));
80
+ }
81
+ return true;
82
+ };
83
+
84
+ const unwrapBlockElement = (element: HTMLElement) => {
85
+ const fragment = element.ownerDocument.createDocumentFragment();
86
+ while (element.firstChild) {
87
+ fragment.appendChild(element.firstChild);
88
+ }
89
+ element.replaceWith(fragment);
90
+ };
91
+
5
92
  export const setHeading = (vditor: IVditor, tagName: string) => {
6
93
  const range = getEditorRange(vditor);
7
94
  let blockElement = hasClosestBlock(range.startContainer);
@@ -12,16 +99,27 @@ export const setHeading = (vditor: IVditor, tagName: string) => {
12
99
  blockElement = vditor.wysiwyg.element;
13
100
  }
14
101
  if (blockElement && !blockElement.classList.contains("vditor-wysiwyg__block")) {
15
- range.insertNode(document.createElement("wbr"));
102
+ const wbrElement = document.createElement("wbr");
103
+ range.insertNode(wbrElement);
16
104
  // Firefox 需要 trim https://github.com/Vanessa219/vditor/issues/207
17
105
  if (blockElement.innerHTML.trim() === "<wbr>") {
18
106
  // Firefox 光标对不齐 https://github.com/Vanessa219/vditor/issues/199 1
19
107
  blockElement.innerHTML = "<wbr><br>";
20
108
  }
21
- if (blockElement.tagName === "BLOCKQUOTE" || blockElement.classList.contains("vditor-reset")) {
22
- blockElement.innerHTML = `<${tagName} data-block="0">${blockElement.innerHTML.trim()}</${tagName}>`;
109
+ const itemElement = hasClosestByMatchTag(wbrElement, "LI");
110
+ if (itemElement && blockElement.isSameNode(itemElement.parentElement)) {
111
+ if (!setListItemHeading(itemElement, wbrElement, tagName)) {
112
+ wbrElement.remove();
113
+ return;
114
+ }
115
+ } else if (blockElement.tagName === "BLOCKQUOTE" || blockElement.classList.contains("vditor-reset")) {
116
+ const headingElement = createBlockElement(blockElement, tagName);
117
+ while (blockElement.firstChild) {
118
+ headingElement.appendChild(blockElement.firstChild);
119
+ }
120
+ blockElement.appendChild(headingElement);
23
121
  } else {
24
- blockElement.outerHTML = `<${tagName} data-block="0">${blockElement.innerHTML.trim()}</${tagName}>`;
122
+ replaceBlockElement(blockElement, tagName);
25
123
  }
26
124
  setRangeByWbr(vditor.wysiwyg.element, range);
27
125
  renderToc(vditor);
@@ -36,8 +134,14 @@ export const removeHeading = (vditor: IVditor) => {
36
134
  }
37
135
  if (blockElement) {
38
136
  range.insertNode(document.createElement("wbr"));
39
- blockElement.outerHTML = `<p data-block="0">${blockElement.innerHTML}</p>`;
137
+ if (blockElement.parentElement?.tagName === "LI" &&
138
+ blockElement.parentElement.parentElement?.getAttribute("data-tight") === "true") {
139
+ unwrapBlockElement(blockElement);
140
+ } else {
141
+ replaceBlockElement(blockElement, "p");
142
+ }
40
143
  setRangeByWbr(vditor.wysiwyg.element, range);
144
+ renderToc(vditor);
41
145
  }
42
146
  vditor.wysiwyg.popover.style.display = "none";
43
147
  };