@cherry-markdown/cherry-markdown-dev 0.8.58-dev

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 (319) hide show
  1. package/package.json +149 -0
  2. package/src/Cherry.config.js +625 -0
  3. package/src/Cherry.js +1104 -0
  4. package/src/CherryStatic.js +70 -0
  5. package/src/Editor.js +748 -0
  6. package/src/Engine.js +381 -0
  7. package/src/Event.js +140 -0
  8. package/src/Factory.js +180 -0
  9. package/src/Logger.js +31 -0
  10. package/src/Previewer.js +1183 -0
  11. package/src/Sanitizer.js +4 -0
  12. package/src/Sanitizer.node.js +7 -0
  13. package/src/UrlCache.js +98 -0
  14. package/src/addons/advance/cherry-table-echarts-plugin.js +170 -0
  15. package/src/addons/cherry-code-block-mermaid-plugin.js +158 -0
  16. package/src/addons/cherry-code-block-plantuml-plugin.js +106 -0
  17. package/src/core/HookCenter.js +297 -0
  18. package/src/core/HooksConfig.js +100 -0
  19. package/src/core/ParagraphBase.js +332 -0
  20. package/src/core/SentenceBase.js +65 -0
  21. package/src/core/SyntaxBase.js +194 -0
  22. package/src/core/hooks/AutoLink.js +232 -0
  23. package/src/core/hooks/BackgroundColor.js +46 -0
  24. package/src/core/hooks/Blockquote.js +70 -0
  25. package/src/core/hooks/Br.js +85 -0
  26. package/src/core/hooks/CodeBlock.js +446 -0
  27. package/src/core/hooks/Color.js +46 -0
  28. package/src/core/hooks/CommentReference.js +96 -0
  29. package/src/core/hooks/Detail.js +108 -0
  30. package/src/core/hooks/Emoji.config.js +1825 -0
  31. package/src/core/hooks/Emoji.js +119 -0
  32. package/src/core/hooks/Emphasis.js +113 -0
  33. package/src/core/hooks/Footnote.js +125 -0
  34. package/src/core/hooks/FrontMatter.js +51 -0
  35. package/src/core/hooks/Header.js +234 -0
  36. package/src/core/hooks/HighLight.js +37 -0
  37. package/src/core/hooks/Hr.js +52 -0
  38. package/src/core/hooks/HtmlBlock.js +184 -0
  39. package/src/core/hooks/Image.js +174 -0
  40. package/src/core/hooks/InlineCode.js +48 -0
  41. package/src/core/hooks/InlineMath.js +107 -0
  42. package/src/core/hooks/Link.js +160 -0
  43. package/src/core/hooks/List.js +264 -0
  44. package/src/core/hooks/MathBlock.js +103 -0
  45. package/src/core/hooks/Panel.js +145 -0
  46. package/src/core/hooks/Paragraph.js +84 -0
  47. package/src/core/hooks/Ruby.js +34 -0
  48. package/src/core/hooks/Size.js +51 -0
  49. package/src/core/hooks/Strikethrough.js +54 -0
  50. package/src/core/hooks/Sub.js +47 -0
  51. package/src/core/hooks/SuggestList.js +333 -0
  52. package/src/core/hooks/Suggester.js +707 -0
  53. package/src/core/hooks/Sup.js +47 -0
  54. package/src/core/hooks/Table.js +275 -0
  55. package/src/core/hooks/Toc.js +292 -0
  56. package/src/core/hooks/Transfer.js +47 -0
  57. package/src/core/hooks/Underline.js +37 -0
  58. package/src/index.core.js +29 -0
  59. package/src/index.engine.core.js +68 -0
  60. package/src/index.engine.js +28 -0
  61. package/src/index.js +32 -0
  62. package/src/libs/mermaidAPI.8.4.8.js +1 -0
  63. package/src/libs/mermaidAPI.8.5.2.js +42 -0
  64. package/src/libs/rawdeflate.js +1663 -0
  65. package/src/locales/en_US.js +139 -0
  66. package/src/locales/index.js +25 -0
  67. package/src/locales/ru_RU.js +139 -0
  68. package/src/locales/zh_CN.js +142 -0
  69. package/src/sass/base.scss +26 -0
  70. package/src/sass/bubble_formula.scss +166 -0
  71. package/src/sass/ch-icon.scss +118 -0
  72. package/src/sass/cherry.scss +1116 -0
  73. package/src/sass/components/bubble.scss +173 -0
  74. package/src/sass/components/shortcut_key_config.scss +108 -0
  75. package/src/sass/formula_utils_bubble.scss +82 -0
  76. package/src/sass/icon_template.scss +24 -0
  77. package/src/sass/icons/uEA03-list.svg +19 -0
  78. package/src/sass/icons/uEA04-check.svg +14 -0
  79. package/src/sass/icons/uEA09-square.svg +10 -0
  80. package/src/sass/icons/uEA0A-bold.svg +20 -0
  81. package/src/sass/icons/uEA0B-code.svg +18 -0
  82. package/src/sass/icons/uEA0C-color.svg +13 -0
  83. package/src/sass/icons/uEA0D-header.svg +8 -0
  84. package/src/sass/icons/uEA0E-image.svg +15 -0
  85. package/src/sass/icons/uEA0F-italic.svg +8 -0
  86. package/src/sass/icons/uEA10-link.svg +16 -0
  87. package/src/sass/icons/uEA11-ol.svg +21 -0
  88. package/src/sass/icons/uEA12-size.svg +11 -0
  89. package/src/sass/icons/uEA13-strike.svg +16 -0
  90. package/src/sass/icons/uEA14-table.svg +12 -0
  91. package/src/sass/icons/uEA15-ul.svg +17 -0
  92. package/src/sass/icons/uEA16-underline.svg +13 -0
  93. package/src/sass/icons/uEA17-word.svg +16 -0
  94. package/src/sass/icons/uEA18-blockquote.svg +11 -0
  95. package/src/sass/icons/uEA19-font.svg +10 -0
  96. package/src/sass/icons/uEA1F-insertClass.svg +39 -0
  97. package/src/sass/icons/uEA20-insertFlow.svg +8 -0
  98. package/src/sass/icons/uEA21-insertFormula.svg +23 -0
  99. package/src/sass/icons/uEA22-insertGantt.svg +13 -0
  100. package/src/sass/icons/uEA23-insertGraph.svg +13 -0
  101. package/src/sass/icons/uEA24-insertPie.svg +19 -0
  102. package/src/sass/icons/uEA25-insertSeq.svg +20 -0
  103. package/src/sass/icons/uEA26-insertState.svg +35 -0
  104. package/src/sass/icons/uEA27-line.svg +11 -0
  105. package/src/sass/icons/uEA28-preview.svg +18 -0
  106. package/src/sass/icons/uEA29-previewClose.svg +24 -0
  107. package/src/sass/icons/uEA2A-toc.svg +24 -0
  108. package/src/sass/icons/uEA2D-sub.svg +15 -0
  109. package/src/sass/icons/uEA2E-sup.svg +15 -0
  110. package/src/sass/icons/uEA2F-h1.svg +16 -0
  111. package/src/sass/icons/uEA30-h2.svg +20 -0
  112. package/src/sass/icons/uEA31-h3.svg +23 -0
  113. package/src/sass/icons/uEA32-h4.svg +16 -0
  114. package/src/sass/icons/uEA33-h5.svg +20 -0
  115. package/src/sass/icons/uEA34-h6.svg +17 -0
  116. package/src/sass/icons/uEA35-video.svg +20 -0
  117. package/src/sass/icons/uEA36-insert.svg +25 -0
  118. package/src/sass/icons/uEA37-little_table.svg +30 -0
  119. package/src/sass/icons/uEA38-pdf.svg +27 -0
  120. package/src/sass/icons/uEA39-checklist.svg +22 -0
  121. package/src/sass/icons/uEA40-close.svg +12 -0
  122. package/src/sass/icons/uEA41-fullscreen.svg +81 -0
  123. package/src/sass/icons/uEA42-minscreen.svg +77 -0
  124. package/src/sass/icons/uEA43-insertChart.svg +23 -0
  125. package/src/sass/icons/uEA44-question.svg +25 -0
  126. package/src/sass/icons/uEA45-settings.svg +32 -0
  127. package/src/sass/icons/uEA46-ok.svg +7 -0
  128. package/src/sass/icons/uEA47-br.svg +22 -0
  129. package/src/sass/icons/uEA48-normal.svg +15 -0
  130. package/src/sass/icons/uEA49-undo.svg +19 -0
  131. package/src/sass/icons/uEA50-redo.svg +21 -0
  132. package/src/sass/icons/uEA51-copy.svg +6 -0
  133. package/src/sass/icons/uEA52-phone.svg +5 -0
  134. package/src/sass/icons/uEA53-cherry-table-delete.svg +17 -0
  135. package/src/sass/icons/uEA54-cherry-table-insert-bottom.svg +16 -0
  136. package/src/sass/icons/uEA55-cherry-table-insert-left.svg +15 -0
  137. package/src/sass/icons/uEA56-cherry-table-insert-right.svg +16 -0
  138. package/src/sass/icons/uEA57-cherry-table-insert-top.svg +16 -0
  139. package/src/sass/icons/uEA58-sort-s.svg +13 -0
  140. package/src/sass/icons/uEA59-pinyin.svg +1 -0
  141. package/src/sass/icons/uEA5A-create.svg +24 -0
  142. package/src/sass/icons/uEA5B-download.svg +34 -0
  143. package/src/sass/icons/uEA5C-edit.svg +3 -0
  144. package/src/sass/icons/uEA5D-export.svg +53 -0
  145. package/src/sass/icons/uEA5E-folder-open.svg +3 -0
  146. package/src/sass/icons/uEA5F-folder.svg +3 -0
  147. package/src/sass/icons/uEA60-help.svg +5 -0
  148. package/src/sass/icons/uEA61-pen-fill.svg +13 -0
  149. package/src/sass/icons/uEA62-pen.svg +3 -0
  150. package/src/sass/icons/uEA64-tips.svg +5 -0
  151. package/src/sass/icons/uEA65-warn.svg +5 -0
  152. package/src/sass/icons/uEA66-mistake.svg +4 -0
  153. package/src/sass/icons/uEA67-success.svg +4 -0
  154. package/src/sass/icons/uEA68-danger.svg +4 -0
  155. package/src/sass/icons/uEA69-info.svg +5 -0
  156. package/src/sass/icons/uEA6A-primary.svg +5 -0
  157. package/src/sass/icons/uEA6B-warning.svg +5 -0
  158. package/src/sass/icons/uEA6C-justify.svg +19 -0
  159. package/src/sass/icons/uEA6D-justifyCenter.svg +19 -0
  160. package/src/sass/icons/uEA6E-justifyLeft.svg +19 -0
  161. package/src/sass/icons/uEA6F-justifyRight.svg +19 -0
  162. package/src/sass/icons/uEA70-chevronsLeft.svg +1 -0
  163. package/src/sass/icons/uEA71-chevronsRight.svg +1 -0
  164. package/src/sass/icons/uEA72-trendingUp.svg +1 -0
  165. package/src/sass/icons/uEA74-codeBlock.svg +1 -0
  166. package/src/sass/icons/uEA75-expand.svg +3 -0
  167. package/src/sass/icons/uEA76-unExpand.svg +3 -0
  168. package/src/sass/icons/uEA77-swap-vert.svg +1 -0
  169. package/src/sass/icons/uEA78-swap.svg +1 -0
  170. package/src/sass/icons/uEA79-keyboard.svg +1 -0
  171. package/src/sass/icons/uEA7A-command.svg +1 -0
  172. package/src/sass/icons/uEA7B-search.svg +1 -0
  173. package/src/sass/index.scss +3 -0
  174. package/src/sass/markdown.scss +668 -0
  175. package/src/sass/markdown_pure.scss +9 -0
  176. package/src/sass/prettyprint/prettyprint.scss +118 -0
  177. package/src/sass/previewer.scss +179 -0
  178. package/src/sass/print.scss +13 -0
  179. package/src/sass/prism/coy.scss +220 -0
  180. package/src/sass/prism/dark.scss +132 -0
  181. package/src/sass/prism/default.scss +143 -0
  182. package/src/sass/prism/funky.scss +133 -0
  183. package/src/sass/prism/okaidia.scss +126 -0
  184. package/src/sass/prism/one-dark.scss +440 -0
  185. package/src/sass/prism/one-light.scss +428 -0
  186. package/src/sass/prism/solarized-light.scss +153 -0
  187. package/src/sass/prism/tomorrow-night.scss +125 -0
  188. package/src/sass/prism/twilight.scss +202 -0
  189. package/src/sass/prism/vs-dark.scss +275 -0
  190. package/src/sass/prism/vs-light.scss +168 -0
  191. package/src/sass/themes/blue.scss +411 -0
  192. package/src/sass/themes/dark.scss +517 -0
  193. package/src/sass/themes/default.scss +255 -0
  194. package/src/sass/themes/green.scss +395 -0
  195. package/src/sass/themes/light.scss +368 -0
  196. package/src/sass/themes/red.scss +397 -0
  197. package/src/sass/themes/violet.scss +410 -0
  198. package/src/sass/variable.scss +84 -0
  199. package/src/toolbars/Bubble.js +234 -0
  200. package/src/toolbars/BubbleFormula.js +298 -0
  201. package/src/toolbars/BubbleTable.js +147 -0
  202. package/src/toolbars/FloatMenu.js +131 -0
  203. package/src/toolbars/HiddenToolbar.js +36 -0
  204. package/src/toolbars/HookCenter.js +234 -0
  205. package/src/toolbars/MenuBase.js +569 -0
  206. package/src/toolbars/PreviewerBubble.js +608 -0
  207. package/src/toolbars/ShortcutKeyConfigPanel.js +345 -0
  208. package/src/toolbars/Sidebar.js +36 -0
  209. package/src/toolbars/Toc.js +242 -0
  210. package/src/toolbars/Toolbar.js +449 -0
  211. package/src/toolbars/ToolbarRight.js +37 -0
  212. package/src/toolbars/hooks/Audio.js +79 -0
  213. package/src/toolbars/hooks/BarTable.js +41 -0
  214. package/src/toolbars/hooks/Bold.js +73 -0
  215. package/src/toolbars/hooks/Br.js +34 -0
  216. package/src/toolbars/hooks/ChangeLocale.js +62 -0
  217. package/src/toolbars/hooks/ChatGpt.js +182 -0
  218. package/src/toolbars/hooks/CheckList.js +41 -0
  219. package/src/toolbars/hooks/Code.js +49 -0
  220. package/src/toolbars/hooks/CodeTheme.js +66 -0
  221. package/src/toolbars/hooks/Color.js +298 -0
  222. package/src/toolbars/hooks/Copy.js +141 -0
  223. package/src/toolbars/hooks/Detail.js +69 -0
  224. package/src/toolbars/hooks/DrawIo.js +57 -0
  225. package/src/toolbars/hooks/Export.js +49 -0
  226. package/src/toolbars/hooks/File.js +79 -0
  227. package/src/toolbars/hooks/Formula.js +69 -0
  228. package/src/toolbars/hooks/FullScreen.js +50 -0
  229. package/src/toolbars/hooks/Graph.js +263 -0
  230. package/src/toolbars/hooks/H1.js +71 -0
  231. package/src/toolbars/hooks/H2.js +71 -0
  232. package/src/toolbars/hooks/H3.js +71 -0
  233. package/src/toolbars/hooks/Header.js +118 -0
  234. package/src/toolbars/hooks/Hr.js +35 -0
  235. package/src/toolbars/hooks/Image.js +91 -0
  236. package/src/toolbars/hooks/InlineCode.js +53 -0
  237. package/src/toolbars/hooks/Insert.js +193 -0
  238. package/src/toolbars/hooks/Italic.js +72 -0
  239. package/src/toolbars/hooks/Justify.js +49 -0
  240. package/src/toolbars/hooks/LineTable.js +41 -0
  241. package/src/toolbars/hooks/Link.js +49 -0
  242. package/src/toolbars/hooks/List.js +55 -0
  243. package/src/toolbars/hooks/MobilePreview.js +44 -0
  244. package/src/toolbars/hooks/Ol.js +41 -0
  245. package/src/toolbars/hooks/Panel.js +140 -0
  246. package/src/toolbars/hooks/Pdf.js +78 -0
  247. package/src/toolbars/hooks/Publish.js +123 -0
  248. package/src/toolbars/hooks/QuickTable.js +43 -0
  249. package/src/toolbars/hooks/Quote.js +45 -0
  250. package/src/toolbars/hooks/Redo.js +33 -0
  251. package/src/toolbars/hooks/Ruby.js +59 -0
  252. package/src/toolbars/hooks/Search.js +53 -0
  253. package/src/toolbars/hooks/Settings.js +220 -0
  254. package/src/toolbars/hooks/ShortcutKey.js +62 -0
  255. package/src/toolbars/hooks/Size.js +118 -0
  256. package/src/toolbars/hooks/Split.js +37 -0
  257. package/src/toolbars/hooks/Strikethrough.js +71 -0
  258. package/src/toolbars/hooks/Sub.js +58 -0
  259. package/src/toolbars/hooks/Sup.js +58 -0
  260. package/src/toolbars/hooks/SwitchModel.js +56 -0
  261. package/src/toolbars/hooks/Table.js +56 -0
  262. package/src/toolbars/hooks/Theme.js +62 -0
  263. package/src/toolbars/hooks/Toc.js +35 -0
  264. package/src/toolbars/hooks/TogglePreview.js +91 -0
  265. package/src/toolbars/hooks/Ul.js +41 -0
  266. package/src/toolbars/hooks/Underline.js +68 -0
  267. package/src/toolbars/hooks/Undo.js +30 -0
  268. package/src/toolbars/hooks/Video.js +79 -0
  269. package/src/toolbars/hooks/Word.js +78 -0
  270. package/src/toolbars/hooks/WordCount.js +106 -0
  271. package/src/utils/autoindent.js +58 -0
  272. package/src/utils/cm-search-replace.js +794 -0
  273. package/src/utils/code-preview-language-setting.js +180 -0
  274. package/src/utils/codeBlockContentHandler.js +400 -0
  275. package/src/utils/config.js +174 -0
  276. package/src/utils/copy.js +55 -0
  277. package/src/utils/dialog.js +214 -0
  278. package/src/utils/dom.js +163 -0
  279. package/src/utils/downloadUtil.js +23 -0
  280. package/src/utils/env.js +22 -0
  281. package/src/utils/error.js +61 -0
  282. package/src/utils/event.js +38 -0
  283. package/src/utils/export.js +166 -0
  284. package/src/utils/file.js +164 -0
  285. package/src/utils/formulaUtilsHandler.js +232 -0
  286. package/src/utils/htmlparser.js +976 -0
  287. package/src/utils/image.js +99 -0
  288. package/src/utils/imgSizeHandler.js +279 -0
  289. package/src/utils/lazyLoadImg.js +327 -0
  290. package/src/utils/lineFeed.js +49 -0
  291. package/src/utils/listContentHandler.js +227 -0
  292. package/src/utils/lookbehind-replace.js +81 -0
  293. package/src/utils/mathjax.js +89 -0
  294. package/src/utils/myersDiff.js +211 -0
  295. package/src/utils/pasteHelper.js +253 -0
  296. package/src/utils/platformTransform.js +71 -0
  297. package/src/utils/recount-pos.js +59 -0
  298. package/src/utils/regexp.js +295 -0
  299. package/src/utils/sanitize.js +477 -0
  300. package/src/utils/selection.js +50 -0
  301. package/src/utils/shortcutKey.js +291 -0
  302. package/src/utils/svgUtils.js +96 -0
  303. package/src/utils/tableContentHandler.js +876 -0
  304. package/test/core/CommonMark.spec.ts +62 -0
  305. package/test/core/hooks/AutoLink.spec.ts +28 -0
  306. package/test/core/hooks/List.spec.ts +79 -0
  307. package/test/core/hooks/__snapshots__/List.spec.ts.snap +11 -0
  308. package/test/example.md +778 -0
  309. package/test/node.js +10 -0
  310. package/test/suites/commonmark.spec.json +5218 -0
  311. package/test/tsconfig.test.json +6 -0
  312. package/test/utils/regexp.spec.ts +28 -0
  313. package/types/cherry.d.ts +675 -0
  314. package/types/codemirror.d.ts +22 -0
  315. package/types/editor.d.ts +72 -0
  316. package/types/global.d.ts +16 -0
  317. package/types/menus.d.ts +24 -0
  318. package/types/previewer.d.ts +53 -0
  319. package/types/syntax.d.ts +52 -0
