react-native-marcus 0.1.0

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 (304) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +227 -0
  3. package/RNMarcus.podspec +78 -0
  4. package/android/build.gradle +220 -0
  5. package/android/proguard-rules.pro +1 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/cpp/CMakeLists.txt +41 -0
  9. package/android/src/main/cpp/MarkdownParser.cpp +49 -0
  10. package/android/src/main/cpp/MarkdownParser.h +56 -0
  11. package/android/src/main/cpp/OnLoad.cpp +17 -0
  12. package/android/src/main/java/app/getbullet/marcus/MarcusModule.kt +28 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +31 -0
  14. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +63 -0
  15. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManager.kt +32 -0
  16. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManagerSpec.kt +21 -0
  17. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +333 -0
  18. package/android/src/main/java/app/getbullet/marcus/MarkdownParseResult.kt +12 -0
  19. package/android/src/main/java/app/getbullet/marcus/MarkdownParser.kt +89 -0
  20. package/android/src/main/java/app/getbullet/marcus/MarkdownRange.kt +11 -0
  21. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +81 -0
  22. package/android/src/main/java/app/getbullet/marcus/MarkdownTextWatcher.kt +17 -0
  23. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +84 -0
  24. package/android/src/main/java/app/getbullet/marcus/ReactLineHeight.java +29 -0
  25. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +6 -0
  26. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +105 -0
  27. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +67 -0
  28. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +6 -0
  29. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontFamilySpan.kt +28 -0
  30. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontSizeSpan.kt +7 -0
  31. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownForegroundColorSpan.kt +6 -0
  32. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +57 -0
  33. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +6 -0
  34. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLineHeightSpan.kt +21 -0
  35. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownSpan.kt +7 -0
  36. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownStrikethroughSpan.kt +5 -0
  37. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownUnderlineSpan.kt +5 -0
  38. package/android/src/main/new_arch/CMakeLists.txt +70 -0
  39. package/android/src/main/new_arch/RNMarcusSpec.cpp +43 -0
  40. package/android/src/main/new_arch/RNMarcusSpec.h +36 -0
  41. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.cpp +218 -0
  42. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.h +54 -0
  43. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +156 -0
  44. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +130 -0
  45. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +205 -0
  46. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +78 -0
  47. package/android/src/test/resources/render-model.txt +583 -0
  48. package/apple/MarcusModule.h +8 -0
  49. package/apple/MarcusModule.mm +26 -0
  50. package/apple/MarcusRange.h +15 -0
  51. package/apple/MarcusRange.mm +15 -0
  52. package/apple/MarcusTextInputDecoratorComponentView.h +12 -0
  53. package/apple/MarcusTextInputDecoratorComponentView.mm +121 -0
  54. package/apple/MarcusTextInputDecoratorShadowNode.cpp +251 -0
  55. package/apple/MarcusTextInputDecoratorShadowNode.h +65 -0
  56. package/apple/MarcusTextInputDecoratorShadowNode.mm +204 -0
  57. package/apple/MarcusTextInputDecoratorViewManager.h +8 -0
  58. package/apple/MarcusTextInputDecoratorViewManager.mm +11 -0
  59. package/apple/MarkdownAdaptiveImageGlyph.swift +44 -0
  60. package/apple/MarkdownAttributes.h +31 -0
  61. package/apple/MarkdownAttributes.m +9 -0
  62. package/apple/MarkdownBackedTextInputDelegate.swift +117 -0
  63. package/apple/MarkdownDecorator.swift +316 -0
  64. package/apple/MarkdownFonts.swift +21 -0
  65. package/apple/MarkdownFormatter+React.swift +51 -0
  66. package/apple/MarkdownFormatter.swift +509 -0
  67. package/apple/MarkdownLog.h +10 -0
  68. package/apple/MarkdownLog.mm +6 -0
  69. package/apple/MarkdownParser.swift +200 -0
  70. package/apple/MarkdownSwiftInterop.h +23 -0
  71. package/apple/MarkdownTextBackground.swift +17 -0
  72. package/apple/MarkdownTextFieldObserver.swift +90 -0
  73. package/apple/MarkdownTextLayoutFragment.swift +205 -0
  74. package/apple/MarkdownTextLayoutManagerDelegate.swift +84 -0
  75. package/apple/MarkdownTextStorageDelegate.swift +32 -0
  76. package/apple/MarkdownTextViewObserver.swift +30 -0
  77. package/apple/RCTMarcusStyle+Codegen.h +18 -0
  78. package/apple/RCTMarcusStyle+Codegen.mm +64 -0
  79. package/apple/RCTMarcusStyle.h +41 -0
  80. package/apple/RCTMarcusStyle.m +5 -0
  81. package/apple/RCTMarcusUtils.h +26 -0
  82. package/apple/RCTMarcusUtils.mm +26 -0
  83. package/apple/RCTTextInput+AdaptiveImageGlyph.h +16 -0
  84. package/apple/RCTTextInput+AdaptiveImageGlyph.mm +79 -0
  85. package/apple/RCTTextInputComponentView+Marcus.h +11 -0
  86. package/apple/RCTTextInputComponentView+Marcus.mm +79 -0
  87. package/cpp/MarkdownCxx.modulemap +8 -0
  88. package/cpp/MarkdownGlobal.cpp +48 -0
  89. package/cpp/MarkdownGlobal.h +33 -0
  90. package/cpp/MarkdownWorkletParser.cpp +93 -0
  91. package/cpp/MarkdownWorkletParser.h +45 -0
  92. package/cpp/RuntimeDecorator.cpp +39 -0
  93. package/cpp/RuntimeDecorator.h +14 -0
  94. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorState.h +33 -0
  95. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorViewComponentDescriptor.h +17 -0
  96. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  97. package/lib/commonjs/MarkdownTextInput.js +99 -0
  98. package/lib/commonjs/MarkdownTextInput.js.map +1 -0
  99. package/lib/commonjs/MarkdownTextInput.web.js +618 -0
  100. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
  101. package/lib/commonjs/NativeMarcusModule.js +9 -0
  102. package/lib/commonjs/NativeMarcusModule.js.map +1 -0
  103. package/lib/commonjs/commonTypes.js +2 -0
  104. package/lib/commonjs/commonTypes.js.map +1 -0
  105. package/lib/commonjs/global.d.js +2 -0
  106. package/lib/commonjs/global.d.js.map +1 -0
  107. package/lib/commonjs/index.js +27 -0
  108. package/lib/commonjs/index.js.map +1 -0
  109. package/lib/commonjs/package.json +1 -0
  110. package/lib/commonjs/parser/index.js +7030 -0
  111. package/lib/commonjs/parser/index.js.map +7 -0
  112. package/lib/commonjs/rangeUtils.js +71 -0
  113. package/lib/commonjs/rangeUtils.js.map +1 -0
  114. package/lib/commonjs/react-native-web.d.js +2 -0
  115. package/lib/commonjs/react-native-web.d.js.map +1 -0
  116. package/lib/commonjs/styleUtils.js +91 -0
  117. package/lib/commonjs/styleUtils.js.map +1 -0
  118. package/lib/commonjs/web/InputHistory.js +111 -0
  119. package/lib/commonjs/web/InputHistory.js.map +1 -0
  120. package/lib/commonjs/web/MarkdownTextInput.css +72 -0
  121. package/lib/commonjs/web/utils/blockLayout.js +132 -0
  122. package/lib/commonjs/web/utils/blockLayout.js.map +1 -0
  123. package/lib/commonjs/web/utils/blockUtils.js +219 -0
  124. package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
  125. package/lib/commonjs/web/utils/browserUtils.js +40 -0
  126. package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
  127. package/lib/commonjs/web/utils/cursorUtils.js +157 -0
  128. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
  129. package/lib/commonjs/web/utils/inputUtils.js +152 -0
  130. package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
  131. package/lib/commonjs/web/utils/measureUtils.js +0 -0
  132. package/lib/commonjs/web/utils/measureUtils.js.map +1 -0
  133. package/lib/commonjs/web/utils/parserUtils.js +552 -0
  134. package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
  135. package/lib/commonjs/web/utils/treeUtils.js +109 -0
  136. package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
  137. package/lib/commonjs/web/utils/webStyleUtils.js +59 -0
  138. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
  139. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  140. package/lib/module/MarkdownTextInput.js +94 -0
  141. package/lib/module/MarkdownTextInput.js.map +1 -0
  142. package/lib/module/MarkdownTextInput.web.js +612 -0
  143. package/lib/module/MarkdownTextInput.web.js.map +1 -0
  144. package/lib/module/NativeMarcusModule.js +5 -0
  145. package/lib/module/NativeMarcusModule.js.map +1 -0
  146. package/lib/module/commonTypes.js +2 -0
  147. package/lib/module/commonTypes.js.map +1 -0
  148. package/lib/module/global.d.js +2 -0
  149. package/lib/module/global.d.js.map +1 -0
  150. package/lib/module/index.js +5 -0
  151. package/lib/module/index.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/parser/index.js +7029 -0
  154. package/lib/module/parser/index.js.map +7 -0
  155. package/lib/module/rangeUtils.js +66 -0
  156. package/lib/module/rangeUtils.js.map +1 -0
  157. package/lib/module/react-native-web.d.js +2 -0
  158. package/lib/module/react-native-web.d.js.map +1 -0
  159. package/lib/module/styleUtils.js +88 -0
  160. package/lib/module/styleUtils.js.map +1 -0
  161. package/lib/module/web/InputHistory.js +106 -0
  162. package/lib/module/web/InputHistory.js.map +1 -0
  163. package/lib/module/web/MarkdownTextInput.css +72 -0
  164. package/lib/module/web/utils/blockLayout.js +128 -0
  165. package/lib/module/web/utils/blockLayout.js.map +1 -0
  166. package/lib/module/web/utils/blockUtils.js +210 -0
  167. package/lib/module/web/utils/blockUtils.js.map +1 -0
  168. package/lib/module/web/utils/browserUtils.js +36 -0
  169. package/lib/module/web/utils/browserUtils.js.map +1 -0
  170. package/lib/module/web/utils/cursorUtils.js +150 -0
  171. package/lib/module/web/utils/cursorUtils.js.map +1 -0
  172. package/lib/module/web/utils/inputUtils.js +144 -0
  173. package/lib/module/web/utils/inputUtils.js.map +1 -0
  174. package/lib/module/web/utils/measureUtils.js +0 -0
  175. package/lib/module/web/utils/measureUtils.js.map +1 -0
  176. package/lib/module/web/utils/parserUtils.js +547 -0
  177. package/lib/module/web/utils/parserUtils.js.map +1 -0
  178. package/lib/module/web/utils/treeUtils.js +103 -0
  179. package/lib/module/web/utils/treeUtils.js.map +1 -0
  180. package/lib/module/web/utils/webStyleUtils.js +54 -0
  181. package/lib/module/web/utils/webStyleUtils.js.map +1 -0
  182. package/lib/typescript/commonjs/e2e/checks.test.d.ts +2 -0
  183. package/lib/typescript/commonjs/e2e/checks.test.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/e2e/editing.test.d.ts +2 -0
  185. package/lib/typescript/commonjs/e2e/editing.test.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/e2e/helpers.d.ts +23 -0
  187. package/lib/typescript/commonjs/e2e/helpers.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/e2e/smoke.test.d.ts +2 -0
  189. package/lib/typescript/commonjs/e2e/smoke.test.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/package.json +1 -0
  191. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  192. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  193. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +22 -0
  194. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -0
  195. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +41 -0
  196. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts +7 -0
  198. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts.map +1 -0
  199. package/lib/typescript/commonjs/src/commonTypes.d.ts +10 -0
  200. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -0
  201. package/lib/typescript/commonjs/src/index.d.ts +5 -0
  202. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  203. package/lib/typescript/commonjs/src/parser/index.d.ts +8 -0
  204. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts +8 -0
  206. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  207. package/lib/typescript/commonjs/src/rangeUtils.d.ts +6 -0
  208. package/lib/typescript/commonjs/src/rangeUtils.d.ts.map +1 -0
  209. package/lib/typescript/commonjs/src/styleUtils.d.ts +8 -0
  210. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -0
  211. package/lib/typescript/commonjs/src/web/InputHistory.d.ts +24 -0
  212. package/lib/typescript/commonjs/src/web/InputHistory.d.ts.map +1 -0
  213. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +41 -0
  214. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -0
  215. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +33 -0
  216. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -0
  217. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts +7 -0
  218. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts.map +1 -0
  219. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts +12 -0
  220. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts.map +1 -0
  221. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts +12 -0
  222. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts.map +1 -0
  223. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts +22 -0
  224. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts.map +1 -0
  225. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +44 -0
  226. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -0
  227. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +25 -0
  228. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -0
  229. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +7 -0
  230. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -0
  231. package/lib/typescript/module/e2e/checks.test.d.ts +2 -0
  232. package/lib/typescript/module/e2e/checks.test.d.ts.map +1 -0
  233. package/lib/typescript/module/e2e/editing.test.d.ts +2 -0
  234. package/lib/typescript/module/e2e/editing.test.d.ts.map +1 -0
  235. package/lib/typescript/module/e2e/helpers.d.ts +23 -0
  236. package/lib/typescript/module/e2e/helpers.d.ts.map +1 -0
  237. package/lib/typescript/module/e2e/smoke.test.d.ts +2 -0
  238. package/lib/typescript/module/e2e/smoke.test.d.ts.map +1 -0
  239. package/lib/typescript/module/package.json +1 -0
  240. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  241. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  242. package/lib/typescript/module/src/MarkdownTextInput.d.ts +22 -0
  243. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -0
  244. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +41 -0
  245. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -0
  246. package/lib/typescript/module/src/NativeMarcusModule.d.ts +7 -0
  247. package/lib/typescript/module/src/NativeMarcusModule.d.ts.map +1 -0
  248. package/lib/typescript/module/src/commonTypes.d.ts +10 -0
  249. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -0
  250. package/lib/typescript/module/src/index.d.ts +5 -0
  251. package/lib/typescript/module/src/index.d.ts.map +1 -0
  252. package/lib/typescript/module/src/parser/index.d.ts +8 -0
  253. package/lib/typescript/module/src/parser/index.d.ts.map +1 -0
  254. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts +8 -0
  255. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  256. package/lib/typescript/module/src/rangeUtils.d.ts +6 -0
  257. package/lib/typescript/module/src/rangeUtils.d.ts.map +1 -0
  258. package/lib/typescript/module/src/styleUtils.d.ts +8 -0
  259. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -0
  260. package/lib/typescript/module/src/web/InputHistory.d.ts +24 -0
  261. package/lib/typescript/module/src/web/InputHistory.d.ts.map +1 -0
  262. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +41 -0
  263. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -0
  264. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +33 -0
  265. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -0
  266. package/lib/typescript/module/src/web/utils/browserUtils.d.ts +7 -0
  267. package/lib/typescript/module/src/web/utils/browserUtils.d.ts.map +1 -0
  268. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts +12 -0
  269. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts.map +1 -0
  270. package/lib/typescript/module/src/web/utils/inputUtils.d.ts +12 -0
  271. package/lib/typescript/module/src/web/utils/inputUtils.d.ts.map +1 -0
  272. package/lib/typescript/module/src/web/utils/measureUtils.d.ts +22 -0
  273. package/lib/typescript/module/src/web/utils/measureUtils.d.ts.map +1 -0
  274. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +44 -0
  275. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -0
  276. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +25 -0
  277. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -0
  278. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +7 -0
  279. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -0
  280. package/package.json +167 -0
  281. package/react-native.config.js +10 -0
  282. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  283. package/src/MarkdownTextInput.tsx +139 -0
  284. package/src/MarkdownTextInput.web.tsx +958 -0
  285. package/src/NativeMarcusModule.ts +8 -0
  286. package/src/commonTypes.ts +30 -0
  287. package/src/global.d.ts +1 -0
  288. package/src/index.tsx +4 -0
  289. package/src/parser/index.ts +556 -0
  290. package/src/parser/micromark-extension-mentions.ts +82 -0
  291. package/src/rangeUtils.ts +68 -0
  292. package/src/react-native-web.d.ts +1 -0
  293. package/src/styleUtils.ts +100 -0
  294. package/src/web/InputHistory.ts +137 -0
  295. package/src/web/MarkdownTextInput.css +72 -0
  296. package/src/web/utils/blockLayout.ts +163 -0
  297. package/src/web/utils/blockUtils.ts +245 -0
  298. package/src/web/utils/browserUtils.ts +41 -0
  299. package/src/web/utils/cursorUtils.ts +204 -0
  300. package/src/web/utils/inputUtils.ts +206 -0
  301. package/src/web/utils/measureUtils.ts +0 -0
  302. package/src/web/utils/parserUtils.ts +813 -0
  303. package/src/web/utils/treeUtils.ts +149 -0
  304. package/src/web/utils/webStyleUtils.ts +69 -0
