cherry-muse 1.0.1

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 (226) hide show
  1. package/LICENSE +162 -0
  2. package/README.md +139 -0
  3. package/dist/addons/cherry-code-block-card-plugin.js +1 -0
  4. package/dist/addons/cherry-code-block-echarts-plugin.js +1 -0
  5. package/dist/addons/cherry-code-block-mermaid-plugin.js +1 -0
  6. package/dist/cherry-markdown.core.common.js +1 -0
  7. package/dist/cherry-markdown.core.js +1 -0
  8. package/dist/cherry-markdown.css +4089 -0
  9. package/dist/cherry-markdown.engine.core.common.js +1 -0
  10. package/dist/cherry-markdown.engine.core.esm.js +1 -0
  11. package/dist/cherry-markdown.engine.core.js +1 -0
  12. package/dist/cherry-markdown.esm.js +1 -0
  13. package/dist/cherry-markdown.js +70837 -0
  14. package/dist/cherry-markdown.js.map +1 -0
  15. package/dist/cherry-markdown.min.css +1 -0
  16. package/dist/cherry-markdown.min.js +1 -0
  17. package/dist/stats.html +4838 -0
  18. package/examples/scripts/pinyin/README.md +53 -0
  19. package/package.json +167 -0
  20. package/src/Cherry.config.js +411 -0
  21. package/src/Cherry.js +788 -0
  22. package/src/CherryStatic.js +70 -0
  23. package/src/Editor.js +746 -0
  24. package/src/Engine.js +334 -0
  25. package/src/Event.js +74 -0
  26. package/src/Factory.js +180 -0
  27. package/src/Logger.js +31 -0
  28. package/src/Previewer.js +1147 -0
  29. package/src/Sanitizer.js +4 -0
  30. package/src/Sanitizer.node.js +7 -0
  31. package/src/Stats.js +101 -0
  32. package/src/Theme.js +46 -0
  33. package/src/UrlCache.js +98 -0
  34. package/src/addons/cherry-code-block-card-plugin.js +213 -0
  35. package/src/addons/cherry-code-block-echarts-plugin.js +161 -0
  36. package/src/addons/cherry-code-block-mermaid-plugin.js +118 -0
  37. package/src/core/HookCenter.js +303 -0
  38. package/src/core/HooksConfig.js +106 -0
  39. package/src/core/ParagraphBase.js +314 -0
  40. package/src/core/SentenceBase.js +65 -0
  41. package/src/core/SyntaxBase.js +197 -0
  42. package/src/core/hooks/AutoLink.js +251 -0
  43. package/src/core/hooks/BackgroundColor.js +46 -0
  44. package/src/core/hooks/Badge.js +100 -0
  45. package/src/core/hooks/Blockquote.js +113 -0
  46. package/src/core/hooks/Br.js +85 -0
  47. package/src/core/hooks/CodeBlock.js +876 -0
  48. package/src/core/hooks/Color.js +78 -0
  49. package/src/core/hooks/CommentReference.js +96 -0
  50. package/src/core/hooks/Detail.js +138 -0
  51. package/src/core/hooks/Emoji.config.js +9388 -0
  52. package/src/core/hooks/Emoji.js +223 -0
  53. package/src/core/hooks/Emphasis.js +113 -0
  54. package/src/core/hooks/Footnote.js +125 -0
  55. package/src/core/hooks/FrontMatter.js +52 -0
  56. package/src/core/hooks/FrontMatterVars.js +82 -0
  57. package/src/core/hooks/Header.js +229 -0
  58. package/src/core/hooks/HighLight.js +37 -0
  59. package/src/core/hooks/Hr.js +52 -0
  60. package/src/core/hooks/HtmlBlock.js +159 -0
  61. package/src/core/hooks/Iframe.js +80 -0
  62. package/src/core/hooks/Image.js +276 -0
  63. package/src/core/hooks/InlineCode.js +45 -0
  64. package/src/core/hooks/InlineMath.js +142 -0
  65. package/src/core/hooks/Link.js +169 -0
  66. package/src/core/hooks/List.js +260 -0
  67. package/src/core/hooks/Mark.js +55 -0
  68. package/src/core/hooks/MathBlock.js +97 -0
  69. package/src/core/hooks/Panel.js +170 -0
  70. package/src/core/hooks/Paragraph.js +84 -0
  71. package/src/core/hooks/Ruby.js +34 -0
  72. package/src/core/hooks/Size.js +84 -0
  73. package/src/core/hooks/Strikethrough.js +54 -0
  74. package/src/core/hooks/Sub.js +47 -0
  75. package/src/core/hooks/SuggestList.js +317 -0
  76. package/src/core/hooks/Suggester.js +759 -0
  77. package/src/core/hooks/Sup.js +47 -0
  78. package/src/core/hooks/Table.js +315 -0
  79. package/src/core/hooks/Toc.js +290 -0
  80. package/src/core/hooks/Transfer.js +47 -0
  81. package/src/core/hooks/Underline.js +37 -0
  82. package/src/index.core.js +29 -0
  83. package/src/index.engine.core.js +62 -0
  84. package/src/index.engine.js +30 -0
  85. package/src/index.js +28 -0
  86. package/src/locales/index.js +21 -0
  87. package/src/locales/zh_CN.js +170 -0
  88. package/src/sass/cherry.scss +122 -0
  89. package/src/sass/components/bubble.scss +122 -0
  90. package/src/sass/components/codemirror.scss +628 -0
  91. package/src/sass/components/dropdown.scss +37 -0
  92. package/src/sass/components/editor.scss +78 -0
  93. package/src/sass/components/preview.scss +71 -0
  94. package/src/sass/components/prism.scss +142 -0
  95. package/src/sass/components/stats.scss +32 -0
  96. package/src/sass/components/toc.scss +184 -0
  97. package/src/sass/components/toolbar.scss +117 -0
  98. package/src/sass/core/AutoLink.scss +20 -0
  99. package/src/sass/core/BackgroundColor.scss +0 -0
  100. package/src/sass/core/Badge.scss +116 -0
  101. package/src/sass/core/Blockquote.scss +12 -0
  102. package/src/sass/core/Br.scss +0 -0
  103. package/src/sass/core/Card.scss +219 -0
  104. package/src/sass/core/CodeBlock.scss +205 -0
  105. package/src/sass/core/Color.scss +37 -0
  106. package/src/sass/core/CommentReference.scss +0 -0
  107. package/src/sass/core/Detail.scss +107 -0
  108. package/src/sass/core/Emoji.scss +127 -0
  109. package/src/sass/core/Emphasis.scss +9 -0
  110. package/src/sass/core/Footnote.scss +21 -0
  111. package/src/sass/core/FrontMatterVars.scss +19 -0
  112. package/src/sass/core/Header.scss +103 -0
  113. package/src/sass/core/HighLight.scss +0 -0
  114. package/src/sass/core/Hr.scss +10 -0
  115. package/src/sass/core/HtmlBlock.scss +0 -0
  116. package/src/sass/core/Iframe.scss +36 -0
  117. package/src/sass/core/Image.scss +59 -0
  118. package/src/sass/core/InlineCode.scss +10 -0
  119. package/src/sass/core/InlineMath.scss +11 -0
  120. package/src/sass/core/Link.scss +16 -0
  121. package/src/sass/core/List.scss +61 -0
  122. package/src/sass/core/Mark.scss +15 -0
  123. package/src/sass/core/MathBlock.scss +0 -0
  124. package/src/sass/core/Panel.scss +150 -0
  125. package/src/sass/core/Paragraph.scss +6 -0
  126. package/src/sass/core/Ruby.scss +0 -0
  127. package/src/sass/core/Size.scss +8 -0
  128. package/src/sass/core/Strikethrough.scss +0 -0
  129. package/src/sass/core/Sub.scss +5 -0
  130. package/src/sass/core/Suggester.scss +62 -0
  131. package/src/sass/core/Sup.scss +5 -0
  132. package/src/sass/core/Table.scss +127 -0
  133. package/src/sass/core/Toc.scss +28 -0
  134. package/src/sass/core/Transfer.scss +0 -0
  135. package/src/sass/core/Underline.scss +0 -0
  136. package/src/sass/google-fonts.scss +34 -0
  137. package/src/sass/index.scss +3 -0
  138. package/src/sass/prism/dark.scss +131 -0
  139. package/src/sass/prism/light.scss +143 -0
  140. package/src/sass/variables/colors.scss +96 -0
  141. package/src/toolbars/Bubble.js +232 -0
  142. package/src/toolbars/BubbleTable.js +147 -0
  143. package/src/toolbars/HookCenter.js +185 -0
  144. package/src/toolbars/MenuBase.js +357 -0
  145. package/src/toolbars/PreviewerBubble.js +558 -0
  146. package/src/toolbars/Toc.js +246 -0
  147. package/src/toolbars/Toolbar.js +401 -0
  148. package/src/toolbars/hooks/Audio.js +53 -0
  149. package/src/toolbars/hooks/Badge.js +80 -0
  150. package/src/toolbars/hooks/BarTable.js +41 -0
  151. package/src/toolbars/hooks/Bold.js +70 -0
  152. package/src/toolbars/hooks/Br.js +34 -0
  153. package/src/toolbars/hooks/Card.js +64 -0
  154. package/src/toolbars/hooks/CheckList.js +41 -0
  155. package/src/toolbars/hooks/Code.js +46 -0
  156. package/src/toolbars/hooks/Color.js +285 -0
  157. package/src/toolbars/hooks/Copy.js +139 -0
  158. package/src/toolbars/hooks/Detail.js +70 -0
  159. package/src/toolbars/hooks/ECharts.js +303 -0
  160. package/src/toolbars/hooks/Emoji.js +303 -0
  161. package/src/toolbars/hooks/Export.js +47 -0
  162. package/src/toolbars/hooks/File.js +54 -0
  163. package/src/toolbars/hooks/Formula.js +36 -0
  164. package/src/toolbars/hooks/FullScreen.js +55 -0
  165. package/src/toolbars/hooks/Graph.js +281 -0
  166. package/src/toolbars/hooks/H1.js +71 -0
  167. package/src/toolbars/hooks/H2.js +71 -0
  168. package/src/toolbars/hooks/H3.js +71 -0
  169. package/src/toolbars/hooks/Header.js +100 -0
  170. package/src/toolbars/hooks/Hr.js +35 -0
  171. package/src/toolbars/hooks/Iframe.js +35 -0
  172. package/src/toolbars/hooks/Image.js +60 -0
  173. package/src/toolbars/hooks/Insert.js +36 -0
  174. package/src/toolbars/hooks/Italic.js +70 -0
  175. package/src/toolbars/hooks/LineTable.js +41 -0
  176. package/src/toolbars/hooks/Link.js +46 -0
  177. package/src/toolbars/hooks/List.js +55 -0
  178. package/src/toolbars/hooks/Ol.js +41 -0
  179. package/src/toolbars/hooks/Panel.js +155 -0
  180. package/src/toolbars/hooks/Quote.js +45 -0
  181. package/src/toolbars/hooks/Redo.js +33 -0
  182. package/src/toolbars/hooks/Ruby.js +59 -0
  183. package/src/toolbars/hooks/Size.js +100 -0
  184. package/src/toolbars/hooks/Split.js +37 -0
  185. package/src/toolbars/hooks/Strikethrough.js +65 -0
  186. package/src/toolbars/hooks/Sub.js +58 -0
  187. package/src/toolbars/hooks/Sup.js +58 -0
  188. package/src/toolbars/hooks/SwitchModel.js +78 -0
  189. package/src/toolbars/hooks/Table.js +56 -0
  190. package/src/toolbars/hooks/Toc.js +35 -0
  191. package/src/toolbars/hooks/TogglePreview.js +79 -0
  192. package/src/toolbars/hooks/Ul.js +41 -0
  193. package/src/toolbars/hooks/Underline.js +65 -0
  194. package/src/toolbars/hooks/Undo.js +30 -0
  195. package/src/toolbars/hooks/Video.js +53 -0
  196. package/src/utils/LazyLoadImg.js +341 -0
  197. package/src/utils/autoindent.js +58 -0
  198. package/src/utils/codeBlockContentHandler.js +351 -0
  199. package/src/utils/config.js +98 -0
  200. package/src/utils/copy.js +55 -0
  201. package/src/utils/dialog.js +196 -0
  202. package/src/utils/dom.js +162 -0
  203. package/src/utils/downloadUtil.js +23 -0
  204. package/src/utils/env.js +22 -0
  205. package/src/utils/error.js +61 -0
  206. package/src/utils/event.js +38 -0
  207. package/src/utils/export.js +115 -0
  208. package/src/utils/file.js +121 -0
  209. package/src/utils/formulaUtilsHandler.js +230 -0
  210. package/src/utils/htmlparser.js +977 -0
  211. package/src/utils/image.js +99 -0
  212. package/src/utils/imgSizeHandler.js +279 -0
  213. package/src/utils/jsonUtils.js +17 -0
  214. package/src/utils/lineFeed.js +49 -0
  215. package/src/utils/listContentHandler.js +227 -0
  216. package/src/utils/lookbehind-replace.js +81 -0
  217. package/src/utils/mathjax.js +89 -0
  218. package/src/utils/myersDiff.js +211 -0
  219. package/src/utils/pasteHelper.js +253 -0
  220. package/src/utils/recount-pos.js +59 -0
  221. package/src/utils/regexp.js +295 -0
  222. package/src/utils/sanitize.js +477 -0
  223. package/src/utils/selection.js +50 -0
  224. package/src/utils/svgUtils.js +96 -0
  225. package/src/utils/tableContentHandler.js +592 -0
  226. package/tools/README.md +3 -0
