react-native-marcus 0.1.0 → 0.2.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 (294) hide show
  1. package/README.md +220 -63
  2. package/RNMarcus.podspec +6 -0
  3. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +2 -1
  4. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorView.kt +168 -0
  5. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManager.kt +38 -0
  6. package/android/src/main/java/app/getbullet/marcus/MarcusTextDecoratorViewManagerSpec.kt +19 -0
  7. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +23 -1
  8. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPainter.kt +147 -0
  9. package/android/src/main/java/app/getbullet/marcus/MarkdownBackgroundPass.kt +19 -0
  10. package/android/src/main/java/app/getbullet/marcus/MarkdownCopy.kt +87 -0
  11. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +652 -113
  12. package/android/src/main/java/app/getbullet/marcus/MarkdownRanges.kt +47 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +140 -61
  14. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +37 -7
  15. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownAbsoluteLineHeightSpan.kt +48 -0
  16. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +102 -48
  17. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +22 -4
  18. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBulletSpan.kt +78 -0
  19. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpan.kt +177 -0
  20. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontStyleSpan.kt +32 -0
  21. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontWeightSpan.kt +34 -0
  22. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +24 -11
  23. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLetterSpacingSpan.kt +32 -0
  24. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownTextShadowSpan.kt +32 -0
  25. package/android/src/main/new_arch/CMakeLists.txt +17 -0
  26. package/android/src/main/new_arch/RNMarcusSpec.h +1 -0
  27. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.cpp +235 -0
  28. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorShadowNode.h +62 -0
  29. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  30. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +22 -3
  31. package/android/src/test/java/app/getbullet/marcus/MarkdownCopyTest.kt +69 -0
  32. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +170 -25
  33. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +22 -5
  34. package/android/src/test/java/app/getbullet/marcus/spans/MarkdownCodeBlockSpanTest.kt +91 -0
  35. package/android/src/test/resources/render-model.txt +221 -16
  36. package/apple/MarcusSwizzle.h +25 -0
  37. package/apple/MarcusTextDecoratorComponentView.h +22 -0
  38. package/apple/MarcusTextDecoratorComponentView.mm +72 -0
  39. package/apple/MarcusTextDecoratorShadowNode.h +62 -0
  40. package/apple/MarcusTextDecoratorShadowNode.mm +192 -0
  41. package/apple/MarcusTextDecoratorViewManager.h +4 -0
  42. package/apple/MarcusTextDecoratorViewManager.mm +11 -0
  43. package/apple/MarcusTextInputDecoratorComponentView.mm +1 -1
  44. package/apple/MarcusTextInputDecoratorShadowNode.mm +1 -1
  45. package/apple/MarcusTextLayoutManager.h +40 -0
  46. package/apple/MarcusTextLayoutManager.mm +173 -0
  47. package/apple/MarkdownAttributes.h +23 -2
  48. package/apple/MarkdownAttributes.m +6 -0
  49. package/apple/MarkdownCopy.swift +63 -0
  50. package/apple/MarkdownDecorator.swift +2 -2
  51. package/apple/MarkdownFormatter+React.swift +23 -0
  52. package/apple/MarkdownFormatter.swift +869 -148
  53. package/apple/MarkdownParagraphLayoutManager.swift +455 -0
  54. package/apple/MarkdownSwiftInterop.h +0 -1
  55. package/apple/MarkdownTextBackground.swift +59 -1
  56. package/apple/MarkdownTextFieldObserver.swift +2 -2
  57. package/apple/MarkdownTextLayoutFragment.swift +159 -33
  58. package/apple/MarkdownTextLayoutManagerDelegate.swift +67 -3
  59. package/apple/MarkdownTextStorageDelegate.swift +2 -2
  60. package/apple/MarkdownTextViewObserver.swift +2 -2
  61. package/apple/MarkdownUtils.swift +38 -0
  62. package/apple/MarkdownWritingDirection.swift +132 -0
  63. package/apple/RCTMarcusStyle+Codegen.h +115 -9
  64. package/apple/RCTMarcusStyle.h +128 -31
  65. package/apple/RCTMarcusStyle.m +48 -0
  66. package/apple/RCTParagraphComponentView+Marcus.h +17 -0
  67. package/apple/RCTParagraphComponentView+Marcus.mm +38 -0
  68. package/apple/RCTTextInputComponentView+Marcus.mm +1 -25
  69. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorState.h +37 -0
  70. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextDecoratorViewComponentDescriptor.h +17 -0
  71. package/lib/commonjs/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  72. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  73. package/lib/commonjs/MarkdownText.js +330 -0
  74. package/lib/commonjs/MarkdownText.js.map +1 -0
  75. package/lib/commonjs/MarkdownText.web.js +529 -0
  76. package/lib/commonjs/MarkdownText.web.js.map +1 -0
  77. package/lib/commonjs/MarkdownTextInput.js +22 -27
  78. package/lib/commonjs/MarkdownTextInput.js.map +1 -1
  79. package/lib/commonjs/MarkdownTextInput.web.js +12 -7
  80. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  81. package/lib/commonjs/atomUtils.js +112 -0
  82. package/lib/commonjs/atomUtils.js.map +1 -0
  83. package/lib/commonjs/childrenUtils.js +36 -0
  84. package/lib/commonjs/childrenUtils.js.map +1 -0
  85. package/lib/commonjs/index.js +8 -7
  86. package/lib/commonjs/index.js.map +1 -1
  87. package/lib/commonjs/math/environment.js +69 -0
  88. package/lib/commonjs/math/environment.js.map +1 -0
  89. package/lib/commonjs/math/index.js +61 -0
  90. package/lib/commonjs/math/index.js.map +1 -0
  91. package/lib/commonjs/math/mathjax.js +548 -0
  92. package/lib/commonjs/math/mathjax.js.map +1 -0
  93. package/lib/commonjs/metro.js +172 -0
  94. package/lib/commonjs/metro.js.map +1 -0
  95. package/lib/commonjs/parser/index.d.ts +3 -0
  96. package/lib/commonjs/parser/index.js +450 -42
  97. package/lib/commonjs/parser/index.js.map +4 -4
  98. package/lib/commonjs/stripUtils.js +743 -0
  99. package/lib/commonjs/stripUtils.js.map +1 -0
  100. package/lib/commonjs/styleUtils.js +194 -23
  101. package/lib/commonjs/styleUtils.js.map +1 -1
  102. package/lib/commonjs/web/MarkdownElements.css +67 -0
  103. package/lib/commonjs/web/MarkdownTextInput.css +9 -7
  104. package/lib/commonjs/web/utils/blockLayout.js +95 -7
  105. package/lib/commonjs/web/utils/blockLayout.js.map +1 -1
  106. package/lib/commonjs/web/utils/blockUtils.js +327 -54
  107. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  108. package/lib/commonjs/web/utils/elementUtils.js +80 -0
  109. package/lib/commonjs/web/utils/elementUtils.js.map +1 -0
  110. package/lib/commonjs/web/utils/parserUtils.js +84 -28
  111. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  112. package/lib/commonjs/web/utils/treeUtils.js +6 -0
  113. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  114. package/lib/commonjs/web/utils/webStyleUtils.js +25 -5
  115. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -1
  116. package/lib/module/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  117. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  118. package/lib/module/MarkdownText.js +325 -0
  119. package/lib/module/MarkdownText.js.map +1 -0
  120. package/lib/module/MarkdownText.web.js +524 -0
  121. package/lib/module/MarkdownText.web.js.map +1 -0
  122. package/lib/module/MarkdownTextInput.js +24 -29
  123. package/lib/module/MarkdownTextInput.js.map +1 -1
  124. package/lib/module/MarkdownTextInput.web.js +12 -7
  125. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  126. package/lib/module/atomUtils.js +106 -0
  127. package/lib/module/atomUtils.js.map +1 -0
  128. package/lib/module/childrenUtils.js +33 -0
  129. package/lib/module/childrenUtils.js.map +1 -0
  130. package/lib/module/index.js +1 -1
  131. package/lib/module/index.js.map +1 -1
  132. package/lib/module/math/environment.js +67 -0
  133. package/lib/module/math/environment.js.map +1 -0
  134. package/lib/module/math/index.js +56 -0
  135. package/lib/module/math/index.js.map +1 -0
  136. package/lib/module/math/mathjax.js +541 -0
  137. package/lib/module/math/mathjax.js.map +1 -0
  138. package/lib/module/metro.js +169 -0
  139. package/lib/module/metro.js.map +1 -0
  140. package/lib/module/parser/index.d.ts +3 -0
  141. package/lib/module/parser/index.js +450 -42
  142. package/lib/module/parser/index.js.map +4 -4
  143. package/lib/module/stripUtils.js +739 -0
  144. package/lib/module/stripUtils.js.map +1 -0
  145. package/lib/module/styleUtils.js +196 -25
  146. package/lib/module/styleUtils.js.map +1 -1
  147. package/lib/module/web/MarkdownElements.css +67 -0
  148. package/lib/module/web/MarkdownTextInput.css +9 -7
  149. package/lib/module/web/utils/blockLayout.js +94 -8
  150. package/lib/module/web/utils/blockLayout.js.map +1 -1
  151. package/lib/module/web/utils/blockUtils.js +328 -56
  152. package/lib/module/web/utils/blockUtils.js.map +1 -1
  153. package/lib/module/web/utils/elementUtils.js +75 -0
  154. package/lib/module/web/utils/elementUtils.js.map +1 -0
  155. package/lib/module/web/utils/parserUtils.js +86 -30
  156. package/lib/module/web/utils/parserUtils.js.map +1 -1
  157. package/lib/module/web/utils/treeUtils.js +6 -0
  158. package/lib/module/web/utils/treeUtils.js.map +1 -1
  159. package/lib/module/web/utils/webStyleUtils.js +25 -5
  160. package/lib/module/web/utils/webStyleUtils.js.map +1 -1
  161. package/lib/typescript/commonjs/e2e/links.test.d.ts +2 -0
  162. package/lib/typescript/commonjs/e2e/links.test.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  164. package/lib/typescript/commonjs/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  166. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/src/MarkdownText.d.ts +127 -0
  168. package/lib/typescript/commonjs/src/MarkdownText.d.ts.map +1 -0
  169. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts +126 -0
  170. package/lib/typescript/commonjs/src/MarkdownText.web.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +23 -1
  172. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +26 -4
  174. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/src/atomUtils.d.ts +58 -0
  176. package/lib/typescript/commonjs/src/atomUtils.d.ts.map +1 -0
  177. package/lib/typescript/commonjs/src/childrenUtils.d.ts +12 -0
  178. package/lib/typescript/commonjs/src/childrenUtils.d.ts.map +1 -0
  179. package/lib/typescript/commonjs/src/commonTypes.d.ts +39 -3
  180. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -1
  181. package/lib/typescript/commonjs/src/index.d.ts +2 -1
  182. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/src/math/environment.d.ts +7 -0
  184. package/lib/typescript/commonjs/src/math/environment.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/src/math/index.d.ts +64 -0
  186. package/lib/typescript/commonjs/src/math/index.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/src/math/mathjax.d.ts +63 -0
  188. package/lib/typescript/commonjs/src/math/mathjax.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/src/metro.d.ts +59 -0
  190. package/lib/typescript/commonjs/src/metro.d.ts.map +1 -0
  191. package/lib/typescript/commonjs/src/parser/index.d.ts +3 -3
  192. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -1
  193. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/src/rangeUtils.d.ts +1 -1
  195. package/lib/typescript/commonjs/src/stripUtils.d.ts +108 -0
  196. package/lib/typescript/commonjs/src/stripUtils.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/styleUtils.d.ts +62 -4
  198. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -1
  199. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +32 -4
  200. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -1
  201. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +20 -8
  202. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -1
  203. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts +37 -0
  204. package/lib/typescript/commonjs/src/web/utils/elementUtils.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +5 -5
  206. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -1
  207. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +6 -1
  208. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -1
  209. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +2 -2
  210. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -1
  211. package/lib/typescript/module/e2e/links.test.d.ts +2 -0
  212. package/lib/typescript/module/e2e/links.test.d.ts.map +1 -0
  213. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts +93 -0
  214. package/lib/typescript/module/src/MarcusTextDecoratorViewNativeComponent.d.ts.map +1 -0
  215. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +64 -51
  216. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  217. package/lib/typescript/module/src/MarkdownText.d.ts +127 -0
  218. package/lib/typescript/module/src/MarkdownText.d.ts.map +1 -0
  219. package/lib/typescript/module/src/MarkdownText.web.d.ts +126 -0
  220. package/lib/typescript/module/src/MarkdownText.web.d.ts.map +1 -0
  221. package/lib/typescript/module/src/MarkdownTextInput.d.ts +23 -1
  222. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -1
  223. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +26 -4
  224. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -1
  225. package/lib/typescript/module/src/atomUtils.d.ts +58 -0
  226. package/lib/typescript/module/src/atomUtils.d.ts.map +1 -0
  227. package/lib/typescript/module/src/childrenUtils.d.ts +12 -0
  228. package/lib/typescript/module/src/childrenUtils.d.ts.map +1 -0
  229. package/lib/typescript/module/src/commonTypes.d.ts +39 -3
  230. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -1
  231. package/lib/typescript/module/src/index.d.ts +2 -1
  232. package/lib/typescript/module/src/index.d.ts.map +1 -1
  233. package/lib/typescript/module/src/math/environment.d.ts +7 -0
  234. package/lib/typescript/module/src/math/environment.d.ts.map +1 -0
  235. package/lib/typescript/module/src/math/index.d.ts +64 -0
  236. package/lib/typescript/module/src/math/index.d.ts.map +1 -0
  237. package/lib/typescript/module/src/math/mathjax.d.ts +63 -0
  238. package/lib/typescript/module/src/math/mathjax.d.ts.map +1 -0
  239. package/lib/typescript/module/src/metro.d.ts +59 -0
  240. package/lib/typescript/module/src/metro.d.ts.map +1 -0
  241. package/lib/typescript/module/src/parser/index.d.ts +3 -3
  242. package/lib/typescript/module/src/parser/index.d.ts.map +1 -1
  243. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -1
  244. package/lib/typescript/module/src/rangeUtils.d.ts +1 -1
  245. package/lib/typescript/module/src/stripUtils.d.ts +108 -0
  246. package/lib/typescript/module/src/stripUtils.d.ts.map +1 -0
  247. package/lib/typescript/module/src/styleUtils.d.ts +62 -4
  248. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -1
  249. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +32 -4
  250. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -1
  251. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +20 -8
  252. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -1
  253. package/lib/typescript/module/src/web/utils/elementUtils.d.ts +37 -0
  254. package/lib/typescript/module/src/web/utils/elementUtils.d.ts.map +1 -0
  255. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +5 -5
  256. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -1
  257. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +6 -1
  258. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -1
  259. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +2 -2
  260. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -1
  261. package/package.json +29 -2
  262. package/react-native.config.js +8 -1
  263. package/src/MarcusTextDecoratorViewNativeComponent.ts +199 -0
  264. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +145 -58
  265. package/src/MarkdownText.tsx +472 -0
  266. package/src/MarkdownText.web.tsx +668 -0
  267. package/src/MarkdownTextInput.tsx +42 -38
  268. package/src/MarkdownTextInput.web.tsx +36 -13
  269. package/src/atomUtils.ts +112 -0
  270. package/src/childrenUtils.ts +39 -0
  271. package/src/commonTypes.ts +69 -9
  272. package/src/index.tsx +2 -1
  273. package/src/math/environment.ts +68 -0
  274. package/src/math/index.ts +78 -0
  275. package/src/math/mathjax.ts +604 -0
  276. package/src/metro.ts +238 -0
  277. package/src/parser/index.ts +175 -10
  278. package/src/parser/micromark-extension-mentions.ts +104 -11
  279. package/src/stripUtils.ts +919 -0
  280. package/src/styleUtils.ts +290 -26
  281. package/src/web/MarkdownElements.css +67 -0
  282. package/src/web/MarkdownTextInput.css +9 -7
  283. package/src/web/utils/blockLayout.ts +124 -14
  284. package/src/web/utils/blockUtils.ts +365 -59
  285. package/src/web/utils/elementUtils.ts +87 -0
  286. package/src/web/utils/parserUtils.ts +119 -31
  287. package/src/web/utils/treeUtils.ts +6 -1
  288. package/src/web/utils/webStyleUtils.ts +32 -8
  289. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +0 -6
  290. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +0 -6
  291. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +0 -6
  292. package/apple/RCTMarcusStyle+Codegen.mm +0 -64
  293. package/apple/RCTMarcusUtils.h +0 -26
  294. package/apple/RCTMarcusUtils.mm +0 -26