@@ -0,0 +1,813 @@
1
+ import type { HTMLMarkdownElement, MarkdownTextInputElement } from "../../MarkdownTextInput.web"
2
+ import { addNodeToTree, createRootTreeNode } from "./treeUtils"
3
+ import type { NodeType, TreeNode } from "./treeUtils"
4
+ import type { PartialMarkdownStyle } from "../../styleUtils"
5
+ import { getCurrentCursorPosition, moveCursorToEnd, setCursorPosition } from "./cursorUtils"
6
+ import {
7
+ addBlockPrefixGap,
8
+ addLineLayout,
9
+ addStyleToBlock,
10
+ isMultilineMarkdownType,
11
+ } from "./blockUtils"
12
+ import { layoutBlocks } from "./blockLayout"
13
+ import type { LineLayout } from "./blockLayout"
14
+ import { createTextMeasurer } from "./measureUtils"
15
+ import type { TextMeasurer } from "./measureUtils"
16
+ import type { MarkdownRange, MarkdownType } from "../../commonTypes"
17
+ import { sortRanges, ungroupRanges } from "../../rangeUtils"
18
+
19
+ type Paragraph = {
20
+ text: string
21
+ start: number
22
+ length: number
23
+ markdownRanges: MarkdownRange[]
24
+ /** Where the containers this line sits in put its text, once they are all placed. */
25
+ layout?: LineLayout
26
+ }
27
+
28
+ function splitTextIntoLines(text: string): Paragraph[] {
29
+ let lineStartIndex = 0
30
+ const lines: Paragraph[] = text.split("\n").map((line) => {
31
+ const lineObject: Paragraph = {
32
+ text: line,
33
+ start: lineStartIndex,
34
+ length: line.length,
35
+ markdownRanges: [],
36
+ }
37
+ lineStartIndex += line.length + 1 // Adding 1 for the newline character
38
+ return lineObject
39
+ })
40
+
41
+ return lines
42
+ }
43
+
44
+ /**
45
+ * Merges lines with multiline markdown tags (like `pre`) into a single line.
46
+ * The main line will contain the text and all markdown ranges from the other lines.
47
+ */
48
+ function mergeLinesWithMultilineTags(
49
+ lines: Paragraph[],
50
+ currentLine: Paragraph,
51
+ range: MarkdownRange,
52
+ correspondingLineIndexes: number[],
53
+ ) {
54
+ const mainLine = currentLine
55
+ currentLine.markdownRanges.push(range)
56
+
57
+ correspondingLineIndexes.forEach((lineIndex) => {
58
+ const otherLine = lines[lineIndex] as Paragraph
59
+ mainLine.text += `\n${otherLine.text}`
60
+ mainLine.length += otherLine.length + 1
61
+ mainLine.markdownRanges.push(...otherLine.markdownRanges)
62
+ })
63
+
64
+ if (correspondingLineIndexes.length > 0 && correspondingLineIndexes[0] !== undefined) {
65
+ lines.splice(correspondingLineIndexes[0], correspondingLineIndexes.length)
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Splits a markdown range that spans multiple lines into separate lines.
71
+ */
72
+ function splitRangeIntoSeparateLines(
73
+ lines: Paragraph[],
74
+ currentLine: Paragraph,
75
+ range: MarkdownRange,
76
+ correspondingLineIndexes: number[],
77
+ ) {
78
+ const mainLineRangeLength = currentLine.start + currentLine.length - range.start
79
+ // A range starting exactly at the newline has nothing on this line. Pushing the empty portion
80
+ // anyway made the builder render an empty span, and an empty span is given a `<br>` to keep the
81
+ // line height -- a line break in the text that the user never typed.
82
+ if (mainLineRangeLength > 0) {
83
+ currentLine.markdownRanges.push({
84
+ ...range,
85
+ length: mainLineRangeLength,
86
+ })
87
+ }
88
+
89
+ let rangeLength = range.length - mainLineRangeLength
90
+ correspondingLineIndexes.forEach((lineIndex) => {
91
+ const otherLine = lines[lineIndex] as Paragraph
92
+ let currentLength = otherLine.length
93
+ if (rangeLength <= currentLength) {
94
+ currentLength = rangeLength - 1
95
+ }
96
+
97
+ if (currentLength > 0) {
98
+ lines[lineIndex]?.markdownRanges.push({
99
+ ...range,
100
+ start: otherLine.start,
101
+ length: currentLength,
102
+ })
103
+ }
104
+
105
+ rangeLength -= currentLength
106
+ })
107
+ }
108
+
109
+ /**
110
+ * For singleline markdown types, the function splits markdown ranges that spread beyond the line length into separate lines.
111
+ * For multiline markdown types (like `pre`), it merges them and corresponding text into one line.
112
+ */
113
+ function normalizeLines(lines: Paragraph[], ranges: MarkdownRange[]) {
114
+ const mergedLines = [...lines]
115
+ const lineIndexes = mergedLines.map((_line, index) => index)
116
+
117
+ ranges.forEach((range) => {
118
+ const beginLineIndex = mergedLines.findLastIndex((line) => line.start <= range.start)
119
+ const endLineIndex = mergedLines.findIndex(
120
+ (line) => line.start + line.length >= range.start + range.length,
121
+ )
122
+ const correspondingLineIndexes = lineIndexes.slice(beginLineIndex, endLineIndex + 1)
123
+
124
+ if (correspondingLineIndexes.length > 0) {
125
+ const mainLineIndex = correspondingLineIndexes[0] as number
126
+ const mainLine = mergedLines[mainLineIndex] as Paragraph
127
+ const otherLineIndexes = correspondingLineIndexes.slice(1)
128
+
129
+ if (isMultilineMarkdownType(range.type)) {
130
+ mergeLinesWithMultilineTags(mergedLines, mainLine, range, otherLineIndexes)
131
+ } else if (otherLineIndexes.length > 0) {
132
+ splitRangeIntoSeparateLines(mergedLines, mainLine, range, otherLineIndexes)
133
+ } else {
134
+ mainLine.markdownRanges.push(range)
135
+ }
136
+ }
137
+ })
138
+
139
+ return mergedLines
140
+ }
141
+
142
+ /** Adds a value prop to the element and appends the value to the parent node element */
143
+ function appendValueToElement(
144
+ element: HTMLMarkdownElement,
145
+ parentTreeNode: TreeNode,
146
+ value: string,
147
+ ) {
148
+ const targetElement = element
149
+ const parentNode = parentTreeNode
150
+ targetElement.value = value
151
+ parentNode.element.value = (parentNode.element.value || "") + value
152
+ }
153
+
154
+ function appendNode(
155
+ element: HTMLMarkdownElement,
156
+ parentTreeNode: TreeNode,
157
+ type: NodeType,
158
+ length: number,
159
+ start: number | null = null,
160
+ ) {
161
+ const node = addNodeToTree(element, parentTreeNode, type, length, start)
162
+ parentTreeNode.element.appendChild(element)
163
+ return node
164
+ }
165
+
166
+ function addBrElement(node: TreeNode) {
167
+ const span = document.createElement("span") as HTMLMarkdownElement
168
+ span.setAttribute("data-type", "br")
169
+ appendValueToElement(span, node, "\n")
170
+ const spanNode = appendNode(span, node, "br", 1)
171
+ appendNode(document.createElement("br") as unknown as HTMLMarkdownElement, spanNode, "br", 1)
172
+ return spanNode
173
+ }
174
+
175
+ function addTextToElement(node: TreeNode, text: string, isMultiline = true) {
176
+ const lines = text.split("\n")
177
+ lines.forEach((line, index) => {
178
+ if (line !== "") {
179
+ const span = document.createElement("span") as HTMLMarkdownElement
180
+ appendValueToElement(span, node, line)
181
+ span.setAttribute("data-type", "text")
182
+ span.appendChild(document.createTextNode(line))
183
+ appendNode(span, node, "text", line.length)
184
+
185
+ const parentType = span.parentElement?.dataset.type
186
+ if (
187
+ !isMultiline &&
188
+ parentType &&
189
+ ["pre", "code", "mention-here", "mention-user", "mention-report"].includes(parentType)
190
+ ) {
191
+ // this is a fix to background colors being shifted downwards in a singleline input
192
+ addStyleToBlock(span, "text", {}, false)
193
+ }
194
+ }
195
+
196
+ // Only add BR elements for multiline inputs or when there are actual line breaks
197
+ if (isMultiline && (index < lines.length - 1 || (index === 0 && line === ""))) {
198
+ addBrElement(node)
199
+ }
200
+ })
201
+ }
202
+
203
+ function addParagraph(
204
+ node: TreeNode,
205
+ text: string | null,
206
+ length: number,
207
+ layout: LineLayout | undefined,
208
+ markdownStyle: PartialMarkdownStyle,
209
+ disableInlineStyles = false,
210
+ ) {
211
+ const p = document.createElement("p")
212
+ p.setAttribute("data-type", "line")
213
+ if (!disableInlineStyles) {
214
+ addLineLayout(p, layout, markdownStyle)
215
+ }
216
+
217
+ const pNode = appendNode(p as unknown as HTMLMarkdownElement, node, "line", length)
218
+
219
+ if (text === "") {
220
+ // If the line is empty, we still need to add a br element to keep the line height
221
+ addBrElement(pNode)
222
+ } else if (text) {
223
+ addTextToElement(pNode, text)
224
+ }
225
+
226
+ return pNode
227
+ }
228
+
229
+ /**
230
+ * `syntax`, `block-prefix` and `emoji` decorate whatever they sit in, so where ranges cover exactly
231
+ * the same text those three have to come last: the container, then the `block-prefix` holding its
232
+ * marker, then the `syntax` of the marker itself. Typing a bare `>` produces all three over one
233
+ * character, and the parser emits them in precisely the wrong order -- `flushLine` hands the marker
234
+ * over before the container it belongs to, because that is the pairing the native formatters read.
235
+ * Inverting that is the whole job of `getTagPriority`.
236
+ *
237
+ * Every other tie is already right. A range is emitted when its construct opens, so emission order
238
+ * is containment order, and a stable sort leaves it alone. That is also the only thing that *can*
239
+ * order two containers of the same size: `> - a` and `- > a` are the same pair of containers over
240
+ * the same line, nested opposite ways, and no table of types can say which.
241
+ *
242
+ * So the one thing worth checking is a decoration that sorted ahead of something it belongs to --
243
+ * which is what a container type missing from `getTagPriority` produces. The builder then renders
244
+ * that text once per range, silently, multiplying with every keystroke. Nothing about that failure
245
+ * points at the missing table entry, so say it out loud instead.
246
+ */
247
+ const INNERMOST_TYPES = new Set<MarkdownType>(["block-prefix", "syntax", "emoji"])
248
+
249
+ function reportAmbiguousNesting(ranges: MarkdownRange[]) {
250
+ for (let i = 1; i < ranges.length; i++) {
251
+ const previous = ranges[i - 1] as MarkdownRange
252
+ const current = ranges[i] as MarkdownRange
253
+
254
+ if (
255
+ previous.start === current.start &&
256
+ previous.length === current.length &&
257
+ INNERMOST_TYPES.has(previous.type) &&
258
+ !INNERMOST_TYPES.has(current.type)
259
+ ) {
260
+ // eslint-disable-next-line no-console
261
+ console.error(
262
+ `[react-native-marcus] \`${previous.type}\` sorts ahead of \`${current.type}\` over the same text, so the marker ends up outside what it belongs to and renders once per range. Give \`${current.type}\` a higher priority in src/rangeUtils.ts.`,
263
+ )
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Builds one `<p data-type="line">` under `rootNode`.
270
+ *
271
+ * Lines are independent: nothing carries across from the previous one, which is what lets an update
272
+ * rebuild a single line and leave the rest of the document alone.
273
+ */
274
+ function addLine(
275
+ rootNode: TreeNode,
276
+ line: Paragraph,
277
+ textLength: number,
278
+ hasRanges: boolean,
279
+ isMultiline: boolean,
280
+ markdownStyle: PartialMarkdownStyle,
281
+ disableInlineStyles: boolean,
282
+ ) {
283
+ if (!hasRanges) {
284
+ return addParagraph(
285
+ rootNode,
286
+ line.text,
287
+ line.length,
288
+ line.layout,
289
+ markdownStyle,
290
+ disableInlineStyles,
291
+ )
292
+ }
293
+
294
+ const lineNode = addParagraph(
295
+ rootNode,
296
+ null,
297
+ line.length,
298
+ line.layout,
299
+ markdownStyle,
300
+ disableInlineStyles,
301
+ )
302
+ let currentParentNode: TreeNode = lineNode
303
+
304
+ if (line.markdownRanges.length === 0) {
305
+ addTextToElement(currentParentNode, line.text, isMultiline)
306
+ }
307
+
308
+ let lastRangeEndIndex = line.start
309
+ const lineMarkdownRanges = [...line.markdownRanges]
310
+ // go through all markdown ranges in the line
311
+ while (lineMarkdownRanges.length > 0) {
312
+ const range = lineMarkdownRanges.shift()
313
+ if (!range) {
314
+ break
315
+ }
316
+
317
+ const endOfCurrentRange = range.start + range.length
318
+ const nextRangeStartIndex =
319
+ lineMarkdownRanges.length > 0 && !!lineMarkdownRanges[0]
320
+ ? lineMarkdownRanges[0].start || 0
321
+ : textLength
322
+
323
+ // add text before the markdown range. Ranges that start behind the cursor are already
324
+ // rendered; without the guard `substring` would swap its arguments and emit that text again.
325
+ const textBeforeRange =
326
+ range.start > lastRangeEndIndex
327
+ ? line.text.substring(lastRangeEndIndex - line.start, range.start - line.start)
328
+ : ""
329
+ if (textBeforeRange) {
330
+ addTextToElement(currentParentNode, textBeforeRange, isMultiline)
331
+ }
332
+
333
+ // create markdown span element
334
+ const span = document.createElement("span") as HTMLMarkdownElement
335
+ span.setAttribute("data-type", range.type)
336
+
337
+ const spanNode = appendNode(span, currentParentNode, range.type, range.length, range.start)
338
+
339
+ // Styled once it is in the tree, not before: a heading's size depends on how many heading
340
+ // levels enclose it, and an element with no parent looks like the outermost one.
341
+ if (!disableInlineStyles) {
342
+ addStyleToBlock(span, range.type, markdownStyle, isMultiline)
343
+ if (range.type === "block-prefix") {
344
+ addBlockPrefixGap(span, line.layout?.gaps.get(range.start))
345
+ }
346
+ }
347
+
348
+ if (
349
+ lineMarkdownRanges.length > 0 &&
350
+ nextRangeStartIndex < endOfCurrentRange &&
351
+ range.type !== "syntax"
352
+ ) {
353
+ // tag nesting
354
+ currentParentNode = spanNode
355
+ lastRangeEndIndex = range.start
356
+ } else {
357
+ // adding markdown tag
358
+ addTextToElement(spanNode, line.text.substring(range.start - line.start, endOfCurrentRange - line.start), isMultiline)
359
+ currentParentNode.element.value =
360
+ (currentParentNode.element.value || "") + (spanNode.element.value || "")
361
+ lastRangeEndIndex = endOfCurrentRange
362
+ // tag unnesting and adding text after the tag
363
+ while (
364
+ currentParentNode.parentNode !== null &&
365
+ nextRangeStartIndex >= currentParentNode.start + currentParentNode.length
366
+ ) {
367
+ const parentEndIndex = currentParentNode.start + currentParentNode.length
368
+ const textAfterRange =
369
+ parentEndIndex > lastRangeEndIndex
370
+ ? line.text.substring(lastRangeEndIndex - line.start, parentEndIndex - line.start)
371
+ : ""
372
+ if (textAfterRange) {
373
+ addTextToElement(currentParentNode, textAfterRange, isMultiline)
374
+ }
375
+ // Never backwards: an enclosing range can end before the text already consumed inside it
376
+ // -- a list whose item holds a code block runs to the end of its marker line while the
377
+ // block inside it runs on for several more. Letting it rewind made the paragraph re-emit
378
+ // everything from there, so the line rendered twice.
379
+ lastRangeEndIndex = parentEndIndex > lastRangeEndIndex ? parentEndIndex : lastRangeEndIndex
380
+ // A line's content never escapes its own paragraph. Without this the walk could reach the
381
+ // root, and every later range on the line was appended there instead -- a stray top-level
382
+ // element between the paragraphs, which shifts every `data-id` after it and leaves the
383
+ // line it belonged to rendered in two places.
384
+ if (currentParentNode === lineNode) {
385
+ break
386
+ }
387
+ if (currentParentNode.parentNode.type !== "root") {
388
+ currentParentNode.parentNode.element.value = currentParentNode.element.value || ""
389
+ }
390
+ currentParentNode = currentParentNode.parentNode || rootNode
391
+ }
392
+ }
393
+ }
394
+
395
+ return lineNode
396
+ }
397
+
398
+ /** Splits the text into lines and hands each the markdown ranges that fall inside it. */
399
+ function prepareLines(
400
+ text: string,
401
+ ranges: MarkdownRange[],
402
+ markdownStyle: PartialMarkdownStyle,
403
+ measure: TextMeasurer,
404
+ ) {
405
+ const lines = splitTextIntoLines(text)
406
+
407
+ if (ranges.length === 0) {
408
+ return lines
409
+ }
410
+
411
+ // Before anything reorders them: the block walk reads the ranges in the order the parser emits
412
+ // them -- each marker immediately before the container it belongs to -- and `sortRanges` sorts
413
+ // that order away in place.
414
+ const layouts = layoutBlocks(text, ranges, markdownStyle, measure)
415
+
416
+ // Sort all ranges by start position, length, and by tag hierarchy so the styles and text are applied in correct order
417
+ const sortedRanges = sortRanges(ranges)
418
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
419
+ reportAmbiguousNesting(sortedRanges)
420
+ }
421
+
422
+ const normalized = normalizeLines(lines, ungroupRanges(sortedRanges))
423
+
424
+ // Ranges reach a line in document order, but a range spanning several lines hands its tail to
425
+ // those lines as it is processed -- ahead of ranges that start later on the same line and should
426
+ // have come first. `1. *l\nd**` puts an italic that opened on line 1 in front of the list its
427
+ // second half sits inside, and the builder then nests the list *within* the italic and renders
428
+ // the overlap twice. Restoring the order per line costs nothing on lines that were already
429
+ // sorted, which is nearly all of them.
430
+ normalized.forEach((paragraph) => {
431
+ const line = paragraph
432
+ sortRanges(line.markdownRanges)
433
+ line.layout = layouts.get(line.start)
434
+ })
435
+
436
+ return normalized
437
+ }
438
+
439
+ /** Builds HTML DOM structure based on passed text and markdown ranges */
440
+ function parseRangesToHTMLNodes(
441
+ text: string,
442
+ ranges: MarkdownRange[],
443
+ isMultiline = true,
444
+ markdownStyle: PartialMarkdownStyle = {},
445
+ disableInlineStyles = false,
446
+ measure: TextMeasurer = createTextMeasurer(null),
447
+ ) {
448
+ const rootElement: HTMLMarkdownElement = document.createElement("span") as HTMLMarkdownElement
449
+ const textLength = text.length
450
+ const rootNode: TreeNode = createRootTreeNode(rootElement, textLength)
451
+
452
+ // Built line by line, exactly as an update builds the lines it has to replace, so the two agree
453
+ // by construction and each line sits at the offset the text gives it rather than at wherever the
454
+ // line before it happened to end.
455
+ prepareLines(text, ranges, markdownStyle, measure).forEach((line, index) => {
456
+ const node = buildLine(
457
+ line,
458
+ index,
459
+ textLength,
460
+ ranges.length > 0,
461
+ isMultiline,
462
+ markdownStyle,
463
+ disableInlineStyles,
464
+ )
465
+ node.parentNode = rootNode
466
+ rootNode.childNodes.push(node)
467
+ rootElement.appendChild(node.element)
468
+ })
469
+
470
+ return { dom: rootElement, tree: rootNode }
471
+ }
472
+
473
+ /**
474
+ * A line as it was last rendered: what it was built from, the markup it produced, and its subtree.
475
+ */
476
+ type CachedLine = {
477
+ signature: string
478
+ html: string
479
+ node: TreeNode
480
+ }
481
+
482
+ type LineCache = {
483
+ markdownStyle: PartialMarkdownStyle
484
+ isMultiline: boolean
485
+ lines: CachedLine[]
486
+ }
487
+
488
+ /**
489
+ * Everything a line's markup depends on, as a string.
490
+ *
491
+ * A line is rendered from its own text and the ranges falling inside it, and nothing else -- so two
492
+ * lines with equal signatures render identically and the second one need not be built at all. Range
493
+ * offsets go in relative to the line, so a line that only moved still matches.
494
+ */
495
+ function signatureOf(line: Paragraph, hasRanges: boolean) {
496
+ let signature = hasRanges ? "r" : "-"
497
+ signature += line.text
498
+
499
+ line.markdownRanges.forEach((range) => {
500
+ signature += `\u0000${range.start - line.start},${range.length},${range.type},${range.depth ?? ""}`
501
+ })
502
+
503
+ // The layout is not derived from this line alone -- a continuation line indents past a marker
504
+ // that belongs to the line that opened the block, and every width comes out of the font the
505
+ // input happens to be rendering in. Both reach the line only through these numbers, so they are
506
+ // what the signature has to compare.
507
+ const layout = line.layout
508
+ if (layout) {
509
+ signature += `\u0000${layout.firstLineIndent},${layout.indent},${layout.ribbons.join(":")}`
510
+ layout.gaps.forEach((gap, start) => {
511
+ signature += `,${start - line.start}:${gap}`
512
+ })
513
+ }
514
+
515
+ return signature
516
+ }
517
+
518
+ /** Moves a subtree along the text by `delta`, for a line that shifted without changing. */
519
+ function shiftSubtree(node: TreeNode, delta: number) {
520
+ const target = node
521
+ target.start += delta
522
+ target.childNodes.forEach((child) => shiftSubtree(child, delta))
523
+ }
524
+
525
+ /** Renumbers a subtree, for a line whose index changed because lines were added or removed above. */
526
+ function restampSubtree(node: TreeNode, orderIndex: string) {
527
+ const target = node
528
+ target.orderIndex = orderIndex
529
+ target.element.setAttribute("data-id", orderIndex)
530
+ target.childNodes.forEach((child, index) => restampSubtree(child, `${orderIndex},${index}`))
531
+ }
532
+
533
+ /**
534
+ * Grows every node to at least cover its children.
535
+ *
536
+ * A container range can be shorter than what the builder puts inside it -- a list item's own range
537
+ * stops at the end of its marker line, while a fenced code block opened there runs on for several
538
+ * more. The tree is what maps a text offset onto a node, so a parent that does not reach the end of
539
+ * its own children leaves that overhang mapping to nothing and the caret cannot be placed in it.
540
+ *
541
+ * It runs once the line is built, never during: a node's length decides where its next sibling
542
+ * starts and how far the builder thinks it has got, so growing one mid-build changes the markup.
543
+ */
544
+ function coverChildren(node: TreeNode) {
545
+ const target = node
546
+ let end = target.start + target.length
547
+
548
+ target.childNodes.forEach((child) => {
549
+ coverChildren(child)
550
+ const childEnd = child.start + child.length
551
+ if (childEnd > end) {
552
+ end = childEnd
553
+ }
554
+ })
555
+
556
+ target.length = end - target.start
557
+ }
558
+
559
+ /** Builds one line on its own, then places it at the offset and index it occupies in the document. */
560
+ function buildLine(
561
+ line: Paragraph,
562
+ index: number,
563
+ textLength: number,
564
+ hasRanges: boolean,
565
+ isMultiline: boolean,
566
+ markdownStyle: PartialMarkdownStyle,
567
+ disableInlineStyles = false,
568
+ ) {
569
+ // The scratch root carries the line's offset, so the subtree comes out with the absolute `start`
570
+ // values the builder compares against range offsets -- the same ones it would get if the whole
571
+ // document had been built in one pass.
572
+ const scratch = createRootTreeNode(
573
+ document.createElement("span") as HTMLMarkdownElement,
574
+ line.length,
575
+ line.start,
576
+ )
577
+ const node = addLine(
578
+ scratch,
579
+ line,
580
+ textLength,
581
+ hasRanges,
582
+ isMultiline,
583
+ markdownStyle,
584
+ disableInlineStyles,
585
+ )
586
+
587
+ coverChildren(node)
588
+ restampSubtree(node, `${index}`)
589
+
590
+ return node
591
+ }
592
+
593
+ /**
594
+ * A cached line can be reused only if it would be rebuilt identically *and* the live element is
595
+ * still exactly what was put there.
596
+ *
597
+ * The identity check matters as much as the signature: the browser edits the input directly as the
598
+ * user types, and it is free to merge, split or wrap the top-level elements. Requiring the live
599
+ * node to be the very one this entry rendered means anything the browser introduced falls outside
600
+ * the reusable prefix and suffix, and so lands in the run that gets replaced.
601
+ */
602
+ function canReuseLine(
603
+ cached: CachedLine | undefined,
604
+ signature: string | undefined,
605
+ liveNode: ChildNode | undefined,
606
+ ) {
607
+ return (
608
+ !!cached &&
609
+ cached.signature === signature &&
610
+ liveNode === cached.node.element &&
611
+ (liveNode as HTMLElement).outerHTML === cached.html
612
+ )
613
+ }
614
+
615
+ /**
616
+ * Renders `lines` into the input, building only the ones that changed.
617
+ *
618
+ * Rebuilding every line and then diffing meant the expensive half of an update happened whatever
619
+ * the edit was -- construction is the dominant cost, not applying the result. Matching lines are
620
+ * skipped from both ends by signature, so a keystroke builds one line and the rest of the document
621
+ * keeps the elements and the subtrees it already had. Lines after the edit are shifted along the
622
+ * text, and renumbered if lines were added or removed above them; both are cheap walks that touch
623
+ * no element beyond a `data-id`.
624
+ */
625
+ function reconcileLines(
626
+ targetElement: MarkdownTextInputElement,
627
+ lines: Paragraph[],
628
+ textLength: number,
629
+ hasRanges: boolean,
630
+ isMultiline: boolean,
631
+ markdownStyle: PartialMarkdownStyle,
632
+ shouldForceDOMUpdate: boolean,
633
+ ) {
634
+ const signatures = lines.map((line) => signatureOf(line, hasRanges))
635
+ const cache = targetElement.lineCache
636
+ const previous =
637
+ !shouldForceDOMUpdate &&
638
+ cache &&
639
+ cache.markdownStyle === markdownStyle &&
640
+ cache.isMultiline === isMultiline
641
+ ? cache.lines
642
+ : []
643
+ const live = Array.from(targetElement.childNodes)
644
+
645
+ let head = 0
646
+ while (
647
+ head < lines.length &&
648
+ head < previous.length &&
649
+ canReuseLine(previous[head], signatures[head], live[head])
650
+ ) {
651
+ head += 1
652
+ }
653
+
654
+ let tail = lines.length
655
+ let previousTail = previous.length
656
+ let liveTail = live.length
657
+ while (
658
+ tail > head &&
659
+ previousTail > head &&
660
+ liveTail > head &&
661
+ canReuseLine(previous[previousTail - 1], signatures[tail - 1], live[liveTail - 1])
662
+ ) {
663
+ tail -= 1
664
+ previousTail -= 1
665
+ liveTail -= 1
666
+ }
667
+
668
+ const fresh: TreeNode[] = []
669
+ for (let i = head; i < tail; i += 1) {
670
+ fresh.push(
671
+ buildLine(
672
+ lines[i] as Paragraph,
673
+ i,
674
+ textLength,
675
+ hasRanges,
676
+ isMultiline,
677
+ markdownStyle,
678
+ ),
679
+ )
680
+ }
681
+
682
+ // Read before removing anything: this is the first live node of the matching tail, which stays.
683
+ const anchor = live[liveTail] ?? null
684
+ for (let i = head; i < liveTail; i += 1) {
685
+ live[i]?.remove()
686
+ }
687
+
688
+ if (fresh.length > 0) {
689
+ const fragment = document.createDocumentFragment()
690
+ fresh.forEach((node) => fragment.appendChild(node.element))
691
+ targetElement.insertBefore(fragment, anchor)
692
+ }
693
+
694
+ const rootNode = createRootTreeNode(targetElement, textLength)
695
+ const nextCache: CachedLine[] = []
696
+
697
+ for (let i = 0; i < lines.length; i += 1) {
698
+ const line = lines[i] as Paragraph
699
+ let node: TreeNode
700
+ let html: string
701
+
702
+ if (i < head) {
703
+ const cached = previous[i] as CachedLine
704
+ node = cached.node
705
+ html = cached.html
706
+ } else if (i < tail) {
707
+ node = fresh[i - head] as TreeNode
708
+ html = (node.element as HTMLElement).outerHTML
709
+ } else {
710
+ const cached = previous[previousTail + (i - tail)] as CachedLine
711
+ node = cached.node
712
+ const delta = line.start - node.start
713
+ if (delta !== 0) {
714
+ shiftSubtree(node, delta)
715
+ }
716
+ if (node.orderIndex !== `${i}`) {
717
+ restampSubtree(node, `${i}`)
718
+ html = (node.element as HTMLElement).outerHTML
719
+ } else {
720
+ html = cached.html
721
+ }
722
+ }
723
+
724
+ node.parentNode = rootNode
725
+ rootNode.childNodes.push(node)
726
+ nextCache.push({ signature: signatures[i] as string, html, node })
727
+ }
728
+
729
+ targetElement.lineCache = { markdownStyle, isMultiline, lines: nextCache }
730
+
731
+ return rootNode
732
+ }
733
+
734
+ function moveCursor(
735
+ isFocused: boolean,
736
+ alwaysMoveCursorToTheEnd: boolean,
737
+ cursorPosition: number | null,
738
+ target: MarkdownTextInputElement,
739
+ shouldScrollIntoView = false,
740
+ ) {
741
+ if (!isFocused) {
742
+ return
743
+ }
744
+
745
+ if (alwaysMoveCursorToTheEnd || cursorPosition === null) {
746
+ moveCursorToEnd(target)
747
+ } else if (cursorPosition !== null) {
748
+ setCursorPosition(target, cursorPosition, null, shouldScrollIntoView)
749
+ }
750
+ }
751
+
752
+ function updateInputStructure(
753
+ parserFunction: (input: string) => MarkdownRange[],
754
+ target: MarkdownTextInputElement,
755
+ text: string,
756
+ cursorPositionIndex: number | null,
757
+ isMultiline = true,
758
+ markdownStyle: PartialMarkdownStyle = {},
759
+ alwaysMoveCursorToTheEnd = false,
760
+ shouldForceDOMUpdate = false,
761
+ shouldScrollIntoView = false,
762
+ ) {
763
+ const targetElement = target
764
+
765
+ // in case the cursorPositionIndex is larger than text length, cursorPosition will be null, i.e: move the caret to the end
766
+ let cursorPosition: number | null =
767
+ cursorPositionIndex !== null && cursorPositionIndex <= text.length
768
+ ? cursorPositionIndex
769
+ : null
770
+ const isFocused = document.activeElement === target
771
+ if (isFocused && cursorPositionIndex === null) {
772
+ const selection = getCurrentCursorPosition(target)
773
+ cursorPosition = selection ? selection.start : null
774
+ }
775
+ const markdownRanges = parserFunction(text)
776
+ if (
777
+ !text ||
778
+ targetElement.innerHTML === "<br>" ||
779
+ (targetElement && targetElement.innerHTML === "\n")
780
+ ) {
781
+ targetElement.innerHTML = ""
782
+ targetElement.innerText = ""
783
+ }
784
+
785
+ // We don't want to parse text with single '\n', because contentEditable represents it as invisible <br />
786
+ if (text) {
787
+ targetElement.tree = reconcileLines(
788
+ targetElement,
789
+ prepareLines(text, markdownRanges, markdownStyle, createTextMeasurer(targetElement)),
790
+ text.length,
791
+ markdownRanges.length > 0,
792
+ isMultiline,
793
+ markdownStyle,
794
+ shouldForceDOMUpdate,
795
+ )
796
+
797
+ moveCursor(
798
+ isFocused,
799
+ alwaysMoveCursorToTheEnd,
800
+ cursorPosition,
801
+ targetElement,
802
+ shouldScrollIntoView,
803
+ )
804
+ } else {
805
+ targetElement.tree = createRootTreeNode(targetElement)
806
+ targetElement.lineCache = undefined
807
+ }
808
+
809
+ return { text, cursorPosition: cursorPosition || 0 }
810
+ }
811
+
812
+ export { updateInputStructure, parseRangesToHTMLNodes, normalizeLines }
813
+ export type { Paragraph, LineCache }