package/src/Editor.js ADDED
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // @ts-check
17
+ import codemirror from 'codemirror';
18
+ // import 'codemirror/mode/markdown/markdown';
19
+ import 'codemirror/mode/gfm/gfm'; // https://codemirror.net/mode/gfm/index.html
20
+ import 'codemirror/mode/yaml-frontmatter/yaml-frontmatter';
21
+ // import 'codemirror/mode/xml/xml';
22
+ import 'codemirror/addon/edit/continuelist';
23
+ import 'codemirror/addon/edit/closetag';
24
+ import 'codemirror/addon/fold/xml-fold';
25
+ import 'codemirror/addon/edit/matchtags';
26
+ import 'codemirror/addon/search/searchcursor';
27
+ import 'codemirror/addon/display/placeholder';
28
+ import 'codemirror/keymap/sublime';
29
+
30
+ import 'cm-search-replace/src/search';
31
+ import 'codemirror/addon/scroll/annotatescrollbar';
32
+ import 'codemirror/addon/search/matchesonscrollbar';
33
+ // import 'codemirror/addon/selection/active-line';
34
+ // import 'codemirror/addon/edit/matchbrackets';
35
+ import htmlParser from '@/utils/htmlparser';
36
+ import pasteHelper from '@/utils/pasteHelper';
37
+ import { addEvent } from './utils/event';
38
+ import Logger from '@/Logger';
39
+ import Event from '@/Event';
40
+ import { handleFileUploadCallback } from '@/utils/file';
41
+ import { createElement } from './utils/dom';
42
+ import { imgBase64Reg, imgDrawioXmlReg } from './utils/regexp';
43
+ import { handleNewlineIndentList } from './utils/autoindent';
44
+
45
+ /**
46
+ * @typedef {import('~types/editor').EditorConfiguration} EditorConfiguration
47
+ * @typedef {import('~types/editor').EditorEventCallback} EditorEventCallback
48
+ * @typedef {import('codemirror')} CodeMirror
49
+ */
50
+
51
+ /** @type {import('~types/editor')} */
52
+ export default class Editor {
53
+ /**
54
+ * @constructor
55
+ * @param {Partial<EditorConfiguration>} options
56
+ */
57
+ constructor(options) {
58
+ /**
59
+ * @property
60
+ * @type {EditorConfiguration}
61
+ */
62
+ this.options = {
63
+ id: 'code', // textarea 的id属性值
64
+ name: 'code', // textarea 的name属性值
65
+ autoSave2Textarea: false,
66
+ editorDom: document.createElement('div'),
67
+ wrapperDom: null,
68
+ autoScrollByCursor: true,
69
+ convertWhenPaste: true,
70
+ codemirror: {
71
+ lineNumbers: false, // 显示行数
72
+ cursorHeight: 0.85, // 光标高度,0.85好看一些
73
+ indentUnit: 4, // 缩进单位为4
74
+ tabSize: 4, // 一个tab转换成的空格数量
75
+ // styleActiveLine: false, // 当前行背景高亮
76
+ // matchBrackets: true, // 括号匹配
77
+ mode: {
78
+ name: 'yaml-frontmatter', // yaml-frontmatter在gfm的基础上增加了对yaml的支持
79
+ base: {
80
+ name: 'gfm',
81
+ gitHubSpice: false, // 修复github风格的markdown语法高亮,见[issue#925](https://github.com/Tencent/cherry-markdown/issues/925)
82
+ },
83
+ }, // 从markdown模式改成gfm模式,以使用默认高亮规则
84
+ lineWrapping: true, // 自动换行
85
+ indentWithTabs: true, // 缩进用tab表示
86
+ autofocus: true,
87
+ theme: 'default',
88
+ autoCloseTags: true, // 输入html标签时自动补充闭合标签
89
+ extraKeys: {
90
+ Enter: handleNewlineIndentList,
91
+ }, // 增加markdown回车自动补全
92
+ matchTags: { bothTags: true }, // 自动高亮选中的闭合html标签
93
+ placeholder: '',
94
+ // 设置为 contenteditable 对输入法定位更友好
95
+ // 但已知会影响某些悬浮菜单的定位,如粘贴选择文本或markdown模式的菜单
96
+ // inputStyle: 'contenteditable',
97
+ keyMap: 'sublime',
98
+ },
99
+ toolbars: {},
100
+ onKeydown() {},
101
+ onChange() {},
102
+ onFocus() {},
103
+ onBlur() {},
104
+ onPaste: this.onPaste,
105
+ onScroll: this.onScroll,
106
+ };
107
+ /**
108
+ * @property
109
+ * @private
110
+ * @type {{ timer?: number; destinationTop?: number }}
111
+ */
112
+ this.animation = {};
113
+ const { codemirror, ...restOptions } = options;
114
+ if (codemirror) {
115
+ Object.assign(this.options.codemirror, codemirror);
116
+ }
117
+ Object.assign(this.options, restOptions);
118
+ this.$cherry = this.options.$cherry;
119
+ this.instanceId = this.$cherry.getInstanceId();
120
+ }
121
+
122
+ /**
123
+ * 在onChange后处理draw.io的xml数据和图片的base64数据,对这种超大的数据增加省略号,
124
+ * 以及对全角符号进行特殊染色。
125
+ */
126
+ dealSpecialWords = () => {
127
+ if (this.noChange) {
128
+ this.noChange = false;
129
+ return;
130
+ }
131
+ this.formatFullWidthMark();
132
+ this.formatBigData2Mark(imgBase64Reg, 'cm-url base64');
133
+ this.formatBigData2Mark(imgDrawioXmlReg, 'cm-url drawio');
134
+ };
135
+
136
+ /**
137
+ * 把大字符串变成省略号
138
+ * @param {*} reg 正则
139
+ * @param {*} className 利用codemirror的MarkText生成的新元素的class
140
+ */
141
+ formatBigData2Mark = (reg, className) => {
142
+ const codemirror = this.editor;
143
+ const searcher = codemirror.getSearchCursor(reg);
144
+
145
+ let oneSearch = searcher.findNext();
146
+ for (; oneSearch !== false; oneSearch = searcher.findNext()) {
147
+ const target = searcher.from();
148
+ if (!target) {
149
+ continue;
150
+ }
151
+ const bigString = oneSearch[2] ?? '';
152
+ const targetChFrom = target.ch + oneSearch[1]?.length;
153
+ const targetChTo = targetChFrom + bigString.length;
154
+ const targetLine = target.line;
155
+ const begin = { line: targetLine, ch: targetChFrom };
156
+ const end = { line: targetLine, ch: targetChTo };
157
+ // 如果所在区域已经有mark了,则不再增加mark
158
+ if (codemirror.findMarks(begin, end).length > 0) {
159
+ continue;
160
+ }
161
+ const newSpan = createElement('span', `cm-string ${className}`, { title: bigString });
162
+ newSpan.textContent = bigString;
163
+ this.noChange = true;
164
+ codemirror.markText(begin, end, { replacedWith: newSpan, atomic: true });
165
+ }
166
+ };
167
+
168
+ /**
169
+ * 高亮全角符号 ·|¥|、|:|“|”|【|】|(|)|《|》
170
+ * full width翻译为全角
171
+ */
172
+ formatFullWidthMark() {
173
+ if (!this.options.showFullWidthMark) {
174
+ return;
175
+ }
176
+ const { editor } = this;
177
+ const regex = /[·¥、:“”【】()《》]/; // 此处以仅匹配单个全角符号
178
+ const searcher = editor.getSearchCursor(regex);
179
+ let oneSearch = searcher.findNext();
180
+ // 防止出现错误的mark
181
+ editor.getAllMarks().forEach(function (mark) {
182
+ if (mark.className === 'cm-fullWidth') {
183
+ const range = JSON.parse(JSON.stringify(mark.find()));
184
+ const markedText = editor.getRange(range.from, range.to);
185
+ if (!regex.test(markedText)) {
186
+ mark.clear();
187
+ }
188
+ }
189
+ });
190
+ for (; oneSearch !== false; oneSearch = searcher.findNext()) {
191
+ const target = searcher.from();
192
+ if (!target) {
193
+ continue;
194
+ }
195
+ const from = { line: target.line, ch: target.ch };
196
+ const to = { line: target.line, ch: target.ch + 1 };
197
+ // 当没有标记时再进行标记,判断textMaker的className必须为"cm-fullWidth",
198
+ // 因为cm的addon里会引入className: "CodeMirror-composing"的textMaker干扰判断
199
+ const existMarksLength = editor.findMarks(from, to).filter((item) => {
200
+ return item.className === 'cm-fullWidth';
201
+ });
202
+ if (existMarksLength.length === 0) {
203
+ editor.markText(from, to, {
204
+ className: 'cm-fullWidth',
205
+ title: '按住Ctrl/Cmd点击切换成半角(Hold down Ctrl/Cmd and click to switch to half-width)',
206
+ });
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ *
213
+ * @param {CodeMirror.Editor} codemirror
214
+ * @param {MouseEvent} evt
215
+ */
216
+ toHalfWidth(codemirror, evt) {
217
+ const { target } = evt;
218
+ if (!(target instanceof HTMLElement)) {
219
+ return;
220
+ }
221
+ // 针对windows用户为Ctrl按键,Mac用户为Cmd按键
222
+ if (target.classList.contains('cm-fullWidth') && (evt.ctrlKey || evt.metaKey) && evt.buttons === 1) {
223
+ const rect = target.getBoundingClientRect();
224
+ // 由于是一个字符,所以肯定在一行
225
+ const from = codemirror.coordsChar({ left: rect.left, top: rect.top });
226
+ const to = { line: from.line, ch: from.ch + 1 };
227
+ codemirror.setSelection(from, to);
228
+ codemirror.replaceSelection(
229
+ target.innerText
230
+ .replace('·', '`')
231
+ .replace('¥', '$')
232
+ .replace('、', '/')
233
+ .replace(':', ':')
234
+ .replace('“', '"')
235
+ .replace('”', '"')
236
+ .replace('【', '[')
237
+ .replace('】', ']')
238
+ .replace('(', '(')
239
+ .replace(')', ')')
240
+ .replace('《', '<')
241
+ .replace('》', '>'),
242
+ );
243
+ }
244
+ }
245
+ /**
246
+ *
247
+ * @param {KeyboardEvent} e
248
+ * @param {CodeMirror.Editor} codemirror
249
+ */
250
+ onKeyup = (e, codemirror) => {
251
+ const { line: targetLine } = codemirror.getCursor();
252
+ this.previewer.highlightLine(targetLine + 1);
253
+ };
254
+
255
+ /**
256
+ *
257
+ * @param {ClipboardEvent} e
258
+ * @param {CodeMirror.Editor} codemirror
259
+ */
260
+ onPaste(e, codemirror) {
261
+ let { clipboardData } = e;
262
+ if (clipboardData) {
263
+ this.handlePaste(e, clipboardData, codemirror);
264
+ } else {
265
+ ({ clipboardData } = window);
266
+ this.handlePaste(e, clipboardData, codemirror);
267
+ }
268
+ }
269
+
270
+ /**
271
+ *
272
+ * @param {ClipboardEvent} event
273
+ * @param {ClipboardEvent['clipboardData']} clipboardData
274
+ * @param {CodeMirror.Editor} codemirror
275
+ * @returns {boolean | void}
276
+ */
277
+ handlePaste(event, clipboardData, codemirror) {
278
+ const onPasteRet = this.$cherry.options.callback.onPaste(clipboardData);
279
+ if (onPasteRet !== false && typeof onPasteRet === 'string') {
280
+ event.preventDefault();
281
+ codemirror.replaceSelection(onPasteRet);
282
+ return;
283
+ }
284
+ let html = clipboardData.getData('Text/Html');
285
+ const { items } = clipboardData;
286
+ // 清空注释
287
+ html = html.replace(/<!--[^>]+>/g, '');
288
+ /**
289
+ * 处理“右键复制图片”场景
290
+ * 在这种场景下,我们希望粘贴进来的图片可以走文件上传逻辑,所以当检测到这种场景后,我们会清空html
291
+ */
292
+ if (
293
+ /<body>\s*<img [^>]+>\s*<\/body>/.test(html) &&
294
+ items[1]?.kind === 'file' &&
295
+ items[1]?.type.match(/^image\//i)
296
+ ) {
297
+ html = '';
298
+ }
299
+ const codemirrorDoc = codemirror.getDoc();
300
+ this.fileUploadCount = 0;
301
+ // 只要有html内容,就不处理剪切板里的其他内容,这么做的后果是粘贴excel内容时,只会粘贴html内容,不会把excel对应的截图粘进来
302
+ for (let i = 0; !html && i < items.length; i++) {
303
+ const item = items[i];
304
+ // 判断是否为图片数据
305
+ if (item && item.kind === 'file' && item.type.match(/^image\//i)) {
306
+ // 读取该图片
307
+ const file = item.getAsFile();
308
+ this.options.fileUpload(file, (url, params = {}) => {
309
+ this.fileUploadCount += 1;
310
+ if (typeof url !== 'string') {
311
+ return;
312
+ }
313
+ const mdStr = `${this.fileUploadCount > 1 ? '\n' : ''}${handleFileUploadCallback(url, params, file)}`;
314
+ codemirrorDoc.replaceSelection(mdStr);
315
+ // if (this.pasterHtml) {
316
+ // // 如果同时粘贴了html内容和文件内容,则在文件上传完成后强制让光标处于非选中状态,以防止自动选中的html内容被文件内容替换掉
317
+ // const { line, ch } = codemirror.getCursor();
318
+ // codemirror.setSelection({ line, ch }, { line, ch });
319
+ // codemirrorDoc.replaceSelection(mdStr, 'end');
320
+ // } else {
321
+ // codemirrorDoc.replaceSelection(mdStr);
322
+ // }
323
+ });
324
+ event.preventDefault();
325
+ }
326
+ }
327
+
328
+ // 复制html转换markdown
329
+ const htmlText = clipboardData.getData('text/plain');
330
+ if (!html || !this.options.convertWhenPaste) {
331
+ return true;
332
+ }
333
+
334
+ let divObj = document.createElement('DIV');
335
+ divObj.innerHTML = html;
336
+ html = divObj.innerHTML;
337
+ const mdText = htmlParser.run(html);
338
+ if (typeof mdText === 'string' && mdText.trim().length > 0) {
339
+ const range = codemirror.listSelections();
340
+ if (codemirror.getSelections().length <= 1 && range[0] && range[0].anchor) {
341
+ const currentCursor = {};
342
+ currentCursor.line = range[0].anchor.line;
343
+ currentCursor.ch = range[0].anchor.ch;
344
+ codemirrorDoc.replaceSelection(mdText);
345
+ pasteHelper.showSwitchBtnAfterPasteHtml(this.$cherry, currentCursor, codemirror, htmlText, mdText);
346
+ } else {
347
+ codemirrorDoc.replaceSelection(mdText);
348
+ }
349
+ event.preventDefault();
350
+ }
351
+ divObj = null;
352
+ }
353
+
354
+ /**
355
+ *
356
+ * @param {CodeMirror.Editor} codemirror
357
+ */
358
+ onScroll = (codemirror) => {
359
+ Event.emit(this.instanceId, Event.Events.cleanAllSubMenus); // 滚动时清除所有子菜单,这不应该在Bubble中处理,我们关注的是编辑器的滚动 add by ufec
360
+ if (this.disableScrollListener) {
361
+ this.disableScrollListener = false;
362
+ return;
363
+ }
364
+ const scroller = codemirror.getScrollerElement();
365
+ if (scroller.scrollTop <= 0) {
366
+ this.previewer.scrollToLineNum(0);
367
+ return;
368
+ }
369
+ if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 20) {
370
+ this.previewer.scrollToLineNum(null); // 滚动到底
371
+ return;
372
+ }
373
+ const currentTop = codemirror.getScrollInfo().top;
374
+ const targetLine = codemirror.lineAtHeight(currentTop, 'local');
375
+ const lineRect = codemirror.charCoords({ line: targetLine, ch: 0 }, 'local');
376
+ const lineHeight = codemirror.getLineHandle(targetLine).height;
377
+ const lineTop = lineRect.bottom - lineHeight; // 直接用lineRect.top在自动折行时计算的是最后一行的top
378
+ const percent = (100 * (currentTop - lineTop)) / lineHeight / 100;
379
+ // console.log(percent);
380
+ // codemirror中行号以0开始,所以需要+1
381
+ this.previewer.scrollToLineNum(targetLine + 1, percent);
382
+ };
383
+
384
+ /**
385
+ *
386
+ * @param {CodeMirror.Editor} codemirror
387
+ * @param {MouseEvent} evt
388
+ */
389
+ onMouseDown = (codemirror, evt) => {
390
+ Event.emit(this.instanceId, Event.Events.cleanAllSubMenus); // Bubble中处理需要考虑太多,直接在编辑器中处理可包括Bubble中所有情况,因为产生Bubble的前提是光标在编辑器中 add by ufec
391
+ const { line: targetLine } = codemirror.getCursor();
392
+ const top = Math.abs(evt.y - codemirror.getWrapperElement().getBoundingClientRect().y);
393
+ this.previewer.scrollToLineNumWithOffset(targetLine + 1, top);
394
+ this.toHalfWidth(codemirror, evt);
395
+ };
396
+
397
+ /**
398
+ * 光标变化事件
399
+ */
400
+ onCursorActivity = () => {
401
+ this.refreshWritingStatus();
402
+ };
403
+
404
+ /**
405
+ * 记忆页面的滚动高度,在cherry初始化后恢复到这个高度
406
+ */
407
+ storeDocumentScroll() {
408
+ if (!this.options.keepDocumentScrollAfterInit) {
409
+ return;
410
+ }
411
+ this.needRestoreDocumentScroll = true;
412
+ this.documentElementScrollTop = document.documentElement.scrollTop;
413
+ this.documentElementScrollLeft = document.documentElement.scrollLeft;
414
+ }
415
+
416
+ /**
417
+ * 在cherry初始化后恢复到这个高度
418
+ */
419
+ restoreDocumentScroll() {
420
+ if (!this.options.keepDocumentScrollAfterInit || !this.needRestoreDocumentScroll) {
421
+ return;
422
+ }
423
+ this.needRestoreDocumentScroll = false;
424
+ window.scrollTo(this.documentElementScrollLeft, this.documentElementScrollTop);
425
+ }
426
+
427
+ /**
428
+ *
429
+ * @param {*} previewer
430
+ */
431
+ init(previewer) {
432
+ this.storeDocumentScroll();
433
+ const textArea = this.options.editorDom.querySelector(`#${this.options.id}`);
434
+ if (!(textArea instanceof HTMLTextAreaElement)) {
435
+ throw new Error('The specific element is not a textarea.');
436
+ }
437
+ const editor = codemirror.fromTextArea(textArea, this.options.codemirror);
438
+ // editor.refresh();
439
+ editor.addOverlay({
440
+ name: 'invisibles',
441
+ token: function nextToken(stream) {
442
+ let tokenClass;
443
+ let spaces = 0;
444
+ let peek = stream.peek() === ' ';
445
+ if (peek) {
446
+ while (peek && spaces < Number.MAX_VALUE) {
447
+ spaces += 1;
448
+ stream.next();
449
+ peek = stream.peek() === ' ';
450
+ }
451
+ tokenClass = `whitespace whitespace-${spaces}`;
452
+ } else {
453
+ while (!stream.eol()) {
454
+ stream.next();
455
+ }
456
+ tokenClass = '';
457
+ }
458
+ return tokenClass;
459
+ },
460
+ });
461
+
462
+ for (const item of this.options.overlay) {
463
+ editor.addOverlay(item);
464
+ }
465
+
466
+ this.previewer = previewer;
467
+ this.disableScrollListener = false;
468
+
469
+ if (this.options.value) {
470
+ editor.setOption('value', this.options.value);
471
+ }
472
+
473
+ editor.on('blur', (codemirror, evt) => {
474
+ this.options.onBlur(evt, codemirror);
475
+ });
476
+
477
+ editor.on('focus', (codemirror, evt) => {
478
+ this.options.onFocus(evt, codemirror);
479
+ });
480
+
481
+ editor.on('change', (codemirror, evt) => {
482
+ this.options.onChange(evt, codemirror);
483
+ this.dealSpecialWords();
484
+ if (this.options.autoSave2Textarea) {
485
+ // @ts-ignore
486
+ // 将codemirror里的内容回写到textarea里
487
+ codemirror.save();
488
+ }
489
+ });
490
+ let currentLine = null;
491
+ // 监听光标移动事件
492
+ editor.on('cursorActivity', function () {
493
+ // 获取当前光标位置的行号
494
+ const { line } = editor.getCursor();
495
+
496
+ // 移除之前高亮的行
497
+ if (currentLine !== null) {
498
+ editor.removeLineClass(currentLine, 'background', 'cm-cursor-line');
499
+ }
500
+
501
+ // 添加新高亮的行
502
+ currentLine = line;
503
+ editor.addLineClass(currentLine, 'background', 'cm-cursor-line');
504
+ });
505
+
506
+ editor.on('keydown', (codemirror, evt) => {
507
+ this.options.onKeydown(evt, codemirror);
508
+ });
509
+
510
+ editor.on('keyup', (codemirror, evt) => {
511
+ this.onKeyup(evt, codemirror);
512
+ });
513
+
514
+ editor.on('paste', (codemirror, evt) => {
515
+ this.options.onPaste.call(this, evt, codemirror);
516
+ });
517
+
518
+ if (this.options.autoScrollByCursor) {
519
+ editor.on('mousedown', (codemirror, evt) => {
520
+ setTimeout(() => {
521
+ this.onMouseDown(codemirror, evt);
522
+ });
523
+ });
524
+ }
525
+
526
+ editor.on('drop', (codemirror, evt) => {
527
+ const files = evt.dataTransfer.files || [];
528
+ if (files && files.length > 0) {
529
+ // 增加延时,让drop的位置变成codemirror的光标位置
530
+ setTimeout(() => {
531
+ for (let i = 0; i < files.length; i++) {
532
+ const file = files[i];
533
+ const fileType = file.type || '';
534
+ // text格式或md格式文件,直接读取内容,不做上传文件的操作
535
+ if (/\.(text|md)/.test(file.name) || /^text/i.test(fileType)) {
536
+ continue;
537
+ }
538
+ this.options.fileUpload(file, (url, params = {}) => {
539
+ if (typeof url !== 'string') {
540
+ return;
541
+ }
542
+ // 拖拽上传文件时,强制改成没有文字选择区的状态
543
+ codemirror.setSelection(codemirror.getCursor());
544
+ const mdStr = handleFileUploadCallback(url, params, file);
545
+ // 当批量上传文件时,每个被插入的文件中间需要加个换行,但单个上传文件的时候不需要加换行
546
+ const insertValue = i > 0 ? `\n${mdStr} ` : `${mdStr} `;
547
+ codemirror.replaceSelection(insertValue);
548
+ this.dealSpecialWords();
549
+ });
550
+ }
551
+ }, 50);
552
+ }
553
+ });
554
+
555
+ editor.on('scroll', (codemirror) => {
556
+ this.options.onScroll(codemirror);
557
+ this.options.writingStyle === 'focus' && this.refreshWritingStatus();
558
+ });
559
+
560
+ editor.on('cursorActivity', () => {
561
+ this.onCursorActivity();
562
+ });
563
+
564
+ addEvent(
565
+ this.getEditorDom(),
566
+ 'wheel',
567
+ () => {
568
+ // 鼠标滚轮滚动时,强制监听滚动事件
569
+ this.disableScrollListener = false;
570
+ // 打断滚动动画
571
+ cancelAnimationFrame(this.animation.timer);
572
+ this.animation.timer = 0;
573
+ },
574
+ false,
575
+ );
576
+
577
+ /**
578
+ * @property
579
+ * @type {CodeMirror.Editor}
580
+ */
581
+ this.editor = editor;
582
+
583
+ if (this.options.writingStyle !== 'normal') {
584
+ this.initWritingStyle();
585
+ }
586
+ // 处理特殊字符,主要将base64等大文本替换成占位符,以提高可读性
587
+ this.dealSpecialWords();
588
+ this.restoreDocumentScroll();
589
+ }
590
+
591
+ /**
592
+ *
593
+ * @param {number | null} beginLine 起始行,传入null时跳转到文档尾部
594
+ * @param {number} [endLine] 终止行
595
+ * @param {number} [percent] 百分比,取值0~1
596
+ */
597
+ jumpToLine(beginLine, endLine, percent) {
598
+ if (beginLine === null) {
599
+ cancelAnimationFrame(this.animation.timer);
600
+ this.disableScrollListener = true;
601
+ this.editor.scrollIntoView({
602
+ line: this.editor.lineCount() - 1,
603
+ ch: 1,
604
+ });
605
+ this.animation.timer = 0;
606
+ return;
607
+ }
608
+ const position = this.editor.charCoords({ line: beginLine, ch: 0 }, 'local');
609
+ let { top } = position;
610
+ const positionEnd = this.editor.charCoords({ line: beginLine + endLine, ch: 0 }, 'local');
611
+ const height = positionEnd.top - position.top;
612
+ top += height * percent;
613
+ this.animation.destinationTop = Math.ceil(top - 15);
614
+ if (this.animation.timer) {
615
+ return;
616
+ }
617
+ const animationHandler = () => {
618
+ const currentTop = this.editor.getScrollInfo().top;
619
+ const delta = this.animation.destinationTop - currentTop;
620
+ // 100毫秒内完成动画
621
+ const move = Math.ceil(Math.min(Math.abs(delta), Math.max(1, Math.abs(delta) / (100 / 16.7))));
622
+ // console.log('should scroll: ', move, delta, currentTop, this.animation.destinationTop);
623
+ if (delta > 0) {
624
+ if (currentTop >= this.animation.destinationTop) {
625
+ this.animation.timer = 0;
626
+ return;
627
+ }
628
+ this.disableScrollListener = true;
629
+ this.editor.scrollTo(null, currentTop + move);
630
+ } else if (delta < 0) {
631
+ if (currentTop <= this.animation.destinationTop || currentTop <= 0) {
632
+ this.animation.timer = 0;
633
+ return;
634
+ }
635
+ this.disableScrollListener = true;
636
+ this.editor.scrollTo(null, currentTop - move);
637
+ } else {
638
+ this.animation.timer = 0;
639
+ return;
640
+ }
641
+ // 无法再继续滚动
642
+ if (currentTop === this.editor.getScrollInfo().top || move >= Math.abs(delta)) {
643
+ this.animation.timer = 0;
644
+ return;
645
+ }
646
+ this.animation.timer = requestAnimationFrame(animationHandler);
647
+ };
648
+ this.animation.timer = requestAnimationFrame(animationHandler);
649
+ }
650
+
651
+ /**
652
+ *
653
+ * @param {number | null} lineNum
654
+ * @param {number} [endLine]
655
+ * @param {number} [percent]
656
+ */
657
+ scrollToLineNum(lineNum, endLine, percent) {
658
+ if (lineNum === null) {
659
+ this.jumpToLine(null);
660
+ return;
661
+ }
662
+ const $lineNum = Math.max(0, lineNum);
663
+ this.jumpToLine($lineNum, endLine, percent);
664
+ Logger.log('滚动预览区域,左侧应scroll to ', $lineNum);
665
+ }
666
+
667
+ /**
668
+ *
669
+ * @returns {HTMLElement}
670
+ */
671
+ getEditorDom() {
672
+ return this.options.editorDom;
673
+ }
674
+
675
+ /**
676
+ *
677
+ * @param {string} event 事件名
678
+ * @param {EditorEventCallback} callback 回调函数
679
+ */
680
+ addListener(event, callback) {
681
+ this.editor.on(event, callback);
682
+ }
683
+
684
+ /**
685
+ * 初始化书写风格
686
+ */
687
+ initWritingStyle() {
688
+ const { writingStyle } = this.options;
689
+ const className = `cherry-editor-writing-style--${writingStyle}`;
690
+ const editorDom = this.getEditorDom();
691
+ // 重置状态
692
+ Array.from(editorDom.classList)
693
+ .filter((className) => className.startsWith('cherry-editor-writing-style--'))
694
+ .forEach((className) => editorDom.classList.remove(className));
695
+ if (writingStyle === 'normal') {
696
+ return;
697
+ }
698
+ editorDom.classList.add(className);
699
+ this.refreshWritingStatus();
700
+ }
701
+
702
+ /**
703
+ * 刷新书写状态
704
+ */
705
+ refreshWritingStatus() {
706
+ const { writingStyle } = this.options;
707
+ if (writingStyle !== 'focus' && writingStyle !== 'typewriter') {
708
+ return;
709
+ }
710
+ const className = `cherry-editor-writing-style--${writingStyle}`;
711
+ /**
712
+ * @type {HTMLStyleElement}
713
+ */
714
+ const style = document.querySelector('#cherry-editor-writing-style') || document.createElement('style');
715
+ style.id = 'cherry-editor-writing-style';
716
+ Array.from(document.head.childNodes).find((node) => node === style) || document.head.appendChild(style);
717
+ const { sheet } = style;
718
+ Array.from(Array(sheet.cssRules.length)).forEach(() => sheet.deleteRule(0));
719
+ if (writingStyle === 'focus') {
720
+ const editorDomRect = this.getEditorDom().getBoundingClientRect();
721
+ // 获取光标所在位置
722
+ const { top, bottom } = this.editor.charCoords(this.editor.getCursor());
723
+ // 光标上部距离编辑器顶部距离(不包含菜单)
724
+ const topHeight = top - editorDomRect.top;
725
+ // 光标下部距离编辑器底部距离
726
+ const bottomHeight = editorDomRect.height - (bottom - editorDomRect.top);
727
+ sheet.insertRule(`.${className}::before { height: ${topHeight > 0 ? topHeight : 0}px; }`, 0);
728
+ sheet.insertRule(`.${className}::after { height: ${bottomHeight > 0 ? bottomHeight : 0}px; }`, 0);
729
+ }
730
+ if (writingStyle === 'typewriter') {
731
+ // 编辑器顶/底部填充的空白高度 (用于内容不足时使光标所在行滚动到编辑器中央)
732
+ const height = this.editor.getScrollInfo().clientHeight / 2;
733
+ sheet.insertRule(`.${className} .CodeMirror-lines::before { height: ${height}px; }`, 0);
734
+ sheet.insertRule(`.${className} .CodeMirror-lines::after { height: ${height}px; }`, 0);
735
+ this.editor.scrollTo(null, this.editor.cursorCoords(null, 'local').top - height);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * 修改书写风格
741
+ */
742
+ setWritingStyle(writingStyle) {
743
+ this.options.writingStyle = writingStyle;
744
+ this.initWritingStyle();
745
+ }
746
+ }