react-native-marcus 0.1.1 → 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
package/src/metro.ts ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Metro configuration for drawing maths.
3
+ *
4
+ * Two things stand between a Metro bundle and a working `MarkdownText` with
5
+ * `math` on, and `withMath` is both of them:
6
+ *
7
+ * 1. **The maths backend is swapped in.** `src/math/index.ts` is a stub that
8
+ * draws nothing, and nothing in the library ever imports the real one --
9
+ * otherwise every application that installed this package would carry
10
+ * MathJax, whether or not it has an equation in it. This resolver points
11
+ * that import at `src/math/mathjax.ts` instead, which is what opting in
12
+ * means.
13
+ * 2. **MathJax's own imports resolve.** It addresses its modules through
14
+ * Node's *subpath imports* -- the `imports` field in its `package.json`,
15
+ * which maps `#js/*`, `#default-font/*` and four others onto real files.
16
+ * Metro does not implement that field, so every internal import fails and
17
+ * the bundle never builds. There is nothing to fix in MathJax: the field is
18
+ * standard and Metro has not caught up. The mapping is read out of MathJax's
19
+ * own manifest rather than written down here, because a list copied into a
20
+ * second place is a list that goes stale.
21
+ *
22
+ * const { getDefaultConfig } = require("expo/metro-config")
23
+ * const { withMath } = require("react-native-marcus/metro")
24
+ *
25
+ * module.exports = withMath(getDefaultConfig(__dirname))
26
+ *
27
+ * It does one thing more, which is to *remove* rather than add: the speech
28
+ * engine's rules for every language but the ones named in `{ locales }` are
29
+ * resolved away to nothing, so a display that reads its maths in English does
30
+ * not carry the twelve other languages it will never speak.
31
+ */
32
+ import { dirname } from "path"
33
+
34
+ /** As much of a Metro config as this touches. */
35
+ interface MetroConfig {
36
+ resolver?: {
37
+ resolveRequest?: ResolveRequest
38
+ [key: string]: unknown
39
+ }
40
+ [key: string]: unknown
41
+ }
42
+
43
+ type Resolution = { type?: string; filePath?: string } | null | undefined
44
+
45
+ type ResolveRequest = (
46
+ context: unknown,
47
+ moduleName: string,
48
+ platform: string | null,
49
+ ) => Resolution
50
+
51
+ interface ResolutionContext {
52
+ resolveRequest: ResolveRequest
53
+ }
54
+
55
+ interface WithMathOptions {
56
+ /**
57
+ * The package whose `imports` field is read, in case a fork or a pinned copy
58
+ * is installed under another name.
59
+ *
60
+ * @default "@mathjax/src"
61
+ */
62
+ packageName?: string
63
+ /**
64
+ * This library, in case it is installed under another name. Its own directory
65
+ * is what bounds the backend swap below.
66
+ *
67
+ * @default "react-native-marcus"
68
+ */
69
+ libraryName?: string
70
+ /**
71
+ * The languages maths can be read out in, as the speech engine names them:
72
+ * `af`, `ca`, `da`, `de`, `en`, `es`, `fr`, `hi`, `it`, `ko`, `nb`, `nn` and
73
+ * `sv`. Everything else is left out of the bundle.
74
+ *
75
+ * This is what `MarkdownText`'s `mathAccessibilityLocale` can choose between.
76
+ * It is a build-time list rather than a runtime one for the same reason
77
+ * MathJax itself is: the rules for one language are around 300KB, and an
78
+ * application that reads its maths in English has no business carrying the
79
+ * four megabytes of rules for the twelve languages it does not. Naming the
80
+ * ones you support is the same thing you already do for your own
81
+ * translations.
82
+ *
83
+ * A locale asked for and not bundled is read in English instead, with a
84
+ * warning in the console saying so.
85
+ *
86
+ * @default ["en"]
87
+ */
88
+ locales?: string[]
89
+ }
90
+
91
+ /**
92
+ * MathJax's `#…` specifiers, as prefix pairs.
93
+ *
94
+ * Its manifest maps `"#js/*": "@mathjax/src/mjs/*"` and so on, and every entry
95
+ * is a single trailing wildcard, so a prefix swap is the whole of it. Anything
96
+ * shaped differently is skipped rather than guessed at.
97
+ */
98
+ function importPrefixes(packageName: string): [string, string][] {
99
+ let manifest: { imports?: Record<string, unknown> }
100
+
101
+ try {
102
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
103
+ manifest = require(`${packageName}/package.json`) as { imports?: Record<string, unknown> }
104
+ } catch {
105
+ throw new Error(
106
+ `[react-native-marcus] withMath() could not read "${packageName}/package.json". ` +
107
+ `Install it (npm install ${packageName}) or drop withMath() from your Metro config.`,
108
+ )
109
+ }
110
+
111
+ const prefixes: [string, string][] = []
112
+
113
+ for (const [specifier, target] of Object.entries(manifest.imports ?? {})) {
114
+ if (
115
+ typeof target === "string" &&
116
+ specifier.endsWith("/*") &&
117
+ target.endsWith("/*") &&
118
+ specifier.startsWith("#")
119
+ ) {
120
+ prefixes.push([specifier.slice(0, -1), target.slice(0, -1)])
121
+ }
122
+ }
123
+
124
+ return prefixes
125
+ }
126
+
127
+ /**
128
+ * The stub backend, whichever build of the library Metro picked: `lib/module`,
129
+ * `lib/commonjs`, or `src` itself when the library is being worked on.
130
+ */
131
+ const STUB = /([\\/])math([\\/])index\.(js|jsx|ts|tsx|mjs|cjs)$/
132
+
133
+ /**
134
+ * One of the speech engine's rule tables, by the locale it holds.
135
+ *
136
+ * They are all named in `math/mathjax.ts` -- a `require` has to be written down
137
+ * for a bundler to find it -- and all but the ones the application asked for
138
+ * are resolved away here.
139
+ */
140
+ const MATHMAPS = /[\\/]mathmaps[\\/]([a-z]+)\.json$/
141
+
142
+ /** The table every locale shares, which is never the one to leave out. */
143
+ const BASE_LOCALE = "base"
144
+
145
+ /** Where this library lives, so that nothing else's `math/index.js` is touched. */
146
+ function libraryRoot(libraryName: string): string {
147
+ try {
148
+ return dirname(require.resolve(`${libraryName}/package.json`))
149
+ } catch {
150
+ throw new Error(
151
+ `[react-native-marcus] withMath() could not locate "${libraryName}". ` +
152
+ `Pass { libraryName } if it is installed under another name.`,
153
+ )
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Where the speech engine lives, when it is installed at all.
159
+ *
160
+ * It bounds the locale trimming the way the library root bounds the backend
161
+ * swap: only files under this directory are considered rule tables. Missing is
162
+ * not an error -- an application can draw maths without ever reading it out --
163
+ * and then there is nothing to trim.
164
+ */
165
+ function speechRoot(): string | null {
166
+ try {
167
+ return dirname(require.resolve("speech-rule-engine/package.json"))
168
+ } catch {
169
+ return null
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Teaches Metro to resolve MathJax, and points the library's maths backend at
175
+ * it.
176
+ *
177
+ * Any `resolveRequest` already on the config is kept and called for everything
178
+ * else, so this composes with whatever else the application is doing.
179
+ */
180
+ function withMath(config: MetroConfig, options: WithMathOptions = {}): MetroConfig {
181
+ const prefixes = importPrefixes(options.packageName ?? "@mathjax/src")
182
+ const root = libraryRoot(options.libraryName ?? "react-native-marcus")
183
+ const speech = speechRoot()
184
+ const locales = new Set(options.locales ?? ["en"])
185
+ const existing = config.resolver?.resolveRequest
186
+
187
+ const resolveRequest: ResolveRequest = (context, moduleName, platform) => {
188
+ let name = moduleName
189
+
190
+ if (name.charCodeAt(0) === 35 /* # */) {
191
+ for (const [from, to] of prefixes) {
192
+ if (name.startsWith(from)) {
193
+ name = to + name.slice(from.length)
194
+ break
195
+ }
196
+ }
197
+ }
198
+
199
+ const resolution = existing
200
+ ? existing(context, name, platform)
201
+ : (context as ResolutionContext).resolveRequest(context, name, platform)
202
+
203
+ // Matched on what it resolved to rather than on what was asked for: the
204
+ // import is written `"./math"`, relative to whichever of the three builds
205
+ // of this library the application ended up with, and a relative specifier
206
+ // says nothing about which package it is in.
207
+ const filePath = resolution?.filePath
208
+
209
+ if (filePath && STUB.test(filePath) && isInside(root, filePath)) {
210
+ return { ...resolution, filePath: filePath.replace(STUB, "$1math$2mathjax.$3") }
211
+ }
212
+
213
+ if (filePath && speech !== null && isInside(speech, filePath)) {
214
+ const locale = MATHMAPS.exec(filePath)?.[1]
215
+
216
+ // An empty module rather than a missing one: the backend requires every
217
+ // locale it knows of, unconditionally, and reads the empty object back as
218
+ // "this one was not bundled" -- which is what it warns about and falls
219
+ // back from. A resolution error here would be a bundle that does not
220
+ // build.
221
+ if (locale !== undefined && locale !== BASE_LOCALE && !locales.has(locale)) {
222
+ return { type: "empty" }
223
+ }
224
+ }
225
+
226
+ return resolution
227
+ }
228
+
229
+ return { ...config, resolver: { ...config.resolver, resolveRequest } }
230
+ }
231
+
232
+ function isInside(root: string, filePath: string) {
233
+ return filePath.length > root.length && filePath.startsWith(root)
234
+ }
235
+
236
+ export type { MetroConfig, WithMathOptions }
237
+
238
+ export { withMath }
@@ -2,13 +2,16 @@ import emojiRegex from "emoji-regex"
2
2
  import { parse, postprocess, preprocess } from "micromark"
3
3
  import { gfmAutolinkLiteral } from "micromark-extension-gfm-autolink-literal"
4
4
  import { gfmStrikethrough } from "micromark-extension-gfm-strikethrough"
5
+ import { math } from "micromark-extension-math"
5
6
  import type { Token, TokenType } from "micromark-util-types"
6
7
 
7
- import type { MarkdownRange, MarkdownType } from "../commonTypes"
8
+ import type { MarkdownParser, MarkdownRange, MarkdownType, ParserOptions } from "../commonTypes"
8
9
  import { mentions } from "./micromark-extension-mentions"
9
10
 
10
11
  const EMOJI_TYPE = "emoji"
11
12
  const SYNTAX_TYPE = "syntax"
13
+ const LABEL_TYPE = "label"
14
+ const ALT_TYPE = "alt"
12
15
 
13
16
  const MAX_DEPTH = 6
14
17
 
@@ -72,6 +75,20 @@ const TOKENS: {
72
75
  "codeTextSequence",
73
76
  "codeFencedFenceSequence",
74
77
 
78
+ // Maths. The padding is the space `$ x $` is written with, which is part of
79
+ // the markup rather than of the TeX.
80
+ "mathTextSequence",
81
+ "mathTextPadding",
82
+ "mathFlowFenceSequence",
83
+
84
+ // Whatever follows the opening `$$`, which nothing reads.
85
+ "mathFlowFenceMeta",
86
+
87
+ // Whatever follows a fenced block's language. The language itself is
88
+ // `language` -- something will want to read it -- but the rest of
89
+ // that line is markup like any other fence.
90
+ "codeFencedFenceMeta",
91
+
75
92
  // Character references.
76
93
  "characterReferenceMarker",
77
94
  "characterReferenceMarkerNumeric",
@@ -89,22 +106,45 @@ const TOKENS: {
89
106
  special: {
90
107
  codeFenced: "codeblock",
91
108
  codeIndented: "codeblock",
92
- image: "inline-image",
109
+ image: "image",
110
+
111
+ // One range over the whole span, delimiters included, the way an image is
112
+ // one range over `![alt](url)`. What is inside is TeX: a display strips it
113
+ // whole and hands it to a renderer, and an input leaves it alone. Nothing
114
+ // is emitted for the content itself -- there is nothing markdown-shaped in
115
+ // there to style.
116
+ mathText: "math",
117
+ mathFlow: "math-block",
93
118
  },
94
119
  scope: {
95
- emphasis: "italic",
96
- strong: "bold",
120
+ // Identities: these carry CommonMark's own names, so what micromark calls
121
+ // a token and what a range is typed as are the same word.
122
+ emphasis: "emphasis",
123
+ strong: "strong",
97
124
  strikethrough: "strikethrough",
98
125
 
99
126
  // Headings are absent: their level is only known once the marker token
100
127
  // arrives, so they are emitted from emitHeading() instead.
101
128
  },
102
129
  content: {
103
- mention: "mention-user",
130
+ mention: "mention",
104
131
 
105
132
  codeTextData: "code",
106
133
  codeFlowValue: "pre",
107
134
 
135
+ // The language a fenced block declares. Its own type rather than `syntax`
136
+ // because it is metadata something will want to read -- highlighting, at
137
+ // least -- and no formatter styles it, so it stays plain text in an input
138
+ // the way the rest of the fence line does not.
139
+ codeFencedFenceInfo: "language",
140
+
141
+ // The title after a destination -- `[a](b "title")`, on a link as much as on
142
+ // an image. Metadata for the same reasons the language above is: a display
143
+ // renderer removes it with the rest of the resource, and something that
144
+ // renders an image wants to read it. Left unstyled in an input, where it is
145
+ // text between two quotes.
146
+ resourceTitleString: "title",
147
+
108
148
  // TODO:
109
149
  // htmlFlow: 'html',
110
150
  // htmlText: 'html',
@@ -155,22 +195,31 @@ const EMOJI: {
155
195
  "autolinkProtocol",
156
196
  "autolinkEmail",
157
197
  ]),
158
- exclude: new Set(["bold", "italic", "strikethrough"]),
198
+ exclude: new Set(["strong", "emphasis", "strikethrough"]),
159
199
  pattern: emojiRegex(),
160
200
  }
161
201
 
162
202
  // Bundler shenanigans: when bundling esm export ends up in wrapper function body
163
203
  // which is a syntax error so don't export anything at all.
164
204
  // Instead declare export so typing still works.
205
+ //
206
+ // The factory is synthesized by `scripts/bundle-parser.mjs`: the whole bundle
207
+ // below ends up inside the worklet it returns, with the options baked into that
208
+ // worklet's closure. Nothing native ever passes options -- the C++ bridge calls
209
+ // a registered worklet with a string and nothing else -- so the only place they
210
+ // can be chosen is where the worklet is made.
165
211
  declare module "./index" {
166
- export function parse(markdown: string): MarkdownRange[]
212
+ export function createParser(options?: ParserOptions): MarkdownParser
167
213
  }
168
214
 
169
215
  declare global {
170
- function __parse__micromark(markdown: string): MarkdownRange[]
216
+ function __parse__micromark(markdown: string, options?: ParserOptions): MarkdownRange[]
171
217
  }
172
218
 
173
- globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
219
+ globalThis.__parse__micromark = function (
220
+ markdown: string,
221
+ options?: ParserOptions,
222
+ ): MarkdownRange[] {
174
223
  // micromark's preprocessor drops a leading BOM, so every offset it reports is
175
224
  // one short of the caller's string -- a document that opens with one had every
176
225
  // range shifted left by a character, leaving the last one uncovered. Parse the
@@ -182,7 +231,30 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
182
231
  markdown = markdown.slice(bomLength)
183
232
  }
184
233
 
185
- const extensions = [gfmAutolinkLiteral(), gfmStrikethrough(), mentions()]
234
+ // Every extension is off unless it is asked for: a parser made with no
235
+ // options at all is CommonMark and nothing else. Each of these changes what
236
+ // some perfectly ordinary character means -- a tilde, an `@`, a dollar sign --
237
+ // and that is not a thing to do to a document on the way past.
238
+ const extensions = []
239
+
240
+ if (options?.gfm === true) {
241
+ extensions.push(gfmAutolinkLiteral(), gfmStrikethrough())
242
+ }
243
+
244
+ if (options?.mention === true) {
245
+ extensions.push(mentions())
246
+ }
247
+
248
+ if (options?.math === true) {
249
+ // Two dollars, never one. A single `$` is a currency sign far more often
250
+ // than it is a delimiter, and with the single form on, `$5 and $6` is one
251
+ // maths span whose TeX is "5 and ": prose silently turned into a picture,
252
+ // in the one place -- a document written by someone else -- where that is
253
+ // least recoverable. `$$x^2$$` costs a keystroke and can never be an
254
+ // accident.
255
+ extensions.push(math({ singleDollarTextMath: false }))
256
+ }
257
+
186
258
  const ranges: MarkdownRange[] = []
187
259
  // Counted block containers, innermost last. Never exceeds MAX_DEPTH per type.
188
260
  const openBlocks: MarkdownType[] = []
@@ -190,6 +262,16 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
190
262
  const countedBlocks: boolean[] = []
191
263
  // Heading awaiting the marker that states its level.
192
264
  let pendingHeading: Token | null = null
265
+ // Open images. A link's label is a link; an image's label is alt text, and
266
+ // the two are the same token type distinguished only by what encloses them.
267
+ let openImages = 0
268
+ // Open labels inside an image's, so that the outermost one is the alt text
269
+ // and everything under it is suppressed.
270
+ let altDepth = 0
271
+ // Open link labels. A label carries everything else it is written with --
272
+ // emphasis, code, mentions, an image -- but not a link: a link inside a link
273
+ // is text, and the one wrapped around it is the one that is pressed.
274
+ let labelDepth = 0
193
275
  // The container markers on the line being built, in the order they appear.
194
276
  // Positional rather than keyed by type: one type can open more than once on a
195
277
  // line, and the occurrences need not be adjacent (`> - > `). Cleared per
@@ -220,6 +302,9 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
220
302
  return ranges
221
303
 
222
304
  function enter(token: Token) {
305
+ // Ahead of the alt-text gate below, and deliberately: an emoji is not
306
+ // markup but a property of a character that survives, and alt text shown as
307
+ // prose has to draw it in the emoji font like any other.
223
308
  if (EMOJI.scan.has(token.type)) {
224
309
  emitEmoji(token)
225
310
  }
@@ -289,6 +374,16 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
289
374
  }
290
375
 
291
376
  if (TOKENS.special[token.type]) {
377
+ if (token.type === "image") {
378
+ openImages++
379
+
380
+ // An image written inside another one's label is not an image. It is
381
+ // some of the characters that label is spelled with.
382
+ if (altDepth > 0) {
383
+ return
384
+ }
385
+ }
386
+
292
387
  emit(token, TOKENS.special[token.type]!)
293
388
  return
294
389
  }
@@ -298,6 +393,47 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
298
393
  return
299
394
  }
300
395
 
396
+ // The visible half of a link, emitted so that a renderer which removes the
397
+ // markup around it has something left to style. Deliberately not `link`:
398
+ // while it is being edited the label is text you are typing, and only a
399
+ // renderer that has taken the brackets away has any business making it look
400
+ // like one.
401
+ if (token.type === "labelText") {
402
+ // A label nested inside an image's is part of that alt text, not a second
403
+ // one: `![a [b](c) d](e)` has exactly one.
404
+ if (altDepth > 0) {
405
+ altDepth++
406
+ return
407
+ }
408
+
409
+ // An image's label is alt text, not a link label. The two are the same
410
+ // token, distinguished only by what encloses them -- and they part ways
411
+ // downstream: a label is the link once the brackets are gone, while alt
412
+ // text is what stands in for an image nobody rendered.
413
+ if (openImages > 0) {
414
+ emit(token, ALT_TYPE)
415
+ altDepth = 1
416
+ return
417
+ }
418
+
419
+ emit(token, LABEL_TYPE)
420
+ labelDepth++
421
+ return
422
+ }
423
+
424
+ // Alt text is a string. CommonMark says as much -- it flattens a label to
425
+ // plain text to render the `alt` attribute -- but micromark reports the
426
+ // structure it found, so `![**a** [b](c)](d)` arrives carrying emphasis, a
427
+ // whole link and that link's own destination. None of it means anything:
428
+ // nothing can draw a link inside an image. Only the syntax handled above
429
+ // survives, so that whatever removes markup can still remove these
430
+ // characters, and everything downstream sees one `alt` range over one
431
+ // flat string. Emoji are the exception, emitted above: they are not markup
432
+ // and the characters they cover are still there to be drawn.
433
+ if (altDepth > 0) {
434
+ return
435
+ }
436
+
301
437
  if (TOKENS.scope[token.type]) {
302
438
  emit(token, TOKENS.scope[token.type]!)
303
439
  return
@@ -314,12 +450,40 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
314
450
  }
315
451
 
316
452
  if (TOKENS.link[token.type]) {
453
+ // A link written inside a label is not one: `[<https://x.com>](y)` reads
454
+ // as its own URL and points somewhere else. Its markers are still markup
455
+ // and still go, but what they leave behind is prose, so no range is
456
+ // emitted over it -- a second `link` here would draw it as a link of its
457
+ // own in an input and, once the display has taken the brackets away,
458
+ // would cover the same characters twice.
459
+ //
460
+ // An image's destination is not that: it is inside the label but it is
461
+ // the image's, and something has to render the image.
462
+ if (labelDepth > 0 && openImages === 0) {
463
+ return
464
+ }
465
+
317
466
  emit(token, TOKENS.link[token.type]!)
318
467
  return
319
468
  }
320
469
  }
321
470
 
322
471
  function exit(token: Token) {
472
+ if (token.type === "labelText") {
473
+ if (altDepth > 0) {
474
+ altDepth--
475
+ } else if (labelDepth > 0) {
476
+ labelDepth--
477
+ }
478
+
479
+ return
480
+ }
481
+
482
+ if (token.type === "image") {
483
+ openImages--
484
+ return
485
+ }
486
+
323
487
  if (!TOKENS.block[token.type]) {
324
488
  return
325
489
  }
@@ -554,3 +718,4 @@ globalThis.__parse__micromark = function (markdown: string): MarkdownRange[] {
554
718
  ranges.push(...result)
555
719
  }
556
720
  }
721
+
@@ -1,9 +1,20 @@
1
- import type { Code, Effects, Extension, State } from "micromark-util-types"
1
+ import type {
2
+ Code,
3
+ Construct,
4
+ Effects,
5
+ Extension,
6
+ State,
7
+ TokenizeContext,
8
+ } from "micromark-util-types"
2
9
 
3
10
  const LETTER = /^\p{L}$/u
4
- const WORD = /^[\p{L}\p{M}\p{N}_]$/u
5
11
  const ALPHANUMERIC = /^[\p{L}\p{M}\p{N}]$/u
6
12
 
13
+ const AT = 64
14
+ const DOT = 46
15
+ const HYPHEN = 45
16
+ const UNDERSCORE = 95
17
+
7
18
  declare module "micromark-util-types" {
8
19
  interface TokenTypeMap {
9
20
  mention: "mention"
@@ -13,8 +24,20 @@ declare module "micromark-util-types" {
13
24
  const mention = {
14
25
  name: "mention",
15
26
  tokenize,
27
+ previous,
16
28
  } as const
17
29
 
30
+ /**
31
+ * A `-`, `_` or `@` and the part of the name after it.
32
+ *
33
+ * Attempted rather than consumed outright, because micromark cannot give a
34
+ * character back: a name that ends in front of one -- `@user@ ` -- has to leave
35
+ * the separator outside the token, and an attempt that fails rewinds to it.
36
+ * That is also what lets `_@someone_` be a mention in italics: the closing `_`
37
+ * has no name after it, so it stays markup.
38
+ */
39
+ const join: Construct = { tokenize: tokenizeJoin, partial: true }
40
+
18
41
  export function mentions(): Extension {
19
42
  return {
20
43
  text: {
@@ -23,6 +46,20 @@ export function mentions(): Extension {
23
46
  }
24
47
  }
25
48
 
49
+ /**
50
+ * A mention starts where a name starts: not inside a word, and not straight
51
+ * after another `@`.
52
+ *
53
+ * That is what keeps `user@example.com` an address rather than an address with
54
+ * a mention buried in it, and `@@user` a stray `@` rather than a name beginning
55
+ * with one. Everything else may sit against it -- `**@someone**`, `[@user](/u)`
56
+ * and `(@user)` all hold one -- because the markup is gone by the time a
57
+ * display draws it, and punctuation was never part of a name to begin with.
58
+ */
59
+ function previous(this: TokenizeContext, code: Code): boolean {
60
+ return code === null || (code !== AT && !test(ALPHANUMERIC, code))
61
+ }
62
+
26
63
  function tokenize(effects: Effects, ok: State, nok: State): State {
27
64
  return start
28
65
 
@@ -44,32 +81,88 @@ function tokenize(effects: Effects, ok: State, nok: State): State {
44
81
 
45
82
  effects.consume(code)
46
83
 
47
- return body
84
+ return part
48
85
  }
49
86
 
50
- function body(code: Code): State {
51
- if (code !== null && test(WORD, code)) {
87
+ /** Inside a part of the name: letters, marks and digits. */
88
+ function part(code: Code): State {
89
+ if (code !== null && test(ALPHANUMERIC, code)) {
90
+ effects.consume(code)
91
+ return part
92
+ }
93
+
94
+ if (code === DOT) {
52
95
  effects.consume(code)
53
- return body
96
+ return afterDot
54
97
  }
55
98
 
56
- if (code === 45) {
57
- // `-` is allowed only when followed by an alphanumeric character.
58
- return hyphen
99
+ if (code === HYPHEN || code === UNDERSCORE || code === AT) {
100
+ return effects.attempt(join, part, done)
59
101
  }
60
102
 
103
+ return done(code)
104
+ }
105
+
106
+ /**
107
+ * Just after a dot, which belongs to the name wherever it falls -- `@bullet.`
108
+ * is one mention rather than a mention and a full stop.
109
+ *
110
+ * Only the first of several does, though: a second dot in a row ends the name
111
+ * here, in front of it, so `@bullet..` is `@bullet.` and a full stop that
112
+ * still reads as one.
113
+ */
114
+ function afterDot(code: Code): State {
115
+ if (code === DOT) {
116
+ return done(code)
117
+ }
118
+
119
+ return part(code)
120
+ }
121
+
122
+ /**
123
+ * The name is finished.
124
+ *
125
+ * Nothing is checked here: `part` consumes every character a name is made of,
126
+ * so whatever stopped it is not one -- whitespace, punctuation, or the markup
127
+ * the mention was written inside.
128
+ */
129
+ function done(code: Code): State {
61
130
  effects.exit("mention")
131
+
62
132
  return ok(code)!
63
133
  }
134
+ }
135
+
136
+ function tokenizeJoin(effects: Effects, ok: State, nok: State): State {
137
+ return separator
138
+
139
+ function separator(code: Code): State {
140
+ // A `-`, `_` or `@`: `part` only attempts this construct on one of the
141
+ // three. An `@` opens a new name and so wants a letter; the other two join
142
+ // two parts of one and take whatever a part is made of.
143
+ effects.consume(code)
64
144
 
65
- function hyphen(code: Code): State {
145
+ return code === AT ? afterAt : afterJoin
146
+ }
147
+
148
+ function afterJoin(code: Code): State {
66
149
  if (code === null || !test(ALPHANUMERIC, code)) {
67
150
  return nok(code)!
68
151
  }
69
152
 
70
153
  effects.consume(code)
71
154
 
72
- return body
155
+ return ok
156
+ }
157
+
158
+ function afterAt(code: Code): State {
159
+ if (code === null || !test(LETTER, code)) {
160
+ return nok(code)!
161
+ }
162
+
163
+ effects.consume(code)
164
+
165
+ return ok
73
166
  }
74
167
  }
75
168