@@ -0,0 +1,1183 @@
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
+ import vDH from 'virtual-dom/h';
17
+ import vDDiff from 'virtual-dom/diff';
18
+ import vDPatch from 'virtual-dom/patch';
19
+ import MyersDiff from './utils/myersDiff';
20
+ import { getBlockTopAndHeightWithMargin } from './utils/dom';
21
+ import Logger from './Logger';
22
+ // import locale from './utils/locale';
23
+ import { addEvent, removeEvent } from './utils/event';
24
+ import { exportPDF, exportScreenShot, exportMarkdownFile, exportHTMLFile } from './utils/export';
25
+ import PreviewerBubble from './toolbars/PreviewerBubble';
26
+ import LazyLoadImg from '@/utils/lazyLoadImg';
27
+
28
+ let onScroll = () => {}; // store in memory for remove event
29
+
30
+ /**
31
+ * 作用:
32
+ * dom更新
33
+ * 局部加载(分片)
34
+ * 与左侧输入区域滚动同步
35
+ */
36
+ export default class Previewer {
37
+ /**
38
+ * @property
39
+ * @private
40
+ * @type {boolean} 等待预览区域更新。预览区域更新时,预览区的滚动不会引起编辑器滚动,避免因插入的元素高度变化导致编辑区域跳动
41
+ */
42
+ applyingDomChanges = false;
43
+
44
+ /**
45
+ * @property
46
+ * @private
47
+ * @type {number} 释放同步滚动锁定的定时器ID
48
+ */
49
+ syncScrollLockTimer = 0;
50
+
51
+ /**
52
+ * @property
53
+ * @public
54
+ * @type {boolean} 是否为移动端预览模式
55
+ */
56
+ isMobilePreview = false;
57
+
58
+ /**
59
+ *
60
+ * @param {Partial<import('~types/previewer').PreviewerOptions>} options 预览区域设置
61
+ */
62
+ constructor(options) {
63
+ /**
64
+ * @property
65
+ * @type {import('~types/previewer').PreviewerOptions}
66
+ */
67
+ this.options = {
68
+ previewerDom: document.createElement('div'),
69
+ virtualDragLineDom: document.createElement('div'),
70
+ editorMaskDom: document.createElement('div'),
71
+ previewerMaskDom: document.createElement('div'),
72
+ minBlockPercentage: 0.2, // editor或previewer所占宽度比例的最小值
73
+ value: '',
74
+ enablePreviewerBubble: true,
75
+ floatWhenClosePreviewer: false, // 是否在关闭预览区时,将预览区浮动
76
+ afterUpdateCallBack: [],
77
+ isPreviewOnly: false,
78
+ previewerCache: {
79
+ // 关闭/开启预览区时缓存的previewer数据
80
+ html: '',
81
+ htmlChanged: false,
82
+ layout: {},
83
+ },
84
+ /**
85
+ * 配置图片懒加载的逻辑
86
+ * 如果不希望图片懒加载,可配置成 lazyLoadImg = {maxNumPerTime: 6, autoLoadImgNum: -1}
87
+ */
88
+ lazyLoadImg: {
89
+ // 加载图片时如果需要展示loading图,则配置loading图的地址
90
+ loadingImgPath: '',
91
+ // 同一时间最多有几个图片请求,最大同时加载6张图片
92
+ maxNumPerTime: 2,
93
+ // 不进行懒加载处理的图片数量,如果为0,即所有图片都进行懒加载处理, 如果设置为-1,则所有图片都不进行懒加载处理
94
+ noLoadImgNum: 5,
95
+ // 首次自动加载几张图片(不论图片是否滚动到视野内),autoLoadImgNum = -1 表示会自动加载完所有图片
96
+ autoLoadImgNum: 5,
97
+ // 针对加载失败的图片 或 beforeLoadOneImgCallback 返回false 的图片,最多尝试加载几次,为了防止死循环,最多5次。以图片的src为纬度统计重试次数
98
+ maxTryTimesPerSrc: 2,
99
+ // 加载一张图片之前的回调函数,函数return false 会终止加载操作
100
+ beforeLoadOneImgCallback: (img) => {},
101
+ // 加载一张图片失败之后的回调函数
102
+ failLoadOneImgCallback: (img) => {},
103
+ // 加载一张图片之后的回调函数,如果图片加载失败,则不会回调该函数
104
+ afterLoadOneImgCallback: (img) => {},
105
+ // 加载完所有图片后调用的回调函数
106
+ afterLoadAllImgCallback: () => {},
107
+ },
108
+ };
109
+
110
+ Object.assign(this.options, options);
111
+ this.$cherry = this.options.$cherry;
112
+ this.instanceId = this.$cherry.getInstanceId();
113
+ /**
114
+ * @property
115
+ * @private
116
+ * @type {{ timer?: number; destinationTop?: number }}
117
+ */
118
+ this.animation = {};
119
+ }
120
+
121
+ init(editor) {
122
+ /**
123
+ * @property
124
+ * @private
125
+ * @type {boolean} 禁用滚动事件监听
126
+ */
127
+ this.disableScrollListener = false;
128
+ this.bindScroll();
129
+ this.editor = editor;
130
+ this.bindDrag();
131
+ this.$initPreviewerBubble();
132
+ this.lazyLoadImg = new LazyLoadImg(this.options.lazyLoadImg, this);
133
+ this.lazyLoadImg.doLazyLoad();
134
+ this.bindClick();
135
+ this.onMouseDown();
136
+ this.onSizeChange();
137
+ }
138
+
139
+ /**
140
+ * “监听”编辑器的尺寸变化,变化时更新拖拽条的位置
141
+ */
142
+ onSizeChange() {
143
+ // 创建一个新的 ResizeObserver 实例
144
+ const resizeObserver = new ResizeObserver(() => {
145
+ this.syncVirtualLayoutFromReal();
146
+ this.subMenusPositionChange();
147
+ });
148
+ // 开始监听元素
149
+ resizeObserver.observe(this.$cherry.wrapperDom);
150
+ }
151
+
152
+ subMenusPositionChange() {
153
+ ['toolbar', 'sidebar', 'toolbarRight'].forEach((toolbarName) => {
154
+ if (this.$cherry[toolbarName]) {
155
+ this.$cherry[toolbarName].updateSubMenuPosition();
156
+ }
157
+ });
158
+ }
159
+
160
+ $initPreviewerBubble() {
161
+ this.previewerBubble = new PreviewerBubble(this);
162
+ }
163
+
164
+ /**
165
+ * @returns {HTMLElement}
166
+ */
167
+ getDomContainer() {
168
+ return this.isMobilePreview
169
+ ? this.options.previewerDom.querySelector('.cherry-mobile-previewer-content')
170
+ : this.options.previewerDom;
171
+ }
172
+
173
+ getDom() {
174
+ return this.options.previewerDom;
175
+ }
176
+
177
+ /**
178
+ * 获取预览区内的html内容
179
+ * @param {boolean} wrapTheme 是否在外层包裹主题class
180
+ * @returns html内容
181
+ */
182
+ getValue(wrapTheme = true) {
183
+ let html = '';
184
+ if (this.isPreviewerHidden()) {
185
+ html = this.options.previewerCache.html;
186
+ } else {
187
+ html = this.getDomContainer().innerHTML;
188
+ }
189
+ // 需要未加载的图片替换成原始图片
190
+ html = this.lazyLoadImg.changeDataSrc2Src(html);
191
+ if (!wrapTheme || !this.$cherry.wrapperDom) {
192
+ return html;
193
+ }
194
+ const inlineCodeTheme = this.$cherry.wrapperDom.getAttribute('data-inline-code-theme');
195
+ const codeBlockTheme = this.$cherry.wrapperDom.getAttribute('data-code-block-theme');
196
+ return `<div data-inline-code-theme="${inlineCodeTheme}" data-code-block-theme="${codeBlockTheme}">${html}</div>`;
197
+ }
198
+
199
+ isPreviewerHidden() {
200
+ return this.options.previewerDom.classList.contains('cherry-previewer--hidden');
201
+ }
202
+
203
+ isPreviewerFloat() {
204
+ const floatDom = this.$cherry.cherryDom.querySelector('.float-previewer-wrap');
205
+ return this.$cherry.cherryDom.contains(floatDom);
206
+ }
207
+
208
+ isPreviewerNeedFloat() {
209
+ return this.options.floatWhenClosePreviewer;
210
+ }
211
+
212
+ calculateRealLayout(editorWidth) {
213
+ // 根据editor的绝对宽度计算editor和previewer的百分比宽度
214
+ const editorDomWidth = this.editor.options.editorDom.getBoundingClientRect().width;
215
+ const previewerDomWidth = this.options.previewerDom.getBoundingClientRect().width;
216
+ const totalWidth = editorDomWidth + previewerDomWidth;
217
+ let editorPercentage = +(editorWidth / totalWidth).toFixed(3);
218
+ if (editorPercentage < this.options.minBlockPercentage) {
219
+ editorPercentage = +this.options.minBlockPercentage.toFixed(3);
220
+ } else if (editorPercentage > 1 - this.options.minBlockPercentage) {
221
+ editorPercentage = +(1 - this.options.minBlockPercentage).toFixed(3);
222
+ }
223
+ const previewerPercentage = +(1 - editorPercentage).toFixed(3);
224
+ const res = {
225
+ editorPercentage: `${editorPercentage * 100}%`,
226
+ previewerPercentage: `${previewerPercentage * 100}%`,
227
+ };
228
+ return res;
229
+ }
230
+
231
+ setRealLayout(editorPercentage, previewerPercentage) {
232
+ // 主动设置editor,previewer宽度,按百分比计算
233
+ let $editorPercentage = editorPercentage;
234
+ let $previewerPercentage = previewerPercentage;
235
+ if (!$editorPercentage || !$previewerPercentage) {
236
+ $editorPercentage = '50%';
237
+ $previewerPercentage = '50%';
238
+ }
239
+ this.editor.options.editorDom.style.width = $editorPercentage;
240
+ this.options.previewerDom.style.width = $previewerPercentage;
241
+
242
+ this.syncVirtualLayoutFromReal();
243
+ }
244
+
245
+ syncVirtualLayoutFromReal() {
246
+ // 通过editor和previewer的百分比宽度,同步更新mask和dragLine的px宽度及位置
247
+ const editorPos = this.editor.options.editorDom.getBoundingClientRect();
248
+ const previewerPos = this.options.previewerDom.getBoundingClientRect();
249
+ const editorHeight = editorPos.height;
250
+ const editorTop = this.editor.options.editorDom.offsetTop;
251
+ const editorLeft = editorPos.left;
252
+ const editorWidth = editorPos.width;
253
+ const previewerLeft = previewerPos.left ? previewerPos.left - editorLeft : 0;
254
+ const previewerWidth = previewerPos.width || 0;
255
+
256
+ const { editorMaskDom, previewerMaskDom, virtualDragLineDom: virtualLineDom } = this.options;
257
+
258
+ this.$tryChangeValue(virtualLineDom, 'top', `${editorTop}px`);
259
+ this.$tryChangeValue(virtualLineDom, 'left', `${previewerLeft}px`);
260
+ this.$tryChangeValue(virtualLineDom, 'bottom', '0px');
261
+
262
+ this.$tryChangeValue(editorMaskDom, 'height', `${editorHeight}px`);
263
+ this.$tryChangeValue(editorMaskDom, 'top', `${editorTop}px`);
264
+ this.$tryChangeValue(editorMaskDom, 'left', '0px');
265
+ this.$tryChangeValue(editorMaskDom, 'width', `${editorWidth}px`);
266
+
267
+ this.$tryChangeValue(previewerMaskDom, 'height', `${editorHeight}px`);
268
+ this.$tryChangeValue(previewerMaskDom, 'top', `${editorTop}px`);
269
+ this.$tryChangeValue(previewerMaskDom, 'left', `${previewerLeft}px`);
270
+ this.$tryChangeValue(previewerMaskDom, 'width', `${previewerWidth}px`);
271
+ }
272
+
273
+ $tryChangeValue(obj, key, value) {
274
+ if (obj.style[key] !== value) {
275
+ obj.style[key] = value;
276
+ }
277
+ }
278
+
279
+ calculateVirtualLayout(editorLeft, editorRight) {
280
+ // 计算mask和dragline应处在的位置,按px计算
281
+ const editorDomWidth = this.editor.options.editorDom.getBoundingClientRect().width;
282
+ const previewerDomWidth = this.options.previewerDom.getBoundingClientRect().width;
283
+ const totalWidth = editorDomWidth + previewerDomWidth;
284
+ const startWidth = editorLeft.toFixed(0);
285
+ let leftWidth = editorRight - editorLeft;
286
+ if (leftWidth < totalWidth * this.options.minBlockPercentage) {
287
+ leftWidth = +(totalWidth * this.options.minBlockPercentage).toFixed(0);
288
+ } else if (leftWidth > totalWidth * (1 - this.options.minBlockPercentage)) {
289
+ leftWidth = +(totalWidth * (1 - this.options.minBlockPercentage)).toFixed(0);
290
+ }
291
+ const rightWidth = totalWidth - leftWidth;
292
+ const ret = {
293
+ startWidth: parseInt(startWidth, 10), // 起始位置(左侧留白)
294
+ leftWidth, // 左侧mask宽度
295
+ rightWidth, // 右侧mask宽度
296
+ };
297
+ return ret;
298
+ }
299
+
300
+ setVirtualLayout(startWidth, leftWidth, rightWidth) {
301
+ // 主动设置mask和dragLine位置,按px计算
302
+ const { editorMaskDom, previewerMaskDom, virtualDragLineDom: virtualLineDom } = this.options;
303
+ const $startWidth = 0; // =startWidth
304
+
305
+ editorMaskDom.style.left = `${$startWidth}px`;
306
+ editorMaskDom.style.width = `${leftWidth}px`;
307
+
308
+ virtualLineDom.style.left = `${$startWidth + leftWidth}px`;
309
+
310
+ previewerMaskDom.style.left = `${$startWidth + leftWidth}px`;
311
+ previewerMaskDom.style.width = `${rightWidth}px`;
312
+ }
313
+
314
+ bindDrag() {
315
+ const dragLineMouseMove = (mouseMoveEvent) => {
316
+ // 阻止事件冒泡
317
+ if (mouseMoveEvent && mouseMoveEvent.stopPropagation) {
318
+ mouseMoveEvent.stopPropagation();
319
+ } else {
320
+ mouseMoveEvent.cancelBubble = true;
321
+ }
322
+ // 取消默认事件
323
+ if (mouseMoveEvent.preventDefault) {
324
+ mouseMoveEvent.preventDefault();
325
+ } else {
326
+ window.event.returnValue = false;
327
+ }
328
+
329
+ const editorLeft = this.editor.options.editorDom.getBoundingClientRect().left;
330
+ const editorRight = mouseMoveEvent.clientX;
331
+ const virtualLayout = this.calculateVirtualLayout(editorLeft, editorRight);
332
+ this.setVirtualLayout(virtualLayout.startWidth, virtualLayout.leftWidth, virtualLayout.rightWidth);
333
+ return false;
334
+ };
335
+
336
+ const dragLineMouseUp = (mouseUpEvent) => {
337
+ // 阻止事件冒泡
338
+ if (mouseUpEvent && mouseUpEvent.stopPropagation) {
339
+ mouseUpEvent.stopPropagation();
340
+ } else {
341
+ mouseUpEvent.cancelBubble = true;
342
+ }
343
+ // 取消默认事件
344
+ if (mouseUpEvent.preventDefault) {
345
+ mouseUpEvent.preventDefault();
346
+ } else {
347
+ window.event.returnValue = false;
348
+ }
349
+
350
+ // 重新设置editor和previewer宽度占比
351
+ const editorLeft = this.editor.options.editorDom.getBoundingClientRect().left;
352
+ const editorRight = mouseUpEvent.clientX;
353
+ const layout = this.calculateRealLayout(editorRight - editorLeft);
354
+ this.setRealLayout(layout.editorPercentage, layout.previewerPercentage);
355
+ // 去掉蒙层和虚拟拖动条
356
+ this.editor.options.editorDom.classList.remove('no-select');
357
+ this.options.previewerDom.classList.remove('no-select');
358
+ this.options.editorMaskDom.classList.remove('cherry-editor-mask--show');
359
+ this.options.previewerMaskDom.classList.remove('cherry-previewer-mask--show');
360
+ this.options.virtualDragLineDom.classList.remove('cherry-drag--show');
361
+ // 刷新codemirror宽度
362
+ this.editor.editor.refresh();
363
+ // 取消事件绑定
364
+ removeEvent(document, 'mousemove', dragLineMouseMove, false);
365
+ removeEvent(document, 'mouseup', dragLineMouseUp, false);
366
+ return false;
367
+ };
368
+
369
+ const dragLineMouseDown = (mouseDownEvent) => {
370
+ // 阻止事件冒泡
371
+ if (mouseDownEvent && mouseDownEvent.stopPropagation) {
372
+ mouseDownEvent.stopPropagation();
373
+ } else {
374
+ mouseDownEvent.cancelBubble = true;
375
+ }
376
+ // 取消默认事件
377
+ if (mouseDownEvent.preventDefault) {
378
+ mouseDownEvent.preventDefault();
379
+ } else {
380
+ window.event.returnValue = false;
381
+ }
382
+
383
+ this.syncVirtualLayoutFromReal();
384
+
385
+ const editorLeft = this.editor.options.editorDom.getBoundingClientRect().left;
386
+ const editorRight = mouseDownEvent.clientX;
387
+ const virtualLayout = this.calculateVirtualLayout(editorLeft, editorRight);
388
+ this.setVirtualLayout(virtualLayout.startWidth, virtualLayout.leftWidth, virtualLayout.rightWidth);
389
+ if (!this.options.virtualDragLineDom.classList.contains('cherry-drag--show')) {
390
+ // 增加蒙层防止选中editor或previewer内容
391
+ this.options.virtualDragLineDom.classList.add('cherry-drag--show');
392
+ this.options.editorMaskDom.classList.add('cherry-editor-mask--show');
393
+ this.options.previewerMaskDom.classList.add('cherry-previewer-mask--show');
394
+ this.options.previewerDom.classList.add('no-select');
395
+ this.editor.options.editorDom.classList.add('no-select');
396
+ // 绑定事件
397
+ addEvent(document, 'mousemove', dragLineMouseMove, false);
398
+ addEvent(document, 'mouseup', dragLineMouseUp, false);
399
+ }
400
+ return false;
401
+ };
402
+
403
+ addEvent(this.options.virtualDragLineDom, 'mousedown', dragLineMouseDown, false);
404
+ addEvent(window, 'resize', this.syncVirtualLayoutFromReal.bind(this), false);
405
+ this.setRealLayout();
406
+ }
407
+
408
+ bindScroll() {
409
+ const domContainer = this.getDomContainer();
410
+ onScroll = () => {
411
+ if (this.applyingDomChanges) {
412
+ Logger.log(new Date(), 'sync scroll locked');
413
+ return;
414
+ }
415
+ if (this.disableScrollListener) {
416
+ this.disableScrollListener = false;
417
+ return;
418
+ }
419
+ if (domContainer.scrollTop <= 0) {
420
+ this.editor.scrollToLineNum(0, 0, 1);
421
+ return;
422
+ }
423
+ // 判定预览区域是否滚动到底部的逻辑,增加10px的冗余
424
+ if (domContainer.scrollTop + domContainer.offsetHeight + 10 > domContainer.scrollHeight) {
425
+ this.editor.scrollToLineNum(null);
426
+ return;
427
+ }
428
+ const domPosition = domContainer.getBoundingClientRect();
429
+ let targetElement;
430
+ let lines = 0;
431
+ const elements = domContainer.children;
432
+ for (let i = 0; i < elements.length; i++) {
433
+ const element = elements[i];
434
+ if (element.getBoundingClientRect().top < domPosition.top) {
435
+ targetElement = element;
436
+ const currentLines = element.getAttribute('data-lines') ?? 0;
437
+ lines += +currentLines;
438
+ } else {
439
+ break;
440
+ }
441
+ }
442
+ if (!targetElement) {
443
+ this.editor.scrollToLineNum(0, 0, 1);
444
+ return;
445
+ }
446
+ // markdown元素存在margin,getBoundingRect不能获取到margin
447
+ const mdElementStyle = getComputedStyle(targetElement);
448
+ const marginTop = parseFloat(mdElementStyle.marginTop);
449
+ const marginBottom = parseFloat(mdElementStyle.marginBottom);
450
+ // markdown元素基于当前页面的矩形模型
451
+ const mdRect = targetElement.getBoundingClientRect();
452
+ const mdActualHeight = mdRect.height + marginTop + marginBottom;
453
+ // (mdRect.y - marginTop)为顶部触达区域,basePoint.y为预览区域的顶部,故可视范围应减去预览区域的偏移
454
+ const mdOffsetTop = mdRect.y - marginTop - domPosition.y;
455
+ const lineNum = +targetElement.getAttribute('data-lines'); // 当前markdown元素所占行数
456
+ const percent = (100 * Math.abs(mdOffsetTop)) / mdActualHeight / 100;
457
+ // console.log('destLine:', lines, percent,
458
+ // mdRect.height + marginTop + marginBottom, mdOffsetTop, mdElement);
459
+ // if(mdOffsetTop < 0) {
460
+ return this.editor.scrollToLineNum(lines - lineNum, lineNum, percent);
461
+ // }
462
+ // return this.editor.scrollToLineNum(lines - lineNum, 0, 0);
463
+ };
464
+ addEvent(domContainer, 'scroll', onScroll, false);
465
+ addEvent(
466
+ domContainer,
467
+ 'wheel',
468
+ () => {
469
+ // 鼠标滚轮滚动时,强制监听滚动事件
470
+ this.disableScrollListener = false;
471
+ // 打断滚动动画
472
+ cancelAnimationFrame(this.animation.timer);
473
+ this.animation.timer = 0;
474
+ },
475
+ false,
476
+ );
477
+ }
478
+
479
+ removeScroll() {
480
+ const domContainer = this.getDomContainer();
481
+ removeEvent(domContainer, 'scroll', onScroll, false);
482
+ }
483
+
484
+ $html2H(dom) {
485
+ if (typeof dom === 'undefined') {
486
+ return vDH('span', {}, []);
487
+ }
488
+ if (!dom.tagName) {
489
+ return dom.textContent;
490
+ }
491
+ const { tagName } = dom;
492
+
493
+ // skip all children if data-cm-atomic attribute is set
494
+ const isAtomic = 'true' === dom.getAttribute('data-cm-atomic');
495
+
496
+ const myAttrs = this.$getAttrsForH(dom.attributes);
497
+ const children = [];
498
+ if (!isAtomic && dom.childNodes && dom.childNodes.length > 0) {
499
+ for (let i = 0; i < dom.childNodes.length; i++) {
500
+ children.push(this.$html2H(dom.childNodes[i]));
501
+ }
502
+ }
503
+ return vDH(tagName, myAttrs, children);
504
+ }
505
+
506
+ $getAttrsForH(obj) {
507
+ if (!obj) {
508
+ return {};
509
+ }
510
+ const ret = { dataset: {} };
511
+ for (let i = 0; i < obj.length; i++) {
512
+ let { name } = obj[i];
513
+ const { value } = obj[i];
514
+ if (/^(width|height)$/i.test(name)) {
515
+ if (isNaN(value)) {
516
+ ret.style = ret.style ? ret.style : [];
517
+ ret.style.push(`${name}:${value}`);
518
+ continue;
519
+ }
520
+ }
521
+ if (/^(class|id|href|rel|target|src|title|controls|align|width|height|style|open|contenteditable)$/i.test(name)) {
522
+ name = name === 'class' ? 'className' : name;
523
+ name = name === 'contenteditable' ? 'contentEditable' : name;
524
+ if (name === 'style') {
525
+ ret.style = ret.style ? ret.style : [];
526
+ ret.style.push(value);
527
+ } else if (name === 'open') {
528
+ // 只要有open这个属性,就一定是true
529
+ ret[name] = true;
530
+ } else {
531
+ ret[name] = value;
532
+ }
533
+ } else {
534
+ // jsDom属性里面rowspan的S要大写,否则应用到html的dom节点会变成data-rowspan
535
+ // https://stackoverflow.com/q/29774686
536
+ if ('colspan' === name) {
537
+ name = 'colSpan';
538
+ } else if ('rowspan' === name) {
539
+ name = 'rowSpan';
540
+ }
541
+ if (/^data-/i.test(name)) {
542
+ name = name.replace(/^data-/i, '');
543
+ } else {
544
+ ret[name] = value;
545
+ }
546
+ ret.dataset[name] = value;
547
+ }
548
+ }
549
+ if (ret.style) {
550
+ ret.style = { cssText: ret.style.join(';') }; // see virtual-dom implementation
551
+ }
552
+ return ret;
553
+ }
554
+
555
+ $updateDom(newDom, oldDom) {
556
+ const diff = vDDiff(this.$html2H(oldDom), this.$html2H(newDom));
557
+ return vDPatch(oldDom, diff);
558
+ }
559
+
560
+ $testChild(dom) {
561
+ if (!dom.parentNode) {
562
+ return true;
563
+ }
564
+ if (dom.parentNode.classList.contains('cherry-previewer')) {
565
+ return true;
566
+ }
567
+ if (dom.parentNode.getAttribute('data-sign')) {
568
+ return false;
569
+ }
570
+ return this.$testChild(dom.parentNode);
571
+ }
572
+ _testMaxIndex(index, arr) {
573
+ if (!arr) {
574
+ return false;
575
+ }
576
+ for (let i = 0; i < arr.length; i++) {
577
+ if (index <= arr[i]) {
578
+ return true;
579
+ }
580
+ }
581
+ return false;
582
+ }
583
+ $getSignData(dom) {
584
+ const list = dom.querySelectorAll('[data-sign]');
585
+ const ret = { list: [], signs: {} };
586
+ for (let i = 0; i < list.length; i++) {
587
+ if (!this.$testChild(list[i])) {
588
+ continue;
589
+ }
590
+ const sign = list[i].getAttribute('data-sign');
591
+ ret.list.push({ sign, dom: list[i] });
592
+ if (!ret.signs[sign]) {
593
+ ret.signs[sign] = [];
594
+ }
595
+ ret.signs[sign].push(i);
596
+ }
597
+ return ret;
598
+ }
599
+
600
+ _hasNewSign(list, sign, signIndex) {
601
+ if (list.length > 0) {
602
+ let resSign;
603
+ list.forEach((listItem, i) => {
604
+ // hash精度校准
605
+ if (listItem.sign.slice(0, 12) === sign.slice(0, 12) && i > signIndex) {
606
+ resSign = {
607
+ index: i > signIndex ? i : signIndex,
608
+ sign,
609
+ };
610
+ }
611
+ });
612
+ return resSign;
613
+ }
614
+ return false;
615
+ }
616
+
617
+ $dealWithMyersDiffResult(result, oldContent, newContent, domContainer) {
618
+ result.forEach((change) => {
619
+ if (newContent[change.newIndex].dom) {
620
+ // 把已经加载过的图片的data-src变成src
621
+ newContent[change.newIndex].dom.innerHTML = this.lazyLoadImg.changeLoadedDataSrc2Src(
622
+ newContent[change.newIndex].dom.innerHTML,
623
+ );
624
+ }
625
+ switch (change.type) {
626
+ case 'delete':
627
+ domContainer.removeChild(oldContent[change.oldIndex].dom);
628
+ break;
629
+ case 'insert':
630
+ if (oldContent[change.oldIndex]) {
631
+ domContainer.insertBefore(newContent[change.newIndex].dom, oldContent[change.oldIndex].dom);
632
+ } else {
633
+ domContainer.appendChild(newContent[change.newIndex].dom);
634
+ }
635
+ break;
636
+ case 'update':
637
+ try {
638
+ // 处理表格包含图表的特殊场景
639
+ let hasUpdate = false;
640
+ if (
641
+ newContent[change.newIndex].dom.className === 'cherry-table-container' &&
642
+ newContent[change.newIndex].dom.querySelector('.cherry-table-figure') &&
643
+ oldContent[change.oldIndex].dom.querySelector('.cherry-table-figure')
644
+ ) {
645
+ oldContent[change.oldIndex].dom
646
+ .querySelector('.cherry-table-figure')
647
+ .replaceWith(newContent[change.newIndex].dom.querySelector('.cherry-table-figure'));
648
+ oldContent[change.oldIndex].dom.dataset.sign = newContent[change.oldIndex].dom.dataset.sign;
649
+ this.$updateDom(
650
+ newContent[change.newIndex].dom.querySelector('.cherry-table'),
651
+ oldContent[change.oldIndex].dom.querySelector('.cherry-table'),
652
+ );
653
+ hasUpdate = true;
654
+ } else if (newContent[change.newIndex].dom.querySelector('svg')) {
655
+ throw new Error(); // SVG暂不使用patch更新
656
+ }
657
+ if (!hasUpdate) {
658
+ this.$updateDom(newContent[change.newIndex].dom, oldContent[change.oldIndex].dom);
659
+ }
660
+ } catch (e) {
661
+ domContainer.insertBefore(newContent[change.newIndex].dom, oldContent[change.oldIndex].dom);
662
+ domContainer.removeChild(oldContent[change.oldIndex].dom);
663
+ }
664
+ }
665
+ });
666
+ }
667
+
668
+ $dealUpdate(domContainer, oldHtmlList, newHtmlList) {
669
+ if (newHtmlList.list !== oldHtmlList.list) {
670
+ if (newHtmlList.list.length && oldHtmlList.list.length) {
671
+ const myersDiff = new MyersDiff(newHtmlList.list, oldHtmlList.list, (obj, index) => obj[index].sign);
672
+ const res = myersDiff.doDiff();
673
+ Logger.log(res);
674
+ this.$dealWithMyersDiffResult(res, oldHtmlList.list, newHtmlList.list, domContainer);
675
+ } else if (newHtmlList.list.length && !oldHtmlList.list.length) {
676
+ // 全新增
677
+ Logger.log('add all');
678
+ newHtmlList.list.forEach((piece) => {
679
+ domContainer.appendChild(piece.dom);
680
+ });
681
+ } else if (!newHtmlList.list.length && oldHtmlList.list.length) {
682
+ // 全删除
683
+ Logger.log('delete all');
684
+ oldHtmlList.list.forEach((piece) => {
685
+ domContainer.removeChild(piece.dom);
686
+ });
687
+ }
688
+ }
689
+ }
690
+
691
+ /**
692
+ * 强制重新渲染预览区域
693
+ */
694
+ refresh(html) {
695
+ const domContainer = this.getDomContainer();
696
+ domContainer.innerHTML = html;
697
+ }
698
+
699
+ update(html) {
700
+ // 更新时保留图片懒加载逻辑
701
+ const newHtml = this.lazyLoadImg.changeSrc2DataSrc(html);
702
+ if (!this.isPreviewerHidden()) {
703
+ // 标记当前正在更新预览区域,锁定同步滚动功能
704
+ window.clearTimeout(this.syncScrollLockTimer);
705
+ this.applyingDomChanges = true;
706
+ // 预览区未隐藏时,直接更新
707
+ const tmpDiv = document.createElement('div');
708
+ const domContainer = this.getDomContainer();
709
+ if (this.editor.selectAll) {
710
+ domContainer.innerHTML = '';
711
+ }
712
+ tmpDiv.innerHTML = newHtml;
713
+ const newHtmlList = this.$getSignData(tmpDiv);
714
+ const oldHtmlList = this.$getSignData(domContainer);
715
+
716
+ try {
717
+ this.$dealUpdate(domContainer, oldHtmlList, newHtmlList);
718
+ this.afterUpdate();
719
+ } finally {
720
+ // 延时释放同步滚动功能,在DOM更新完成后执行
721
+ this.syncScrollLockTimer = window.setTimeout(() => {
722
+ this.applyingDomChanges = false;
723
+ }, 50);
724
+ }
725
+ } else {
726
+ // 预览区隐藏时,先缓存起来,等到预览区打开再一次性更新
727
+ this.doHtmlCache(newHtml);
728
+ }
729
+ }
730
+
731
+ $dealEditAndPreviewOnly(isEditOnly = true) {
732
+ let fullEditorLayout = {
733
+ editorPercentage: '0%',
734
+ previewerPercentage: '100%',
735
+ };
736
+ if (isEditOnly) {
737
+ fullEditorLayout = {
738
+ editorPercentage: '100%',
739
+ previewerPercentage: '0%',
740
+ };
741
+ }
742
+ const editorWidth = this.editor.options.editorDom.getBoundingClientRect().width;
743
+ const layout = this.calculateRealLayout(editorWidth);
744
+ this.options.previewerCache.layout = layout;
745
+ this.setRealLayout(fullEditorLayout.editorPercentage, fullEditorLayout.previewerPercentage);
746
+ this.options.virtualDragLineDom.classList.add('cherry-drag--hidden');
747
+ const { previewerDom } = this.options;
748
+ const { editorDom } = this.editor.options;
749
+ if (isEditOnly) {
750
+ previewerDom.classList.add('cherry-previewer--hidden');
751
+ editorDom.classList.add('cherry-editor--full');
752
+ previewerDom.classList.remove('cherry-preview--full');
753
+ editorDom.classList.remove('cherry-editor--hidden');
754
+ } else {
755
+ previewerDom.classList.add('cherry-preview--full');
756
+ editorDom.classList.add('cherry-editor--hidden');
757
+ previewerDom.classList.remove('cherry-previewer--hidden');
758
+ editorDom.classList.remove('cherry-editor--full');
759
+ }
760
+ setTimeout(() => this.editor.editor.refresh(), 0);
761
+ }
762
+
763
+ previewOnly() {
764
+ this.$dealEditAndPreviewOnly(false);
765
+ if (this.options.previewerCache.htmlChanged) {
766
+ this.update(this.options.previewerCache.html);
767
+ }
768
+ this.cleanHtmlCache();
769
+ this.$cherry.$event.emit('previewerOpen');
770
+ this.$cherry.$event.emit('editorClose');
771
+ }
772
+
773
+ editOnly(dealToolbar = false) {
774
+ this.$dealEditAndPreviewOnly(true);
775
+ this.cleanHtmlCache();
776
+ this.$cherry.$event.emit('previewerClose');
777
+ this.$cherry.$event.emit('editorOpen');
778
+ }
779
+
780
+ floatPreviewer() {
781
+ const fullEditorLayout = {
782
+ editorPercentage: '100%',
783
+ previewerPercentage: '100%',
784
+ };
785
+ const editorWidth = this.editor.options.editorDom.getBoundingClientRect().width;
786
+ const layout = this.calculateRealLayout(editorWidth);
787
+ this.options.previewerCache.layout = layout;
788
+ this.setRealLayout(fullEditorLayout.editorPercentage, fullEditorLayout.previewerPercentage);
789
+ this.options.virtualDragLineDom.classList.add('cherry-drag--hidden');
790
+ this.$cherry.createFloatPreviewer();
791
+ }
792
+
793
+ recoverFloatPreviewer() {
794
+ this.recoverPreviewer(true);
795
+ this.$cherry.clearFloatPreviewer();
796
+ }
797
+
798
+ recoverPreviewer(dealToolbar = false) {
799
+ this.options.previewerDom.classList.remove('cherry-previewer--hidden');
800
+ this.options.virtualDragLineDom.classList.remove('cherry-drag--hidden');
801
+ this.editor.options.editorDom.classList.remove('cherry-editor--full');
802
+ // 恢复现场
803
+ const { layout } = this.options.previewerCache;
804
+ this.setRealLayout(layout.editorPercentage, layout.previewerPercentage);
805
+ if (this.options.previewerCache.htmlChanged) {
806
+ this.update(this.options.previewerCache.html);
807
+ }
808
+ this.cleanHtmlCache();
809
+
810
+ this.$cherry.$event.emit('previewerOpen');
811
+ this.$cherry.$event.emit('editorOpen');
812
+
813
+ setTimeout(() => this.editor.editor.refresh(), 0);
814
+ }
815
+
816
+ doHtmlCache(html) {
817
+ this.options.previewerCache.html = html;
818
+ this.options.previewerCache.htmlChanged = true;
819
+ }
820
+
821
+ cleanHtmlCache() {
822
+ this.options.previewerCache.html = '';
823
+ this.options.previewerCache.htmlChanged = false;
824
+ this.options.previewerCache.layout = {};
825
+ }
826
+
827
+ afterUpdate() {
828
+ this.options.afterUpdateCallBack.map((fn) => fn());
829
+ if (this.highlightLineNum === undefined) {
830
+ this.highlightLineNum = 0;
831
+ }
832
+ this.highlightLine(this.highlightLineNum);
833
+ }
834
+
835
+ registerAfterUpdate(fn) {
836
+ if (Array.isArray(fn)) {
837
+ this.options.afterUpdateCallBack = this.options.afterUpdateCallBack.concat(fn);
838
+ } else if (!fn) {
839
+ throw new Error('[markdown error]: Previewer registerAfterUpdate params are undefined');
840
+ } else {
841
+ this.options.afterUpdateCallBack.push(fn);
842
+ }
843
+ }
844
+
845
+ /**
846
+ * 根据行号计算出top值
847
+ * @param {Number} lineNum
848
+ * @param {Number} linePercent
849
+ * @return {Number} top
850
+ */
851
+ $getTopByLineNum(lineNum, linePercent = 0) {
852
+ const domContainer = this.getDomContainer();
853
+ if (lineNum === null) {
854
+ return domContainer.scrollHeight;
855
+ }
856
+ const $lineNum = typeof lineNum === 'number' ? lineNum : parseInt(lineNum, 10);
857
+ const doms = /** @type {NodeListOf<HTMLElement>}*/ (domContainer.querySelectorAll('[data-sign]'));
858
+ let lines = 0;
859
+ const containerY = domContainer.offsetTop;
860
+ for (let index = 0; index < doms.length; index++) {
861
+ if (doms[index].parentNode !== domContainer) {
862
+ continue;
863
+ }
864
+ const blockLines = parseInt(doms[index].getAttribute('data-lines'), 10);
865
+ if (lines + blockLines < $lineNum) {
866
+ lines += blockLines;
867
+ continue;
868
+ } else {
869
+ // 基础定位,区块高度及offsetTop会受到block margin合并的影响
870
+ const { height: blockHeight, offsetTop } = getBlockTopAndHeightWithMargin(doms[index]);
871
+ const blockY = offsetTop - containerY;
872
+ let scrollTo = blockY + blockHeight * linePercent;
873
+ // 区块多于1行
874
+ if (blockLines > 1) {
875
+ // 高度百分比计算
876
+ // 该区块已经滚动过的行,不包括当前行,减一
877
+ const overScrolledLines = blockLines - Math.abs($lineNum - (lines + blockLines)) - 1;
878
+ const overScrolledHeight = (overScrolledLines / blockLines) * blockHeight; // 已经滚过的高度
879
+ const blockLineHeight = blockHeight / blockLines; // 该区块每一行的高度
880
+ // 应该滚动到的位置
881
+ scrollTo = blockY + overScrolledHeight + blockLineHeight * linePercent;
882
+ // console.log('overscrolled:', overScrolledHeight, blockLineHeight, linePercent);
883
+ }
884
+ // console.log('滚动编辑区域,左侧应scroll to ', lineNum, '::',scrollTo);
885
+ return scrollTo;
886
+ }
887
+ }
888
+ // 如果计算完预览区域所有的行号依然<左侧光标所在的行号,则预览区域直接滚到最低部
889
+ return domContainer.scrollHeight;
890
+ }
891
+
892
+ /**
893
+ * 高亮预览区域对应的行
894
+ * @param {Number} lineNum
895
+ */
896
+ highlightLine(lineNum) {
897
+ const domContainer = this.getDomContainer();
898
+ // 先取消所有行的高亮效果
899
+ domContainer.querySelectorAll('.cherry-highlight-line').forEach((element) => {
900
+ element.classList.remove('cherry-highlight-line');
901
+ });
902
+ // 只有双栏模式下才需要高亮光标对应的预览区域
903
+ if (this.$cherry?.status?.previewer !== 'show' || this.$cherry?.status?.editor !== 'show') {
904
+ return;
905
+ }
906
+ const doms = /** @type {NodeListOf<HTMLElement>}*/ (domContainer.querySelectorAll('[data-sign]'));
907
+ let lines = 0;
908
+ for (let index = 0; index < doms.length; index++) {
909
+ if (doms[index].parentNode !== domContainer) {
910
+ continue;
911
+ }
912
+ const blockLines = parseInt(doms[index].getAttribute('data-lines'), 10);
913
+ if (lines + blockLines < lineNum) {
914
+ lines += blockLines;
915
+ continue;
916
+ } else {
917
+ this.highlightLineNum = lineNum;
918
+ doms[index].classList.add('cherry-highlight-line');
919
+ return;
920
+ }
921
+ }
922
+ }
923
+
924
+ /**
925
+ * 滚动到对应行号位置并加上偏移量
926
+ * @param {Number} lineNum
927
+ * @param {Number} offset
928
+ */
929
+ scrollToLineNumWithOffset(lineNum, offset) {
930
+ const top = this.$getTopByLineNum(lineNum) - offset;
931
+ this.$scrollAnimation(top);
932
+ this.highlightLine(lineNum);
933
+ }
934
+
935
+ /**
936
+ * 滚动到对应位置
937
+ * @param {number} scrollTop 元素的id属性值
938
+ * @param {'auto'|'smooth'|'instant'} behavior 滚动方式
939
+ */
940
+ scrollToTop(scrollTop, behavior = 'auto') {
941
+ const previewDom = this.getDomContainer();
942
+ const scrollDom = this.getDomCanScroll(previewDom);
943
+ scrollDom.scrollTo({
944
+ top: scrollTop,
945
+ left: 0,
946
+ behavior,
947
+ });
948
+ }
949
+
950
+ /**
951
+ * 滚动到对应id的位置,实现锚点滚动能力
952
+ * @param {string} id 元素的id属性值
953
+ * @param {'smooth'|'instant'|'auto'} behavior 滚动方式
954
+ * @return {boolean} 是否有对应id的元素并执行滚动
955
+ */
956
+ scrollToId(id, behavior = 'smooth') {
957
+ const previewDom = this.getDomContainer();
958
+ const scrollDom = this.getDomCanScroll(previewDom);
959
+ // 设置未加载图片的默认尺寸
960
+ const images = previewDom.getElementsByTagName('img');
961
+ const modifiedImages = new Set(); // 记录被修改过样式的图片
962
+ let isDealScroll = false;
963
+ Array.from(images).forEach((img) => {
964
+ if (!img.hasAttribute('width') && !img.hasAttribute('height') && !img.style.width && !img.style.height) {
965
+ // img.style.minHeight = '200px';
966
+ // img.style.aspectRatio = '16/9';
967
+ modifiedImages.add(img);
968
+ }
969
+ });
970
+
971
+ let $id = id.replace(/^\s*#/, '').trim();
972
+ $id = /[%:]/.test($id) ? $id : encodeURIComponent($id);
973
+ const target = previewDom.querySelector(`[id="${$id}"]`) ?? false;
974
+ if (target === false) {
975
+ return false;
976
+ }
977
+
978
+ let scrollTop = 0;
979
+
980
+ if (scrollDom.nodeName === 'HTML') {
981
+ scrollTop = scrollDom.scrollTop + target.getBoundingClientRect().y - 10;
982
+ } else {
983
+ scrollTop = scrollDom.scrollTop + target.getBoundingClientRect().y - scrollDom.getBoundingClientRect().y - 10;
984
+ }
985
+
986
+ // 创建一个函数来清理图片样式并重新滚动
987
+ const cleanupAndScroll = () => {
988
+ // modifiedImages.forEach((img) => {
989
+ // img.style.minHeight = '';
990
+ // img.style.aspectRatio = '';
991
+ // });
992
+ modifiedImages.clear();
993
+
994
+ // 重新计算位置并滚动
995
+ let newScrollTop = 0;
996
+ if (scrollDom.nodeName === 'HTML') {
997
+ newScrollTop = scrollDom.scrollTop + target.getBoundingClientRect().y - 10;
998
+ } else {
999
+ newScrollTop =
1000
+ scrollDom.scrollTop + target.getBoundingClientRect().y - scrollDom.getBoundingClientRect().y - 10;
1001
+ }
1002
+ // 如果位置有变化,使用instant行为重新滚动
1003
+ if (Math.abs(newScrollTop - scrollTop) > 5) {
1004
+ scrollDom.scrollTo({
1005
+ top: newScrollTop,
1006
+ left: 0,
1007
+ behavior: 'instant',
1008
+ });
1009
+ }
1010
+ };
1011
+
1012
+ // 监听滚动结束事件
1013
+ const handleScrollEnd = () => {
1014
+ if (isDealScroll) {
1015
+ return;
1016
+ }
1017
+ isDealScroll = true;
1018
+ // 移除滚动事件监听器
1019
+ scrollDom.removeEventListener('scrollend', handleScrollEnd);
1020
+ // 等待一小段时间确保图片开始加载
1021
+ setTimeout(() => {
1022
+ // 获取所有修改过的图片的加载状态
1023
+ const imageLoadPromises = Array.from(modifiedImages).map((img) => {
1024
+ if (img.complete) return Promise.resolve();
1025
+ return new Promise((resolve) => {
1026
+ const onLoad = () => {
1027
+ img.removeEventListener('load', onLoad);
1028
+ img.removeEventListener('error', onLoad);
1029
+ resolve();
1030
+ };
1031
+ img.addEventListener('load', onLoad);
1032
+ img.addEventListener('error', onLoad);
1033
+ });
1034
+ });
1035
+
1036
+ // 等待所有图片加载完成后再清理样式
1037
+ Promise.all(imageLoadPromises).then(() => {
1038
+ // 使用 requestAnimationFrame 确保在下一帧渲染时处理
1039
+ requestAnimationFrame(cleanupAndScroll);
1040
+ });
1041
+ }, 100);
1042
+ };
1043
+
1044
+ // 添加滚动结束事件监听器
1045
+ scrollDom.addEventListener('scrollend', handleScrollEnd);
1046
+
1047
+ // 如果浏览器不支持 scrollend 事件,使用 setTimeout 作为后备方案
1048
+ setTimeout(() => {
1049
+ scrollDom.removeEventListener('scrollend', handleScrollEnd);
1050
+ handleScrollEnd();
1051
+ }, 1000);
1052
+
1053
+ // 开始滚动
1054
+ scrollDom.scrollTo({
1055
+ top: scrollTop,
1056
+ left: 0,
1057
+ behavior,
1058
+ });
1059
+
1060
+ return true;
1061
+ }
1062
+
1063
+ /**
1064
+ * 实现滚动动画
1065
+ * @param { Number } targetY 目标位置
1066
+ */
1067
+ $scrollAnimation(targetY) {
1068
+ this.animation.destinationTop = targetY;
1069
+ if (this.animation.timer) {
1070
+ return;
1071
+ }
1072
+ const animationHandler = () => {
1073
+ const dom = this.getDomContainer();
1074
+ const currentTop = dom.scrollTop;
1075
+ const delta = this.animation.destinationTop - currentTop;
1076
+ // 100毫秒内完成动画
1077
+ const move = Math.ceil(Math.min(Math.abs(delta), Math.max(1, Math.abs(delta) / (100 / 16.7))));
1078
+ if (delta === 0 || currentTop >= dom.scrollHeight || move > Math.abs(delta)) {
1079
+ cancelAnimationFrame(this.animation.timer);
1080
+ this.animation.timer = 0;
1081
+ return;
1082
+ }
1083
+ this.disableScrollListener = true;
1084
+ this.getDomContainer().scrollTo(null, currentTop + (delta / Math.abs(delta)) * move);
1085
+ this.animation.timer = requestAnimationFrame(animationHandler);
1086
+ };
1087
+ this.animation.timer = requestAnimationFrame(animationHandler);
1088
+ }
1089
+
1090
+ scrollToLineNum(lineNum, linePercent) {
1091
+ const top = this.$getTopByLineNum(lineNum, linePercent);
1092
+ this.$scrollAnimation(top);
1093
+ }
1094
+
1095
+ /**
1096
+ * 获取有滚动条的dom
1097
+ */
1098
+ getDomCanScroll(currentDom = this.getDomContainer()) {
1099
+ if (currentDom.scrollHeight > currentDom.clientHeight || currentDom.clientHeight < window.innerHeight) {
1100
+ return currentDom;
1101
+ }
1102
+ if (currentDom.parentElement) {
1103
+ if (currentDom.nodeName === 'BODY') {
1104
+ // 如果当前是body了,再往上就是html了
1105
+ if (document.documentElement.scrollHeight > document.documentElement.clientHeight) {
1106
+ return document.documentElement;
1107
+ }
1108
+ return currentDom;
1109
+ }
1110
+ return this.getDomCanScroll(currentDom.parentElement);
1111
+ }
1112
+ }
1113
+
1114
+ scrollToHeadByIndex(index) {
1115
+ const previewDom = this.getDomContainer();
1116
+ const targetHead = previewDom.querySelectorAll('h1,h2,h3,h4,h5,h6,h7,h8')[index] ?? false;
1117
+ if (targetHead !== false) {
1118
+ this.scrollToId(targetHead.id);
1119
+ }
1120
+ }
1121
+
1122
+ bindClick() {
1123
+ this.getDomContainer().addEventListener('click', (event) => {
1124
+ if (this.$cherry.options.callback.onClickPreview) {
1125
+ const ret = this.$cherry.options.callback.onClickPreview(event);
1126
+ // @ts-ignore
1127
+ if (ret === false) {
1128
+ return ret;
1129
+ }
1130
+ }
1131
+ // 如果配置了点击toc目录不更新location hash
1132
+ // @ts-ignore
1133
+ if (this.$cherry.options.toolbars.toc?.updateLocationHash === false) {
1134
+ const { target } = event;
1135
+ if (target instanceof Element && target.nodeName === 'A' && /level-\d+/.test(target.className)) {
1136
+ const liNode = target.parentElement;
1137
+ const index = Array.from(liNode.parentElement.children).indexOf(liNode) - 1;
1138
+ this.scrollToHeadByIndex(index);
1139
+ event.stopPropagation();
1140
+ event.preventDefault();
1141
+ }
1142
+ /** 增加个潜规则逻辑,脚注跳转时是否更新location hash也跟随options.toolbars.toc.updateLocationHash 的配置 */
1143
+ if (target instanceof Element && target.nodeName === 'A' && /(footnote|footnote-ref)/.test(target.className)) {
1144
+ const id = target.getAttribute('href');
1145
+ this.scrollToId(id);
1146
+ event.stopPropagation();
1147
+ event.preventDefault();
1148
+ }
1149
+ }
1150
+ });
1151
+ }
1152
+
1153
+ onMouseDown() {
1154
+ addEvent(this.getDomContainer(), 'mousedown', () => {
1155
+ setTimeout(() => {
1156
+ this.$cherry.$event.emit('cleanAllSubMenus');
1157
+ });
1158
+ });
1159
+ }
1160
+ /**
1161
+ * 导出预览区域内容
1162
+ * @public
1163
+ * @param {'pdf' | 'img' | 'screenShot' | 'markdown' | 'html'} [type='pdf']
1164
+ * 'pdf':导出成pdf文件; 'img' | screenShot:导出成png图片; 'markdown':导出成markdown文件; 'html':导出成html文件;
1165
+ * @param {string} [fileName] 导出文件名
1166
+ */
1167
+ export(type = 'pdf', fileName = '') {
1168
+ let name = fileName;
1169
+ if (!fileName) {
1170
+ const { innerText } = this.getDomContainer();
1171
+ name = /^\s*([^\s][^\n]*)\n/.test(innerText) ? innerText.match(/^\s*([^\s][^\n]*)\n/)[1] : 'cherry-export';
1172
+ }
1173
+ if (type === 'pdf') {
1174
+ exportPDF(this.getDomContainer(), name);
1175
+ } else if (type === 'screenShot' || type === 'img') {
1176
+ exportScreenShot(this.getDomContainer(), name);
1177
+ } else if (type === 'markdown') {
1178
+ exportMarkdownFile(this.$cherry.getMarkdown(), name);
1179
+ } else if (type === 'html') {
1180
+ exportHTMLFile(this.getValue(), name);
1181
+ }
1182
+ }
1183
+ }