@@ -0,0 +1,919 @@
1
+ import type { MarkdownRange } from "./commonTypes"
2
+
3
+ // Removing the markdown syntax from the string before it reaches the platform
4
+ // is what separates the display component from the input one: the input shows
5
+ // what you typed, the display shows what it meant. Everything downstream --
6
+ // measurement, drawing, selection, VoiceOver -- then sees only the text that is
7
+ // meant to be read, without any of the hiding tricks that a formatter would
8
+ // otherwise have to play on characters that are still there.
9
+ //
10
+ // The parser's offsets are into the original string, so cutting characters out
11
+ // invalidates every range it emitted. Both halves happen here, in one pass, so
12
+ // there is never a moment where a range and the string it indexes disagree.
13
+
14
+ /** What an image stood for, for a caller that has something to show instead. */
15
+ interface MarkdownEmbed {
16
+ /** Where the placeholder character stands in the stripped text. */
17
+ index: number
18
+ uri: string
19
+ alt: string
20
+ /** Empty when the image was written without one, never absent. */
21
+ title: string
22
+ /**
23
+ * Whether the image shares its line with anything else.
24
+ *
25
+ * An image is written inline and is laid out inline either way, but what it
26
+ * is for depends on what is around it: alone on a line it is a figure, and
27
+ * in the middle of a sentence it is a badge or an emoji. Only the caller can
28
+ * pick a size, and this is what that decision is usually made on.
29
+ *
30
+ * A block marker does not count as company -- an image alone in a list item
31
+ * or a quote is still alone -- and another image does.
32
+ */
33
+ inline: boolean
34
+ }
35
+
36
+ /** A maths span, for a caller that has something to draw it with. */
37
+ interface MarkdownMath {
38
+ /** Where the placeholder character stands in the stripped text. */
39
+ index: number
40
+ /**
41
+ * The TeX between the dollars, with the dollars and the padding of `$ x $`
42
+ * taken off and the surrounding whitespace of a `$$` block trimmed. Never
43
+ * empty: a span with nothing in it is dropped rather than reported.
44
+ */
45
+ tex: string
46
+ /**
47
+ * Whether it was written as a `$$` block on lines of its own, which is what
48
+ * decides whether TeX sets it in display style: bigger operators, limits
49
+ * above and below a sum rather than beside it. Where it sits on the page is
50
+ * the block layout's business, not the typesetter's.
51
+ */
52
+ display: boolean
53
+ }
54
+
55
+ /** A link the display can hand to whatever is going to act on a press. */
56
+ interface MarkdownLink {
57
+ /** Where the link's text stands in the stripped text. */
58
+ start: number
59
+ length: number
60
+ uri: string
61
+ /** The text between the brackets, which is all that is left of the link. */
62
+ label: string
63
+ /** Empty when the link was written without one, never absent. */
64
+ title: string
65
+ }
66
+
67
+ interface StrippedMarkdown {
68
+ text: string
69
+ ranges: MarkdownRange[]
70
+ embeds: MarkdownEmbed[]
71
+ maths: MarkdownMath[]
72
+ links: MarkdownLink[]
73
+ }
74
+
75
+ interface StripOptions {
76
+ /**
77
+ * Replace each image with a single placeholder character and report what it
78
+ * stood for, instead of leaving its alt text behind as prose.
79
+ *
80
+ * One character because that is exactly what a view mounted inside text costs
81
+ * the platform: both iOS and Android splice a single U+FFFC into the string
82
+ * the formatters index, in place of the attachment. So the ranges this
83
+ * returns are already right for a `Text` whose children have the embed
84
+ * spliced in where the placeholder is.
85
+ */
86
+ embeds?: boolean
87
+ /**
88
+ * Replace each maths span with a single placeholder character and report the
89
+ * TeX it stood for, instead of leaving `$x^2$` behind as prose.
90
+ *
91
+ * The same one character an embed costs, for the same reason: what draws the
92
+ * maths is a view mounted in the text, and a view in a text is one character
93
+ * to both platforms.
94
+ */
95
+ math?: boolean
96
+ /**
97
+ * Report where each link ended up and where it points.
98
+ *
99
+ * The destination is removed along with the rest of the markup, so a display
100
+ * that means to do something when a link is pressed has to be told here or
101
+ * not at all.
102
+ */
103
+ links?: boolean
104
+ }
105
+
106
+ /**
107
+ * The character standing in for an embed, matching what the platforms use for
108
+ * an attachment so that the stripped text and the string that reaches the
109
+ * formatters are the same length.
110
+ */
111
+ const OBJECT_REPLACEMENT = "\uFFFC"
112
+
113
+ /** Shared so that the common case of a document with no embeds allocates nothing. */
114
+ const NO_EMBEDS: MarkdownEmbed[] = []
115
+
116
+ /** Shared for the same reason. */
117
+ const NO_MATHS: MarkdownMath[] = []
118
+
119
+ /** Shared for the same reason. */
120
+ const NO_LINKS: MarkdownLink[] = []
121
+
122
+ const TAB = 9
123
+ const LINE_FEED = 10
124
+ const CARRIAGE_RETURN = 13
125
+ const SPACE = 32
126
+ const GREATER_THAN = 62
127
+ const LEFT_PAREN = 40
128
+ const RIGHT_PAREN = 41
129
+
130
+ function isBlank(code: number) {
131
+ return code === SPACE || code === TAB
132
+ }
133
+
134
+ function isLineBreak(code: number) {
135
+ return code === LINE_FEED || code === CARRIAGE_RETURN
136
+ }
137
+
138
+ /**
139
+ * Spans of the block markers that survive stripping, in original offsets.
140
+ *
141
+ * A list's marker is the only syntax that carries meaning of its own -- `1.`
142
+ * numbers the item and `-` stands in for a bullet -- so it stays in the string
143
+ * and is laid out the way the input lays it out. A blockquote's `>` says
144
+ * nothing the ribbon beside it does not already say, so it goes.
145
+ *
146
+ * `flushLine` emits a `block-prefix` immediately before the container that owns
147
+ * it, which is the same pairing the native formatters read, so the container is
148
+ * simply the range that follows.
149
+ */
150
+ function listMarkers(ranges: MarkdownRange[]) {
151
+ const markers: { start: number; end: number }[] = []
152
+
153
+ for (let i = 0; i < ranges.length; i++) {
154
+ const range = ranges[i]!
155
+
156
+ if (range.type !== "block-prefix") {
157
+ continue
158
+ }
159
+
160
+ const container = ranges[i + 1]
161
+
162
+ if (container && (container.type === "list-ordered" || container.type === "list-unordered")) {
163
+ markers.push({ start: range.start, end: range.start + range.length })
164
+ }
165
+ }
166
+
167
+ return markers
168
+ }
169
+
170
+ /**
171
+ * Whether a syntax range is a quote marker sitting inside a list's prefix.
172
+ *
173
+ * A prefix is measured from wherever the previous container's marker ended, so
174
+ * the ordered list in `- > 1. ` is handed the whole run `> 1. ` -- quote marker
175
+ * included. Containment alone would therefore keep a `>` that belongs to
176
+ * something else, and `>` is the only block marker that can appear there.
177
+ */
178
+ function isQuoteMarker(text: string, start: number, end: number) {
179
+ for (let i = start; i < end; i++) {
180
+ if (text.charCodeAt(i) !== GREATER_THAN) {
181
+ return false
182
+ }
183
+ }
184
+
185
+ return true
186
+ }
187
+
188
+ /** The `( ... )` an inline link or image carries, and what it carries in it. */
189
+ interface Resource {
190
+ open: number
191
+ close: number
192
+ /**
193
+ * What was written between the markers. Both are strings whether or not the
194
+ * markdown carried them, so that everything downstream -- a press handler
195
+ * most of all -- is handed the same shape every time.
196
+ */
197
+ uri: string
198
+ title: string
199
+ }
200
+
201
+ /**
202
+ * The resources in `text`, in the order they are written.
203
+ *
204
+ * The parser reports the brackets and parentheses as syntax but the destination
205
+ * itself as a `link`, which is right for an input -- the URL is text you can
206
+ * edit -- and wrong for a display, where cutting only the punctuation out of
207
+ * `[text](url)` leaves `texturl`. So the whole run between the markers goes,
208
+ * and reading it on the way past is what leaves something to hand a press
209
+ * handler afterwards.
210
+ *
211
+ * `(` is emitted as syntax only by `resourceMarker`, so there is nothing else
212
+ * this can match, and one resource cannot hold another: the inner parentheses
213
+ * of `[a](b(c))` are part of the destination rather than markers of their own.
214
+ * Reference links and their definitions are not covered: their label is
215
+ * punctuation around prose the way emphasis is, and telling `[text]` a
216
+ * reference from `[text]` a label needs more than the ranges carry.
217
+ */
218
+ function resourceSpans(text: string, ranges: MarkdownRange[]) {
219
+ const spans: Resource[] = []
220
+ let current: Resource | null = null
221
+
222
+ for (const range of ranges) {
223
+ if (range.type === "syntax" && range.length === 1) {
224
+ const code = text.charCodeAt(range.start)
225
+
226
+ if (code === LEFT_PAREN) {
227
+ current = { open: range.start, close: -1, uri: "", title: "" }
228
+ } else if (code === RIGHT_PAREN && current !== null) {
229
+ current.close = range.start
230
+ spans.push(current)
231
+ current = null
232
+ }
233
+
234
+ continue
235
+ }
236
+
237
+ // A destination and a title are emitted between the two markers, so
238
+ // whichever resource is open is the one they belong to. Anything else that
239
+ // arrives in between -- a container flushed at a line break inside a title
240
+ // -- is neither and is passed over.
241
+ if (current === null) {
242
+ continue
243
+ }
244
+
245
+ if (range.type === "link") {
246
+ current.uri = text.slice(range.start, range.start + range.length)
247
+ } else if (range.type === "title") {
248
+ current.title = text.slice(range.start, range.start + range.length)
249
+ }
250
+ }
251
+
252
+ return spans
253
+ }
254
+
255
+ /** Marks every resource for removal, markers and all. */
256
+ function cutResources(resources: Resource[], cut: Uint8Array) {
257
+ let cutSomething = false
258
+
259
+ for (const resource of resources) {
260
+ for (let i = resource.open; i <= resource.close; i++) {
261
+ cut[i] = 1
262
+ cutSomething = true
263
+ }
264
+ }
265
+
266
+ return cutSomething
267
+ }
268
+
269
+ /**
270
+ * Whether anything but block markers shares a line with the character at
271
+ * `index`.
272
+ *
273
+ * Read off the stripped text, which is the text that will be laid out: by this
274
+ * point a quote's marker is gone entirely and a list's is a `block-prefix`
275
+ * range over characters that were kept to be laid out rather than read.
276
+ */
277
+ function sharesItsLine(text: string, prefixes: MarkdownRange[], index: number) {
278
+ let start = index
279
+
280
+ while (start > 0 && !isLineBreak(text.charCodeAt(start - 1))) {
281
+ start--
282
+ }
283
+
284
+ let end = index
285
+
286
+ while (end < text.length && !isLineBreak(text.charCodeAt(end))) {
287
+ end++
288
+ }
289
+
290
+ for (let i = start; i < end; i++) {
291
+ if (i === index || isBlank(text.charCodeAt(i))) {
292
+ continue
293
+ }
294
+
295
+ if (prefixes.some((prefix) => i >= prefix.start && i < prefix.start + prefix.length)) {
296
+ continue
297
+ }
298
+
299
+ return true
300
+ }
301
+
302
+ return false
303
+ }
304
+
305
+ /** The characters of a span that survive the cut, which is CommonMark's `alt`. */
306
+ function flatten(text: string, cut: Uint8Array, start: number, end: number) {
307
+ let flattened = ""
308
+ let segmentStart = start
309
+
310
+ for (let i = start; i < end; i++) {
311
+ if (cut[i] === 1) {
312
+ if (segmentStart < i) {
313
+ flattened += text.slice(segmentStart, i)
314
+ }
315
+ segmentStart = i + 1
316
+ }
317
+ }
318
+
319
+ return segmentStart < end ? flattened + text.slice(segmentStart, end) : flattened
320
+ }
321
+
322
+ interface ImageSpan {
323
+ start: number
324
+ end: number
325
+ uri: string
326
+ alt: string
327
+ title: string
328
+ }
329
+
330
+ /** Shared for the same reason `NO_EMBEDS` is. */
331
+ const NO_IMAGES: ImageSpan[] = []
332
+
333
+ /**
334
+ * The images in `text`, with the three things a renderer needs to show one.
335
+ *
336
+ * Read straight off the ranges, because the parser has already made an image
337
+ * flat: inside a label it emits nothing but the `syntax` the markup is spelled
338
+ * with and one `alt` range over the lot. So there is exactly one `link`
339
+ * range in a span -- the destination -- and no image nested in another, and
340
+ * neither case needs guarding here.
341
+ *
342
+ * `alt` still has to be flattened, since a single range cannot skip the `**` in
343
+ * `![**a**](b)`. `cut` is what does it: it already marks every character the
344
+ * strip is removing, so reading the label through it gives exactly the string
345
+ * CommonMark would have put in the attribute.
346
+ */
347
+ function imageSpans(text: string, ranges: MarkdownRange[], cut: Uint8Array) {
348
+ const spans: ImageSpan[] = []
349
+
350
+ for (let i = 0; i < ranges.length; i++) {
351
+ const image = ranges[i]!
352
+
353
+ if (image.type !== "image") {
354
+ continue
355
+ }
356
+
357
+ const start = image.start
358
+ const end = Math.min(start + image.length, text.length)
359
+ const span: ImageSpan = { start, end, uri: "", alt: "", title: "" }
360
+
361
+ for (let j = i + 1; j < ranges.length; j++) {
362
+ const inner = ranges[j]!
363
+ const innerEnd = inner.start + inner.length
364
+
365
+ // Ranges are not sorted -- a container is emitted after the content of
366
+ // the line it encloses -- so anything reaching past the image is one of
367
+ // those and is skipped rather than ending the walk.
368
+ if (inner.start >= end) {
369
+ break
370
+ }
371
+ if (innerEnd > end) {
372
+ continue
373
+ }
374
+
375
+ if (inner.type === "alt") {
376
+ span.alt = flatten(text, cut, inner.start, innerEnd)
377
+ } else if (inner.type === "link") {
378
+ span.uri = text.slice(inner.start, innerEnd)
379
+ } else if (inner.type === "title") {
380
+ span.title = text.slice(inner.start, innerEnd)
381
+ }
382
+ }
383
+
384
+ spans.push(span)
385
+ }
386
+
387
+ return spans
388
+ }
389
+
390
+ /**
391
+ * The bounds of every maths span, whether or not the caller means to render one.
392
+ *
393
+ * A display with nothing to draw maths with keeps `$x^2$` exactly as it was
394
+ * written: cutting only the dollars would leave `x^2`, which is neither the
395
+ * markdown nor the maths, and TeX read as prose is worse than TeX read as TeX.
396
+ * So the delimiters inside these bounds are spared the syntax cut.
397
+ */
398
+ function mathBounds(ranges: MarkdownRange[]) {
399
+ const bounds: { start: number; end: number }[] = []
400
+
401
+ for (const range of ranges) {
402
+ if (range.type === "math" || range.type === "math-block") {
403
+ bounds.push({ start: range.start, end: range.start + range.length })
404
+ }
405
+ }
406
+
407
+ return bounds
408
+ }
409
+
410
+ /** A maths span as the strip sees it, before the offsets move. */
411
+ interface MathSpan {
412
+ start: number
413
+ end: number
414
+ tex: string
415
+ display: boolean
416
+ }
417
+
418
+ /** Shared for the same reason `NO_EMBEDS` is. */
419
+ const NO_MATH_SPANS: MathSpan[] = []
420
+
421
+ /**
422
+ * The maths spans in `text`, with the TeX each one carries.
423
+ *
424
+ * Nothing has to be walked for the content: the parser makes a maths span
425
+ * opaque -- one range over the lot and `syntax` over the delimiters, and
426
+ * nothing at all for what is between them -- so the TeX is simply the
427
+ * characters the strip is not already removing. `cut` is what says which those
428
+ * are, which is why this runs at the same moment `imageSpans` does: after the
429
+ * syntax has been marked and before the spans themselves are.
430
+ *
431
+ * A `$$` block is trimmed because its delimiters sit on lines of their own, so
432
+ * what survives between them opens and closes with a line break.
433
+ */
434
+ function mathSpans(text: string, ranges: MarkdownRange[], cut: Uint8Array) {
435
+ const spans: MathSpan[] = []
436
+
437
+ for (const range of ranges) {
438
+ if (range.type !== "math" && range.type !== "math-block") {
439
+ continue
440
+ }
441
+
442
+ const start = range.start
443
+ const end = Math.min(start + range.length, text.length)
444
+ const tex = flatten(text, cut, start, end).trim()
445
+
446
+ // `$$` with nothing in it is punctuation, not maths. Left as it was
447
+ // written rather than turned into a placeholder standing for nothing.
448
+ if (tex === "") {
449
+ continue
450
+ }
451
+
452
+ spans.push({ start, end, tex, display: range.type === "math-block" })
453
+ }
454
+
455
+ return spans
456
+ }
457
+
458
+ interface LinkSpan {
459
+ start: number
460
+ end: number
461
+ uri: string
462
+ title: string
463
+ }
464
+
465
+ /** Shared for the same reason `NO_EMBEDS` is. */
466
+ const NO_LINK_SPANS: LinkSpan[] = []
467
+
468
+ /**
469
+ * The links in `text`, as the text that will be left of each and where it
470
+ * points.
471
+ *
472
+ * A written link is a label followed immediately by a resource, so the label is
473
+ * the one that opens at `](`. A label with no resource after it is a reference
474
+ * or a shortcut, and nothing here can resolve one: it is still styled as a link
475
+ * -- that much the ranges say -- but there is no destination to press it to.
476
+ *
477
+ * An autolink has no label at all and is its own text, so any `link` left over
478
+ * is one. Left over means outside every resource: the destinations are `link`
479
+ * ranges too, and each has already been taken by the label in front of it or
480
+ * belongs to an image, which has no label to press. The exception is a
481
+ * definition's destination -- `[ref]: https://example.com` -- which is written
482
+ * plainly, is shown as written, and is treated as what it looks like.
483
+ */
484
+ function linkSpans(text: string, ranges: MarkdownRange[], resources: Resource[]) {
485
+ const spans: LinkSpan[] = []
486
+
487
+ for (const range of ranges) {
488
+ const end = range.start + range.length
489
+ let span: LinkSpan | null = null
490
+
491
+ if (range.type === "label") {
492
+ const resource = resources.find((candidate) => candidate.open === end + 1)
493
+
494
+ if (resource !== undefined) {
495
+ span = { start: range.start, end, uri: resource.uri, title: resource.title }
496
+ }
497
+ } else if (
498
+ range.type === "link" &&
499
+ !resources.some((resource) => range.start > resource.open && range.start < resource.close)
500
+ ) {
501
+ span = { start: range.start, end, uri: text.slice(range.start, end), title: "" }
502
+ }
503
+
504
+ if (span === null) {
505
+ continue
506
+ }
507
+
508
+ // A link cannot hold another one, but micromark still reports the construct
509
+ // it finds inside a label: `[see <https://x.com>](y)` arrives as a label
510
+ // with an autolink in it. A label is emitted before its own content, so the
511
+ // one that encloses the other is the one already here -- and it is the one
512
+ // to keep, being the whole of what the reader is shown. Two of them would
513
+ // be worse than a wrong choice: the caller splices the text at these, and
514
+ // spans that overlap would splice the run they share in twice.
515
+ if (spans.length > 0 && span.start < spans[spans.length - 1]!.end) {
516
+ continue
517
+ }
518
+
519
+ spans.push(span)
520
+ }
521
+
522
+ return spans
523
+ }
524
+
525
+ /**
526
+ * The spans as offsets into the stripped text, carrying the label they now
527
+ * cover. `offsets` is null when nothing was cut and the two texts are the same.
528
+ */
529
+ function toLinks(text: string, spans: LinkSpan[], offsets: Int32Array | null): MarkdownLink[] {
530
+ if (spans.length === 0) {
531
+ return NO_LINKS
532
+ }
533
+
534
+ const links: MarkdownLink[] = []
535
+
536
+ for (const span of spans) {
537
+ const start = offsets === null ? span.start : offsets[span.start]!
538
+ const end = offsets === null ? span.end : offsets[span.end]!
539
+
540
+ // A label that was nothing but markup -- `[**](x)` -- has nothing left to
541
+ // press.
542
+ if (end <= start) {
543
+ continue
544
+ }
545
+
546
+ links.push({
547
+ start,
548
+ length: end - start,
549
+ uri: span.uri,
550
+ label: text.slice(start, end),
551
+ title: span.title,
552
+ })
553
+ }
554
+
555
+ return links
556
+ }
557
+
558
+ /**
559
+ * A span that collapses to a single placeholder character, of either kind.
560
+ *
561
+ * Images and maths are the same problem -- a run of markdown that the display
562
+ * does not read but draws, and that costs the platform exactly one character --
563
+ * so they go through one set of machinery. Sorting by offset is enough to put
564
+ * them back in emission order: both are emitted from the parser as they are
565
+ * met, and neither can contain the other.
566
+ */
567
+ interface Atom {
568
+ start: number
569
+ end: number
570
+ }
571
+
572
+ /** Shared for the same reason `NO_EMBEDS` is. */
573
+ const NO_ATOMS: Atom[] = []
574
+
575
+ function atomsOf(images: ImageSpan[], maths: MathSpan[]): Atom[] {
576
+ if (images.length === 0) {
577
+ return maths.length === 0 ? NO_ATOMS : maths
578
+ }
579
+
580
+ if (maths.length === 0) {
581
+ return images
582
+ }
583
+
584
+ return [...images, ...maths].sort((a, b) => a.start - b.start)
585
+ }
586
+
587
+ /** Whether a range is one that stands for an atom rather than describing text. */
588
+ function isAtomType(type: MarkdownRange["type"]) {
589
+ return type === "image" || type === "math" || type === "math-block"
590
+ }
591
+
592
+ /**
593
+ * Widens the cuts already marked on a line to cover the whitespace that only
594
+ * existed to separate a marker from what it marked.
595
+ *
596
+ * A line opens with a run of block markers and the blanks between them. Walking
597
+ * that run, a blank goes only if the marker in front of it went: `# Title` and
598
+ * `> - item` lose their spacing, `- item` keeps the one its bullet needs.
599
+ *
600
+ * A line that turns out to be nothing but removed markers takes its own line
601
+ * break with it, so a setext underline or a thematic break does not leave a
602
+ * blank line behind.
603
+ */
604
+ function absorbMarkerSpacing(text: string, cut: Uint8Array, marker: Uint8Array) {
605
+ let lineStart = 0
606
+
607
+ while (lineStart <= text.length) {
608
+ let lineEnd = lineStart
609
+
610
+ while (lineEnd < text.length) {
611
+ const code = text.charCodeAt(lineEnd)
612
+
613
+ if (code === LINE_FEED || code === CARRIAGE_RETURN) {
614
+ break
615
+ }
616
+
617
+ lineEnd++
618
+ }
619
+
620
+ let cursor = lineStart
621
+ let cutSomething = false
622
+ let keptMarker = false
623
+ let afterCut = false
624
+
625
+ while (cursor < lineEnd) {
626
+ const blank = isBlank(text.charCodeAt(cursor))
627
+
628
+ if (cut[cursor] === 1) {
629
+ cutSomething = true
630
+ afterCut = true
631
+ } else if (blank) {
632
+ if (afterCut) {
633
+ cut[cursor] = 1
634
+ }
635
+ } else if (marker[cursor] === 1) {
636
+ keptMarker = true
637
+ afterCut = false
638
+ } else {
639
+ break
640
+ }
641
+
642
+ cursor++
643
+ }
644
+
645
+ // Nothing readable left on the line, and no marker worth keeping either.
646
+ if (cursor >= lineEnd && cutSomething && !keptMarker) {
647
+ for (let i = lineStart; i < lineEnd; i++) {
648
+ cut[i] = 1
649
+ }
650
+
651
+ // The break in front, for any line but the first, which has none and
652
+ // takes the one behind instead.
653
+ if (lineStart > 0) {
654
+ let index = lineStart - 1
655
+
656
+ if (text.charCodeAt(index) === LINE_FEED) {
657
+ cut[index] = 1
658
+ index--
659
+ }
660
+ if (index >= 0 && text.charCodeAt(index) === CARRIAGE_RETURN) {
661
+ cut[index] = 1
662
+ }
663
+ } else {
664
+ let index = lineEnd
665
+
666
+ if (text.charCodeAt(index) === CARRIAGE_RETURN) {
667
+ cut[index] = 1
668
+ index++
669
+ }
670
+ if (text.charCodeAt(index) === LINE_FEED) {
671
+ cut[index] = 1
672
+ }
673
+ }
674
+ }
675
+
676
+ if (lineEnd >= text.length) {
677
+ break
678
+ }
679
+
680
+ lineStart = lineEnd
681
+
682
+ if (text.charCodeAt(lineStart) === CARRIAGE_RETURN) {
683
+ lineStart++
684
+ }
685
+ if (text.charCodeAt(lineStart) === LINE_FEED) {
686
+ lineStart++
687
+ }
688
+ }
689
+ }
690
+
691
+ /**
692
+ * What a type means once the markup around it is gone.
693
+ *
694
+ * A link's label is the one type that changes. While it is being edited it is
695
+ * text between two brackets and is left alone; once the brackets have been
696
+ * removed it is the only thing left standing for the link, so it takes the
697
+ * link's styling -- the destination it was pointing at having been removed too.
698
+ */
699
+ function displayType(type: MarkdownRange["type"]): MarkdownRange["type"] {
700
+ return type === "label" ? "link" : type
701
+ }
702
+
703
+ /**
704
+ * Removes the markdown syntax from `text` and rewrites `ranges` to index what
705
+ * is left.
706
+ *
707
+ * Ranges are only ever dropped or shortened, never reordered, so the emission
708
+ * order the native formatters depend on survives -- a `block-prefix` still
709
+ * arrives immediately before its container, and a container that lost its
710
+ * marker entirely arrives with none, which is exactly the gutter-without-marker
711
+ * case the block layout already handles.
712
+ */
713
+ function stripSyntax(
714
+ text: string,
715
+ ranges: MarkdownRange[],
716
+ options: StripOptions = {},
717
+ ): StrippedMarkdown {
718
+ if (text.length === 0 || ranges.length === 0) {
719
+ return { text, ranges, embeds: NO_EMBEDS, maths: NO_MATHS, links: NO_LINKS }
720
+ }
721
+
722
+ const markers = listMarkers(ranges)
723
+ // Only when there is nothing to draw maths with: otherwise the spans are cut
724
+ // whole a few lines down and sparing their dollars would be pointless work.
725
+ const spared = options.math ? [] : mathBounds(ranges)
726
+ const cut = new Uint8Array(text.length)
727
+ const kept = new Uint8Array(text.length)
728
+ let cutSomething = false
729
+
730
+ for (const range of ranges) {
731
+ // A fenced block's language is metadata about the block, not part of it, so
732
+ // it goes the same way the fence itself does.
733
+ if (range.type !== "syntax" && range.type !== "language") {
734
+ continue
735
+ }
736
+
737
+ const start = range.start
738
+ const end = Math.min(start + range.length, text.length)
739
+
740
+ if (spared.some((span) => start >= span.start && end <= span.end)) {
741
+ continue
742
+ }
743
+ const inListMarker =
744
+ range.type === "syntax" &&
745
+ markers.some((marker) => start >= marker.start && end <= marker.end) &&
746
+ !isQuoteMarker(text, start, end)
747
+
748
+ for (let i = start; i < end; i++) {
749
+ if (inListMarker) {
750
+ kept[i] = 1
751
+ } else {
752
+ cut[i] = 1
753
+ cutSomething = true
754
+ }
755
+ }
756
+ }
757
+
758
+ const resources = resourceSpans(text, ranges)
759
+ const links = options.links ? linkSpans(text, ranges, resources) : NO_LINK_SPANS
760
+
761
+ cutSomething = cutResources(resources, cut) || cutSomething
762
+
763
+ // An image goes whole -- markup, alt text and destination alike -- and leaves
764
+ // one character behind. Keeping one of its own characters rather than
765
+ // splicing a new one in is what lets the offsets below stay as they are: a
766
+ // range is still only ever shortened, never moved.
767
+ // After the cuts above and before the images are cut themselves, which is the
768
+ // one moment `cut` describes the markup inside a label without describing the
769
+ // label's own removal -- exactly what flattening the alt text needs.
770
+ const images = options.embeds ? imageSpans(text, ranges, cut) : NO_IMAGES
771
+ const maths = options.math ? mathSpans(text, ranges, cut) : NO_MATH_SPANS
772
+ const atoms = atomsOf(images, maths)
773
+ const placeholder = atoms.length > 0 ? new Uint8Array(text.length) : null
774
+
775
+ for (const atom of atoms) {
776
+ for (let i = atom.start; i < atom.end; i++) {
777
+ cut[i] = 1
778
+ // 1 for a character the atom swallowed, 2 for the one it left behind.
779
+ placeholder![i] = 1
780
+ }
781
+
782
+ cut[atom.start] = 0
783
+ placeholder![atom.start] = 2
784
+ cutSomething = true
785
+ }
786
+
787
+ if (!cutSomething) {
788
+ // Nothing moved, so the spans already index the text as it stands. A bare
789
+ // URL gets here: it is a link written with no syntax at all.
790
+ return { text, ranges, embeds: NO_EMBEDS, maths: NO_MATHS, links: toLinks(text, links, null) }
791
+ }
792
+
793
+ absorbMarkerSpacing(text, cut, kept)
794
+
795
+ // Kept characters before each offset, which is where that offset lands once
796
+ // the cuts are gone. One extra entry so a range's end can be mapped the same
797
+ // way as its start.
798
+ const offsets = new Int32Array(text.length + 1)
799
+ let stripped = ""
800
+ let segmentStart = 0
801
+ let survivors = 0
802
+
803
+ for (let i = 0; i < text.length; i++) {
804
+ offsets[i] = survivors
805
+
806
+ if (placeholder !== null && placeholder[i] === 2) {
807
+ if (segmentStart < i) {
808
+ stripped += text.slice(segmentStart, i)
809
+ }
810
+ stripped += OBJECT_REPLACEMENT
811
+ segmentStart = i + 1
812
+ survivors++
813
+ } else if (cut[i] === 1) {
814
+ if (segmentStart < i) {
815
+ stripped += text.slice(segmentStart, i)
816
+ }
817
+ segmentStart = i + 1
818
+ } else {
819
+ survivors++
820
+ }
821
+ }
822
+
823
+ offsets[text.length] = survivors
824
+
825
+ if (segmentStart < text.length) {
826
+ stripped += text.slice(segmentStart)
827
+ }
828
+
829
+ const remapped: MarkdownRange[] = []
830
+ // The atom being walked past, if any: everything it swallowed goes with it,
831
+ // save its own range, which shrinks onto the character left behind. Without
832
+ // this the `syntax` over the `!` of an image or the opening `$` of a maths
833
+ // span -- the very character kept as the placeholder -- would survive to
834
+ // style what stands there as markup.
835
+ //
836
+ // Swallowed means emitted after the atom opened and ending inside it, which
837
+ // is what tells the alt text apart from the two kinds of range that reach
838
+ // over an atom and have to survive: the label of a link written around an
839
+ // image, which is emitted before it, and the container of the line it sits
840
+ // on, which is emitted after and runs past its end.
841
+ let atom = 0
842
+ let insideStart = -1
843
+ let insideEnd = -1
844
+
845
+ for (const range of ranges) {
846
+ // One span per atom range, in the order they were emitted, so the two walks
847
+ // stay in step. Matched on the offset as well as the type, because a kind
848
+ // of atom the caller did not ask for is not one: an `image` range with no
849
+ // `renderEmbed` behind it is still just a range.
850
+ if (atom < atoms.length && isAtomType(range.type) && range.start === atoms[atom]!.start) {
851
+ insideStart = atoms[atom]!.start
852
+ insideEnd = atoms[atom]!.end
853
+ atom++
854
+ } else if (range.start >= insideStart && range.start + range.length <= insideEnd) {
855
+ continue
856
+ }
857
+
858
+ let start = offsets[Math.min(range.start, text.length)]!
859
+ const end = offsets[Math.min(range.start + range.length, text.length)]!
860
+
861
+ // A fenced block opens with a line of its own, and cutting that line leaves
862
+ // the break that ended it at the front of the block. What is left then
863
+ // starts on the line above the code, which is a blank first line in
864
+ // whatever draws the block as a box.
865
+ if (range.type === "codeblock") {
866
+ while (start < end && isLineBreak(stripped.charCodeAt(start))) {
867
+ start++
868
+ }
869
+ }
870
+
871
+ if (end <= start) {
872
+ continue
873
+ }
874
+
875
+ const next: MarkdownRange = { type: displayType(range.type), start, length: end - start }
876
+
877
+ if (range.depth !== undefined) {
878
+ next.depth = range.depth
879
+ }
880
+
881
+ remapped.push(next)
882
+ }
883
+
884
+ const prefixes = atoms.length > 0 ? remapped.filter((range) => range.type === "block-prefix") : []
885
+ const embeds =
886
+ images.length > 0
887
+ ? images.map((image) => {
888
+ const index = offsets[image.start]!
889
+
890
+ return {
891
+ index,
892
+ uri: image.uri,
893
+ alt: image.alt,
894
+ title: image.title,
895
+ inline: sharesItsLine(stripped, prefixes, index),
896
+ }
897
+ })
898
+ : NO_EMBEDS
899
+ const mathsOut =
900
+ maths.length > 0
901
+ ? maths.map((span) => {
902
+ const index = offsets[span.start]!
903
+
904
+ return { index, tex: span.tex, display: span.display }
905
+ })
906
+ : NO_MATHS
907
+
908
+ return {
909
+ text: stripped,
910
+ ranges: remapped,
911
+ embeds,
912
+ maths: mathsOut,
913
+ links: toLinks(stripped, links, offsets),
914
+ }
915
+ }
916
+
917
+ export type { MarkdownEmbed, MarkdownMath, MarkdownLink, StrippedMarkdown, StripOptions }
918
+
919
+ export { OBJECT_REPLACEMENT, stripSyntax }