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
@@ -0,0 +1,759 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Tencent is pleased to support the open source community by making CherryMarkdown available.
4
+ *
5
+ * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
6
+ * The below software in this distribution may have been modified by THL A29 Limited ("Tencent Modifications").
7
+ *
8
+ * All Tencent Modifications are Copyright (C) THL A29 Limited.
9
+ *
10
+ * CherryMarkdown is licensed under the Apache License, Version 2.0 (the "License");
11
+ * you may not use this file except in compliance with the License.
12
+ * You may obtain a copy of the License at
13
+ *
14
+ * http://www.apache.org/licenses/LICENSE-2.0
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ import escapeRegExp from 'lodash/escapeRegExp';
23
+ import SyntaxBase from '@/core/SyntaxBase';
24
+ import { systemSuggests } from '@/core/hooks/SuggestList';
25
+ import { Pass } from 'codemirror/src/util/misc';
26
+ import { isLookbehindSupported } from '@/utils/regexp';
27
+ import { replaceLookbehind } from '@/utils/lookbehind-replace';
28
+ import { isBrowser } from '@/utils/env';
29
+ import mergeWith from 'lodash/mergeWith';
30
+ import { Theme } from '@/Theme';
31
+
32
+ /**
33
+ * @typedef {import('codemirror')} CodeMirror
34
+ */
35
+
36
+ /**
37
+ * @typedef { Object } SuggestListItemObject 推荐列表项对象
38
+ * @property { string } icon 图标
39
+ * @property { string } label 候选列表回显的内容
40
+ * @property { string } value 点击候选项的时候回填的值
41
+ * @property { string } keyword 关键词,通过关键词控制候选项的显隐
42
+ * @typedef { SuggestListItemObject | string } SuggestListItem 推荐列表项
43
+ * @typedef { Array<SuggestListItem> } SuggestList 推荐列表
44
+ */
45
+
46
+ /**
47
+ * @typedef {object} SuggesterConfigItem
48
+ * @property {function(string, function(SuggestList): void): void} suggestList
49
+ * @property {string} keyword
50
+ * @property {function} suggestListRender
51
+ * @property {function} echo
52
+ * @typedef {object} SuggesterConfig
53
+ * @property {Array<SuggesterConfigItem>} suggester
54
+ */
55
+
56
+ export default class Suggester extends SyntaxBase {
57
+ static HOOK_NAME = 'suggester';
58
+
59
+ constructor({ config }) {
60
+ /**
61
+ * config.suggester 内容
62
+ * [{
63
+ * keyword:':' # 唤醒关键字,
64
+ * data(keywords,callback){ #自己处理候选列表h
65
+ * callback([
66
+ * {
67
+ * icon:'',
68
+ * key:'',显示名称
69
+ * value:'',设置的值
70
+ * }
71
+ * ]);
72
+ * //callback(false)则表示终止
73
+ * }
74
+ * },
75
+ * {
76
+ * keyword:'@' # 唤醒关键字,
77
+ * data:[ //静态列表
78
+ * {
79
+ * icon:'',
80
+ * key:'',显示名称
81
+ * keyword:'',二次匹配的关键字
82
+ * value:'',设置的值
83
+ * }
84
+ * ]
85
+ * }]
86
+ *
87
+ */
88
+
89
+ super({ needCache: true });
90
+
91
+ this.config = config;
92
+ this.RULE = this.rule();
93
+ this.suggesterPanel = new SuggesterPanel();
94
+ }
95
+
96
+ afterInit(callback) {
97
+ // node环境下直接跳过输入联想
98
+ if (!isBrowser()) {
99
+ return;
100
+ }
101
+ if (typeof callback === 'function') {
102
+ callback();
103
+ }
104
+ this.initConfig(this.config);
105
+ }
106
+
107
+ /**
108
+ * 初始化配置
109
+ * @param {SuggesterConfig} config
110
+ */
111
+ initConfig(config) {
112
+ let { suggester } = config;
113
+
114
+ this.suggester = [];
115
+ if (!suggester) {
116
+ suggester = [];
117
+ }
118
+ for (const suggest of mergeWith(systemSuggests, suggester)) {
119
+ for (const keyword of suggest.keyword) {
120
+ suggester.push({
121
+ keyword,
122
+ data: suggest.data,
123
+ });
124
+ }
125
+ }
126
+ suggester.forEach((configItem) => {
127
+ if (!configItem.data) {
128
+ console.warn('[cherry-suggester]: the suggestList of config is missing.');
129
+ return;
130
+ }
131
+
132
+ if (!configItem.keyword) {
133
+ configItem.keyword = '@';
134
+ }
135
+
136
+ this.suggester[configItem.keyword] = configItem;
137
+ });
138
+
139
+ // 反复初始化时, 缓存还在, dom 已更新情况
140
+ if (this.suggesterPanel.hasEditor()) {
141
+ this.suggesterPanel.editor = null;
142
+ }
143
+ }
144
+
145
+ makeHtml(str) {
146
+ if (!this.RULE.reg) return str;
147
+ if (!this.suggesterPanel.hasEditor() && isBrowser()) {
148
+ const { editor } = this.$engine.$cherry;
149
+ this.suggesterPanel.setEditor(editor);
150
+ this.suggesterPanel.setSuggester(this.suggester);
151
+ this.suggesterPanel.bindEvent();
152
+ }
153
+ if (isLookbehindSupported()) {
154
+ return str.replace(this.RULE.reg, this.toHtml.bind(this));
155
+ }
156
+ return replaceLookbehind(str, this.RULE.reg, this.toHtml.bind(this), true, 1);
157
+ }
158
+
159
+ toHtml(wholeMatch, leadingChar, keyword, text) {
160
+ if (text) {
161
+ return (
162
+ this.suggester[keyword]?.echo?.call(this, text) ||
163
+ `${leadingChar}<span class="cherry-suggestion">${keyword}${text}</span>`
164
+ );
165
+ }
166
+ if (this.suggester[keyword]?.echo === false) {
167
+ return `${leadingChar}`;
168
+ }
169
+ if (!this.suggester[keyword]) {
170
+ return leadingChar + text;
171
+ }
172
+ return text ? leadingChar + text : `${leadingChar}`;
173
+ }
174
+
175
+ rule() {
176
+ if (!this.suggester || Object.keys(this.suggester).length <= 0) {
177
+ return {};
178
+ }
179
+ const keys = Object.keys(this.suggester)
180
+ .map((key) => escapeRegExp(key))
181
+ .join('|');
182
+ const reg = new RegExp(
183
+ `${isLookbehindSupported() ? '((?<!\\\\))[ ]' : '(^|[^\\\\])[ ]'}(${keys})(([^${keys}\\s])+)`,
184
+ 'g',
185
+ );
186
+ return /** @type {any} */ ({
187
+ reg,
188
+ });
189
+ }
190
+
191
+ mounted() {
192
+ if (!this.suggesterPanel.hasEditor() && isBrowser()) {
193
+ const { editor } = this.$engine.$cherry;
194
+ this.suggesterPanel.setEditor(editor);
195
+ this.suggesterPanel.setSuggester(this.suggester);
196
+ this.suggesterPanel.bindEvent();
197
+ this.suggesterPanel.setCherry(this.$engine.$cherry);
198
+ }
199
+ }
200
+ }
201
+
202
+ export function getSuggestList(_list, _key, _word, callback) {
203
+ // 将word全转成小写
204
+ const word = _word.toLowerCase();
205
+ // 加个空格就直接退出联想
206
+ if (/^\s$/.test(word)) {
207
+ callback(false);
208
+ return;
209
+ }
210
+ function escapeRegExp(string) {
211
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& 表示整个匹配到的字符串
212
+ }
213
+ const keyword = word
214
+ .replace(/\s+/g, '') // 删掉空格,避免产生不必要的空数组元素
215
+ .replace(new RegExp(`^${escapeRegExp(_key)}`, 'g'), '') // 删掉word当中suggesterKeywords出现的字符
216
+ .split('')
217
+ .map((item) => escapeRegExp(item))
218
+ .join('.*?');
219
+
220
+ const test = new RegExp(`^.*?${keyword}.*?$`, 'i');
221
+
222
+ const suggestList = _list.filter((item) => {
223
+ return !word || test.test(item.keyword);
224
+ });
225
+
226
+ // 当没有候选项时直接推出联想
227
+ callback(suggestList.length === 0 ? false : suggestList);
228
+ }
229
+
230
+ /**
231
+ *
232
+ * @param {Cherry} $cherry
233
+ * @param {array} list
234
+ * @returns {*[]}
235
+ */
236
+ export function expandList($cherry, list) {
237
+ const retList = [];
238
+ list.forEach(function (suggest) {
239
+ if (suggest.toolbar) {
240
+ const toolbar = $cherry.toolbar.menus.hooks[suggest.toolbar];
241
+ if (toolbar) {
242
+ if (toolbar.subMenuConfig && toolbar.subMenuConfig.length > 0) {
243
+ toolbar.subMenuConfig.forEach(function (menu) {
244
+ const menuItem = {};
245
+ menuItem.icon = menu.iconName;
246
+ menuItem.key = $cherry.locale[menu.name] ?? menu.name;
247
+ menuItem.keyword = `${suggest.keyword} ${menu.name}`;
248
+ menuItem.goLeft = menu.goLeft;
249
+ menuItem.goTop = menu.goTop;
250
+ menuItem.value = function (event, dom) {
251
+ menu.dom = dom;
252
+ return menu.onclick();
253
+ };
254
+ retList.push(menuItem);
255
+ });
256
+ } else {
257
+ suggest.icon = toolbar.iconName;
258
+ suggest.goLeft = toolbar.goLeft;
259
+ suggest.goTop = toolbar.goTop;
260
+ suggest.key = $cherry.locale[toolbar.name] ?? toolbar.name;
261
+ suggest.value = function (event, dom) {
262
+ toolbar.dom = dom;
263
+ toolbar.fire(event, toolbar);
264
+ return '';
265
+ };
266
+ retList.push(suggest);
267
+ }
268
+ }
269
+ } else {
270
+ suggest.key = $cherry.locale[suggest.key] ?? suggest.key;
271
+ retList.push(suggest);
272
+ }
273
+ });
274
+ return retList;
275
+ }
276
+ class SuggesterPanel {
277
+ constructor() {
278
+ this.searchCache = false;
279
+ this.searchKeyCache = [];
280
+ this.optionList = [];
281
+ this.cursorMove = true;
282
+ this.suggesterConfig = {};
283
+ this.$cherry = null;
284
+ }
285
+ setCherry($cherry) {
286
+ this.$cherry = $cherry;
287
+ }
288
+
289
+ /**
290
+ * 如果没有panel,则尝试初始化一个,在node模式不初始化
291
+ */
292
+ tryCreatePanel() {
293
+ if (!this.$suggesterPanel && isBrowser() && document) {
294
+ document?.body?.appendChild(this.createDom(this.panelWrap));
295
+ this.$suggesterPanel = document?.querySelector('.cherry-suggester-panel');
296
+ }
297
+ }
298
+
299
+ panelWrap = `<div class="cherry-suggester-panel cherry__theme__${Theme.getTheme()}"></div>`;
300
+
301
+ hasEditor() {
302
+ return !!this.editor && !!this.editor.editor.display && !!this.editor.editor.display.wrapper;
303
+ }
304
+
305
+ /**
306
+ * 设置编辑器
307
+ * @param {import('@/Editor').default} editor
308
+ */
309
+ setEditor(editor) {
310
+ this.editor = editor;
311
+ }
312
+
313
+ setSuggester(suggester) {
314
+ this.suggesterConfig = suggester;
315
+ }
316
+
317
+ bindEvent() {
318
+ let keyAction = false;
319
+ this.editor.editor.on('change', (codemirror, evt) => {
320
+ keyAction = true;
321
+ this.onCodeMirrorChange(codemirror, evt);
322
+ });
323
+
324
+ this.editor.editor.on('keydown', (codemirror, evt) => {
325
+ keyAction = true;
326
+ if (this.enableRelate()) {
327
+ this.onKeyDown(codemirror, evt);
328
+ }
329
+ });
330
+
331
+ this.editor.editor.on('cursorActivity', () => {
332
+ // 当编辑区光标位置改变时触发
333
+ if (!keyAction) {
334
+ this.stopRelate();
335
+ }
336
+ keyAction = false;
337
+ });
338
+
339
+ const extraKeys = this.editor.editor.getOption('extraKeys');
340
+ const decorateKeys = ['Up', 'Down', 'Enter'];
341
+ decorateKeys.forEach((key) => {
342
+ if (typeof extraKeys[key] === 'function') {
343
+ const proxyTarget = extraKeys[key];
344
+ extraKeys[key] = (codemirror) => {
345
+ if (this.cursorMove) {
346
+ const res = proxyTarget.call(codemirror, codemirror);
347
+
348
+ if (res) {
349
+ return res;
350
+ }
351
+ // logic to decide whether to move up or not
352
+ // return Pass.toString();
353
+ }
354
+ };
355
+ } else if (!extraKeys[key]) {
356
+ extraKeys[key] = () => {
357
+ if (this.cursorMove) {
358
+ // logic to decide whether to move up or not
359
+ return Pass.toString();
360
+ }
361
+ };
362
+ } else if (typeof extraKeys[key] === 'string') {
363
+ const command = extraKeys[key];
364
+ extraKeys[key] = (codemirror) => {
365
+ if (this.cursorMove) {
366
+ this.editor.editor.execCommand(command);
367
+
368
+ // logic to decide whether to move up or not
369
+ // return Pass.toString();
370
+ }
371
+ };
372
+ }
373
+ });
374
+
375
+ this.editor.editor.setOption('extraKeys', extraKeys);
376
+
377
+ this.editor.editor.on('scroll', (codemirror, evt) => {
378
+ if (!this.searchCache) {
379
+ return;
380
+ }
381
+ // 当编辑器滚动时触发
382
+ this.relocatePanel(this.editor.editor);
383
+ });
384
+
385
+ this.onClickPanelItem();
386
+ }
387
+
388
+ onClickPanelItem() {
389
+ this.tryCreatePanel();
390
+ this.$suggesterPanel.addEventListener(
391
+ 'click',
392
+ (evt) => {
393
+ const idx = isChildNode(this.$suggesterPanel, evt.target);
394
+ if (idx > -1) {
395
+ this.pasteSelectResult(idx, evt, evt.target);
396
+ }
397
+ this.stopRelate();
398
+ },
399
+ false,
400
+ );
401
+
402
+ function isChildNode(parent, node) {
403
+ let res = -1;
404
+ parent.childNodes.forEach((item, idx) => (item === node ? (res = idx) : ''));
405
+ return res;
406
+ }
407
+ }
408
+
409
+ showSuggesterPanel({ left, top, items }) {
410
+ this.tryCreatePanel();
411
+ if (!this.$suggesterPanel && isBrowser()) {
412
+ document.body.appendChild(this.createDom(this.panelWrap));
413
+ this.$suggesterPanel = document.querySelector('.cherry-suggester-panel');
414
+ }
415
+ this.updatePanel(items);
416
+ this.$suggesterPanel.style.left = `${left}px`;
417
+ this.$suggesterPanel.style.top = `${top}px`;
418
+ this.$suggesterPanel.style.display = 'block';
419
+ this.$suggesterPanel.style.position = 'absolute';
420
+ this.$suggesterPanel.style.zIndex = '100';
421
+ }
422
+
423
+ hideSuggesterPanel() {
424
+ this.tryCreatePanel();
425
+ // const $suggesterPanel = document.querySelector('.cherry-suggester-panel');
426
+ if (this.$suggesterPanel) {
427
+ this.$suggesterPanel.style.display = 'none';
428
+ }
429
+ }
430
+
431
+ /**
432
+ * 更新suggesterPanel
433
+ * @param {SuggestList} suggestList
434
+ */
435
+ updatePanel(suggestList) {
436
+ this.tryCreatePanel();
437
+ let defaultValue = suggestList
438
+ .map((suggest, idx) => {
439
+ if (typeof suggest === 'object' && suggest !== null) {
440
+ let renderContent = suggest.key;
441
+ if (suggest?.icon) {
442
+ renderContent = `<span class="material-symbols-outlined" style="transform: translateY(0%);top: 0;vertical-align: middle;">${suggest.icon}</span>${renderContent}`;
443
+ }
444
+ return this.renderPanelItem(renderContent, idx === 0);
445
+ }
446
+ return this.renderPanelItem(suggest, idx === 0);
447
+ })
448
+ .join('');
449
+ /**
450
+ * @type { SuggesterConfigItem }
451
+ */
452
+ const suggesterConfig = this.suggesterConfig[this.keyword];
453
+ // 用户自定义渲染逻辑 suggestListRender
454
+ if (suggesterConfig && typeof suggesterConfig.suggestListRender === 'function') {
455
+ defaultValue = suggesterConfig.suggestListRender.call(this, suggestList) || defaultValue;
456
+ }
457
+
458
+ this.$suggesterPanel.innerHTML = ''; // 清空
459
+ if (typeof defaultValue === 'string') {
460
+ this.$suggesterPanel.innerHTML = defaultValue;
461
+ } else if (Array.isArray(defaultValue) && defaultValue.length > 0) {
462
+ defaultValue.forEach((item) => {
463
+ this.$suggesterPanel.appendChild(item);
464
+ });
465
+ } else if (typeof defaultValue === 'object' && defaultValue.nodeType === 1) {
466
+ this.$suggesterPanel.appendChild(defaultValue);
467
+ }
468
+ }
469
+
470
+ /**
471
+ * 渲染suggesterPanel item
472
+ * @param {string} item 渲染内容
473
+ * @param {boolean} selected 是否选中
474
+ * @returns {string} html
475
+ */
476
+ renderPanelItem(item, selected) {
477
+ if (selected) {
478
+ return `<div class="cherry-suggester-panel__item cherry-suggester-panel__item--selected">${item}</div>`;
479
+ }
480
+ return `<div class="cherry-suggester-panel__item">${item}</div>`;
481
+ }
482
+
483
+ createDom(string = '') {
484
+ if (!this.template) {
485
+ this.template = document.createElement('div');
486
+ }
487
+
488
+ this.template.innerHTML = string.trim();
489
+
490
+ // Change this to div.childNodes to support multiple top-level nodes
491
+ const frag = document.createDocumentFragment();
492
+ Array.prototype.map.call(this.template.childNodes, (item, idx) => {
493
+ frag.appendChild(item);
494
+ });
495
+ return frag;
496
+ }
497
+
498
+ // 面板重定位
499
+ relocatePanel(codemirror) {
500
+ // 找到光标位置来确定候选框位置
501
+ let $cursor = document.querySelector('.CodeMirror-cursors .CodeMirror-cursor');
502
+ // 当editor选中某一内容时,".CodeMirror-cursor"会消失,此时通过定位".selected"来确定候选框位置
503
+ if (!$cursor) {
504
+ $cursor = document.querySelector('.CodeMirror-selected');
505
+ }
506
+ if (!$cursor) {
507
+ return false;
508
+ }
509
+ const pos = codemirror.getCursor();
510
+ const lineHeight = codemirror.lineInfo(pos.line).handle.height;
511
+ const rect = $cursor.getBoundingClientRect();
512
+ const top = rect.top + lineHeight;
513
+ const { left } = rect;
514
+ this.showSuggesterPanel({ left, top, items: this.optionList });
515
+ }
516
+
517
+ /**
518
+ * 获取光标位置
519
+ * @param {CodeMirror} codemirror
520
+ * @returns {{ left: number, top: number }}
521
+ */
522
+ getCursorPos(codemirror) {
523
+ const $cursor = document.querySelector('.CodeMirror-cursors .CodeMirror-cursor');
524
+ if (!$cursor) return null;
525
+ const pos = codemirror.getCursor();
526
+ const lineHeight = codemirror.lineInfo(pos.line).handle.height;
527
+ const rect = $cursor.getBoundingClientRect();
528
+ const top = rect.top + lineHeight;
529
+ const { left } = rect;
530
+ return { left, top };
531
+ }
532
+
533
+ // 开启关联
534
+ startRelate(codemirror, keyword, from) {
535
+ this.cursorFrom = from;
536
+ this.keyword = keyword;
537
+ this.searchCache = true;
538
+ this.relocatePanel(codemirror);
539
+ }
540
+
541
+ // 关闭关联
542
+ stopRelate() {
543
+ this.hideSuggesterPanel();
544
+ this.cursorFrom = null;
545
+ this.cursorTo = null;
546
+ this.keyword = '';
547
+ this.searchKeyCache = [];
548
+ this.searchCache = false;
549
+ this.cursorMove = true;
550
+ this.optionList = [];
551
+ }
552
+
553
+ /**
554
+ * 粘贴选择结果
555
+ * @param {number} idx 选择的结果索引
556
+ * @param {KeyboardEvent} evt 键盘事件
557
+ * @param dom
558
+ */
559
+ pasteSelectResult(idx, evt, dom) {
560
+ if (!this.cursorTo) {
561
+ this.cursorTo = JSON.parse(JSON.stringify(this.cursorFrom));
562
+ }
563
+ if (!this.cursorTo) {
564
+ return;
565
+ }
566
+ this.cursorTo.ch += 1;
567
+ const { cursorFrom, cursorTo } = this; // 缓存光标位置
568
+ if (this.optionList[idx]) {
569
+ let result = '';
570
+ if (
571
+ typeof this.optionList[idx] === 'object' &&
572
+ this.optionList[idx] !== null &&
573
+ typeof this.optionList[idx].value === 'string'
574
+ ) {
575
+ result = this.optionList[idx].value;
576
+ }
577
+ if (
578
+ typeof this.optionList[idx] === 'object' &&
579
+ this.optionList[idx] !== null &&
580
+ typeof this.optionList[idx].value === 'function'
581
+ ) {
582
+ this.editor.editor.replaceRange('', cursorFrom, cursorTo);
583
+ result = this.optionList[idx].value(evt, dom);
584
+ }
585
+ if (typeof this.optionList[idx] === 'string') {
586
+ result = ` ${this.keyword}${this.optionList[idx]} `;
587
+ }
588
+ if (result) {
589
+ this.editor.editor.replaceRange(result, cursorFrom, cursorTo);
590
+ }
591
+ // 控制光标左移一位或者选中某个范围
592
+ if (this.optionList[idx].goLeft) {
593
+ const cursor = this.editor.editor.getCursor();
594
+ this.editor.editor.setCursor(cursor.line, cursor.ch - this.optionList[idx].goLeft);
595
+ }
596
+ // 控制光标上移若干位
597
+ if (this.optionList[idx].goTop) {
598
+ const cursor = this.editor.editor.getCursor();
599
+ this.editor.editor.setCursor(cursor.line - this.optionList[idx].goTop, cursor.ch);
600
+ }
601
+ if (this.optionList[idx].selection) {
602
+ const { line } = this.editor.editor.getCursor();
603
+ const { ch } = this.editor.editor.getCursor();
604
+ this.editor.editor.setSelection(
605
+ { line, ch: ch - this.optionList[idx].selection.from },
606
+ { line, ch: ch - this.optionList[idx].selection.to },
607
+ );
608
+ }
609
+ }
610
+ }
611
+
612
+ /**
613
+ * 寻找当前选中项的索引
614
+ * @returns {number}
615
+ */
616
+ findSelectedItemIndex() {
617
+ return Array.prototype.findIndex.call(this.$suggesterPanel.childNodes, (item) =>
618
+ item.classList.contains('cherry-suggester-panel__item--selected'),
619
+ );
620
+ }
621
+
622
+ findSelectedItem() {
623
+ return Array.prototype.find.call(this.$suggesterPanel.childNodes, (item) =>
624
+ item.classList.contains('cherry-suggester-panel__item--selected'),
625
+ );
626
+ }
627
+
628
+ enableRelate() {
629
+ return this.searchCache;
630
+ }
631
+
632
+ /**
633
+ * codeMirror change事件
634
+ * @param {CodeMirror.Editor} codemirror
635
+ * @param {CodeMirror.EditorChange} evt
636
+ * @returns
637
+ */
638
+ onCodeMirrorChange(codemirror, evt) {
639
+ const { text, from, to, origin } = evt;
640
+ const changeValue = text.length === 1 ? text[0] : '';
641
+
642
+ // 首次输入命中关键词的时候开启联想
643
+ if (!this.enableRelate() && this.suggesterConfig[changeValue]) {
644
+ this.startRelate(codemirror, changeValue, from);
645
+ }
646
+ if (this.enableRelate() && (changeValue || origin === '+delete')) {
647
+ this.cursorTo = JSON.parse(JSON.stringify(to));
648
+ if (changeValue) {
649
+ this.searchKeyCache.push(changeValue);
650
+ } else if (origin === '+delete') {
651
+ this.searchKeyCache.pop();
652
+ if (this.searchKeyCache.length === 0) {
653
+ this.stopRelate();
654
+ return;
655
+ }
656
+ }
657
+ const that = this;
658
+ function callbackResult(res) {
659
+ if (res === false) {
660
+ that.stopRelate();
661
+ return;
662
+ }
663
+ // 回显命中的结果
664
+ that.optionList = !res || !res.length ? [] : res.slice(0, 200); // 只要前200个,太多影响性能
665
+ that.updatePanel(expandList(that.$cherry, that.optionList));
666
+ }
667
+ // 展示推荐列表
668
+ if (typeof this.suggesterConfig[this.keyword]?.data === 'function') {
669
+ // 请求api 返回结果拼凑
670
+ this.suggesterConfig[this.keyword].data(
671
+ this.searchKeyCache.join('').slice(1),
672
+ (res) => {
673
+ // 如果返回了false,则强制退出联想
674
+ callbackResult(res);
675
+ },
676
+ this.$cherry,
677
+ this.keyword,
678
+ );
679
+ } else if (typeof this.suggesterConfig[this.keyword]?.data === 'object') {
680
+ getSuggestList(
681
+ expandList(this.$cherry, this.suggesterConfig[this.keyword]?.data),
682
+ this.keyword,
683
+ this.searchKeyCache.join('').slice(1),
684
+ (res) => {
685
+ callbackResult(res);
686
+ },
687
+ );
688
+ }
689
+ }
690
+ }
691
+
692
+ /**
693
+ * 监听方向键选择 options
694
+ * @param {CodeMirror.Editor} codemirror
695
+ * @param {KeyboardEvent} evt
696
+ */
697
+ onKeyDown(codemirror, evt) {
698
+ this.tryCreatePanel();
699
+ if (!this.$suggesterPanel) {
700
+ return false;
701
+ }
702
+ const { keyCode } = evt;
703
+ // up down
704
+ if ([38, 40].includes(keyCode)) {
705
+ // issue 558
706
+ if (this.optionList.length === 0) {
707
+ setTimeout(() => {
708
+ this.stopRelate();
709
+ }, 0);
710
+ return;
711
+ }
712
+
713
+ this.cursorMove = false;
714
+
715
+ const selectedItem = this.$suggesterPanel.querySelector('.cherry-suggester-panel__item--selected');
716
+ let nextElement = null;
717
+ if (keyCode === 38 && !selectedItem.previousElementSibling) {
718
+ nextElement = this.$suggesterPanel.lastElementChild;
719
+ // codemirror.focus();
720
+ } else if (keyCode === 40 && !selectedItem.nextElementSibling) {
721
+ nextElement = this.$suggesterPanel.firstElementChild;
722
+ // codemirror.focus();
723
+ } else {
724
+ if (keyCode === 38) {
725
+ nextElement = selectedItem.previousElementSibling;
726
+ } else if (keyCode === 40) {
727
+ nextElement = selectedItem.nextElementSibling;
728
+ }
729
+ }
730
+
731
+ selectedItem.classList.remove('cherry-suggester-panel__item--selected');
732
+
733
+ if (nextElement === null) return;
734
+ nextElement.classList.add('cherry-suggester-panel__item--selected');
735
+ nextElement.scrollIntoView();
736
+ } else if (keyCode === 13) {
737
+ evt.stopPropagation();
738
+ this.cursorMove = false;
739
+ const dom = this.findSelectedItem();
740
+ this.pasteSelectResult(this.findSelectedItemIndex(), evt, dom);
741
+ //
742
+ codemirror.focus();
743
+ // const cache = JSON.parse(JSON.stringify(this.cursorTo));
744
+ // setTimeout(() => {
745
+ // codemirror.setCursor(cache);
746
+ // }, 100);
747
+ setTimeout(() => {
748
+ this.stopRelate();
749
+ }, 0);
750
+ } else if (keyCode === 27 || keyCode === 0x25 || keyCode === 0x27) {
751
+ // 按下esc或者←、→键的时候退出联想
752
+ evt.stopPropagation();
753
+ codemirror.focus();
754
+ setTimeout(() => {
755
+ this.stopRelate();
756
+ }, 0);
757
+ }
758
+ }
759
+ }