remarkMathCompatibility.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. /** Extend upstream dollar-only math syntax with TeX delimiters while reusing its token vocabulary. */
  2. import { factorySpace } from 'micromark-factory-space'
  3. import type {} from 'micromark-extension-math'
  4. import { markdownLineEnding } from 'micromark-util-character'
  5. import { codes, constants, types } from 'micromark-util-symbol'
  6. import type { Construct, Extension, Previous, State, Tokenizer } from 'micromark-util-types'
  7. // oxlint-disable typescript/no-this-alias -- micromark binds tokenizer context only on the outer callback.
  8. interface RemarkProcessor {
  9. data(): { micromarkExtensions?: Extension[] }
  10. }
  11. const previousBackslash: Previous = function (code) {
  12. if (code !== codes.backslash) return true
  13. const tail = this.events.at(-1)
  14. /* v8 ignore next -- a previous code necessarily has a preceding event. */
  15. if (tail === undefined) return false
  16. return tail[1].type === types.characterEscape
  17. }
  18. const tokenizeBackslashMathText: Tokenizer = function (effects, ok, nok) {
  19. return start
  20. function start(code: number | null): State | undefined {
  21. /* v8 ignore next -- the text construct is dispatched only for a backslash. */
  22. if (code !== codes.backslash) return nok(code)
  23. effects.enter('mathText')
  24. effects.enter('mathTextSequence')
  25. effects.consume(code)
  26. return open
  27. }
  28. function open(code: number | null): State | undefined {
  29. if (code !== codes.leftParenthesis) return nok(code)
  30. effects.consume(code)
  31. effects.exit('mathTextSequence')
  32. return between
  33. }
  34. function between(code: number | null): State | undefined {
  35. if (code === codes.eof) return nok(code)
  36. if (code === codes.backslash) {
  37. return effects.attempt({ partial: true, tokenize: tokenizeClose }, close, afterCloseAttempt)(code)
  38. }
  39. if (markdownLineEnding(code)) {
  40. effects.enter(types.lineEnding)
  41. effects.consume(code)
  42. effects.exit(types.lineEnding)
  43. return between
  44. }
  45. return dataStart(code)
  46. }
  47. function afterCloseAttempt(code: number | null): State | undefined {
  48. return effects.check({ partial: true, tokenize: tokenizeOpen }, nok, dataStart)(code)
  49. }
  50. function dataStart(code: number | null): State | undefined {
  51. effects.enter('mathTextData')
  52. effects.consume(code)
  53. return code === codes.backslash ? afterDataBackslash : data
  54. }
  55. function afterDataBackslash(code: number | null): State | undefined {
  56. if (code === codes.backslash) {
  57. effects.consume(code)
  58. return data
  59. }
  60. return data(code)
  61. }
  62. function data(code: number | null): State | undefined {
  63. if (code === codes.eof || code === codes.backslash || markdownLineEnding(code)) {
  64. effects.exit('mathTextData')
  65. return between(code)
  66. }
  67. effects.consume(code)
  68. return data
  69. }
  70. function close(code: number | null): State | undefined {
  71. effects.exit('mathText')
  72. return ok(code)
  73. }
  74. function tokenizeClose(closeEffects: Parameters<Tokenizer>[0], closeOk: State, closeNok: State): State {
  75. return slash
  76. function slash(code: number | null): State | undefined {
  77. /* v8 ignore next -- this partial construct is attempted only at a backslash. */
  78. if (code !== codes.backslash) return closeNok(code)
  79. closeEffects.enter('mathTextSequence')
  80. closeEffects.consume(code)
  81. return parenthesis
  82. }
  83. function parenthesis(code: number | null): State | undefined {
  84. if (code !== codes.rightParenthesis) return closeNok(code)
  85. closeEffects.consume(code)
  86. closeEffects.exit('mathTextSequence')
  87. return closeOk
  88. }
  89. }
  90. function tokenizeOpen(openEffects: Parameters<Tokenizer>[0], openOk: State, openNok: State): State {
  91. return slash
  92. function slash(code: number | null): State | undefined {
  93. /* v8 ignore next -- the opening check follows a failed close attempt at a backslash. */
  94. if (code !== codes.backslash) return openNok(code)
  95. openEffects.enter(types.chunkString)
  96. openEffects.consume(code)
  97. return parenthesis
  98. }
  99. function parenthesis(code: number | null): State | undefined {
  100. if (code !== codes.leftParenthesis) return openNok(code)
  101. openEffects.consume(code)
  102. openEffects.exit(types.chunkString)
  103. return openOk
  104. }
  105. }
  106. }
  107. function createMathFlow(marker: number, openMarker: number, closeMarker: number, multiline: boolean): Construct {
  108. const tokenize: Tokenizer = function (effects, ok, nok) {
  109. const self = this
  110. let oddBackslashRun = false
  111. const tail = self.events.at(-1)
  112. const initialSize = tail?.[1].type === types.linePrefix
  113. ? tail[2].sliceSerialize(tail[1], true).length
  114. : 0
  115. return start
  116. function start(code: number | null): State | undefined {
  117. /* v8 ignore next -- the flow construct is dispatched only for its marker. */
  118. if (code !== marker) return nok(code)
  119. effects.enter('mathFlow')
  120. effects.enter('mathFlowFence')
  121. effects.enter('mathFlowFenceSequence')
  122. effects.consume(code)
  123. return open
  124. }
  125. function open(code: number | null): State | undefined {
  126. if (code !== openMarker) return nok(code)
  127. effects.consume(code)
  128. effects.exit('mathFlowFenceSequence')
  129. effects.exit('mathFlowFence')
  130. return marker === codes.dollarSign ? afterDollarOpen : content
  131. }
  132. function afterDollarOpen(code: number | null): State | undefined {
  133. return code === codes.dollarSign ? nok(code) : content(code)
  134. }
  135. function content(code: number | null): State | undefined {
  136. if (code === codes.eof) return nok(code)
  137. if (code === marker && (marker !== codes.dollarSign || !oddBackslashRun)) {
  138. return effects.attempt(
  139. { partial: true, tokenize: tokenizeClosingFence },
  140. closed,
  141. afterClosingFenceAttempt,
  142. )(code)
  143. }
  144. if (markdownLineEnding(code)) {
  145. return multiline
  146. ? effects.attempt(nonLazyContinuation, afterContinuation, nok)(code)
  147. : nok(code)
  148. }
  149. return valueStart(code)
  150. }
  151. function afterClosingFenceAttempt(code: number | null): State | undefined {
  152. return marker === codes.backslash
  153. ? effects.check({ partial: true, tokenize: tokenizeOpeningFence }, nok, markerValueStart)(code)
  154. : markerValueStart(code)
  155. }
  156. function afterContinuation(code: number | null): State | undefined {
  157. return effects.attempt(
  158. { partial: true, tokenize: tokenizeClosingFence },
  159. closed,
  160. initialSize
  161. ? factorySpace(effects, content, types.linePrefix, initialSize + 1)
  162. : content,
  163. )(code)
  164. }
  165. function valueStart(code: number | null): State | undefined {
  166. effects.enter('mathFlowValue')
  167. oddBackslashRun = code === codes.backslash
  168. effects.consume(code)
  169. return value
  170. }
  171. function markerValueStart(code: number | null): State | undefined {
  172. effects.enter('mathFlowValue')
  173. oddBackslashRun = false
  174. effects.consume(code)
  175. return valueAfterMarker
  176. }
  177. function valueAfterMarker(code: number | null): State | undefined {
  178. if (code === marker) {
  179. effects.consume(code)
  180. return value
  181. }
  182. return value(code)
  183. }
  184. function value(code: number | null): State | undefined {
  185. if (code === codes.eof || code === marker || markdownLineEnding(code)) {
  186. effects.exit('mathFlowValue')
  187. return content(code)
  188. }
  189. oddBackslashRun = code === codes.backslash ? !oddBackslashRun : false
  190. effects.consume(code)
  191. return value
  192. }
  193. function closed(code: number | null): State | undefined {
  194. effects.exit('mathFlow')
  195. return ok(code)
  196. }
  197. function tokenizeClosingFence(
  198. closeEffects: Parameters<Tokenizer>[0],
  199. closeOk: State,
  200. closeNok: State,
  201. ): State {
  202. return factorySpace(closeEffects, sequenceStart, types.linePrefix, constants.tabSize)
  203. function sequenceStart(code: number | null): State | undefined {
  204. if (code !== marker) return closeNok(code)
  205. closeEffects.enter('mathFlowFence')
  206. closeEffects.enter('mathFlowFenceSequence')
  207. closeEffects.consume(code)
  208. return sequenceEnd
  209. }
  210. function sequenceEnd(code: number | null): State | undefined {
  211. if (code !== closeMarker) return closeNok(code)
  212. closeEffects.consume(code)
  213. closeEffects.exit('mathFlowFenceSequence')
  214. return factorySpace(closeEffects, after, types.whitespace)
  215. }
  216. function after(code: number | null): State | undefined {
  217. if (code !== codes.eof && !markdownLineEnding(code)) return closeNok(code)
  218. closeEffects.exit('mathFlowFence')
  219. return closeOk(code)
  220. }
  221. }
  222. function tokenizeOpeningFence(
  223. openEffects: Parameters<Tokenizer>[0],
  224. openOk: State,
  225. openNok: State,
  226. ): State {
  227. return sequenceStart
  228. function sequenceStart(code: number | null): State | undefined {
  229. /* v8 ignore next -- the opening check follows a failed close attempt at the marker. */
  230. if (code !== marker) return openNok(code)
  231. openEffects.enter(types.chunkString)
  232. openEffects.consume(code)
  233. return sequenceEnd
  234. }
  235. function sequenceEnd(code: number | null): State | undefined {
  236. if (code !== openMarker) return openNok(code)
  237. openEffects.consume(code)
  238. openEffects.exit(types.chunkString)
  239. return openOk
  240. }
  241. }
  242. }
  243. return {
  244. concrete: true,
  245. name: marker === codes.dollarSign ? 'sameLineDollarMathFlow' : 'backslashMathFlow',
  246. tokenize,
  247. }
  248. }
  249. const tokenizeNonLazyContinuation: Tokenizer = function (effects, ok, nok) {
  250. const self = this
  251. return start
  252. function start(code: number | null): State | undefined {
  253. /* v8 ignore next -- continuation constructs are attempted only after a line ending. */
  254. if (code === codes.eof) return ok(code)
  255. /* v8 ignore next -- continuation constructs are attempted only after a line ending. */
  256. if (!markdownLineEnding(code)) return nok(code)
  257. effects.enter(types.lineEnding)
  258. effects.consume(code)
  259. effects.exit(types.lineEnding)
  260. return lineStart
  261. }
  262. function lineStart(code: number | null): State | undefined {
  263. return self.parser.lazy[self.now().line] ? nok(code) : ok(code)
  264. }
  265. }
  266. const nonLazyContinuation: Construct = {
  267. partial: true,
  268. tokenize: tokenizeNonLazyContinuation,
  269. }
  270. const backslashMathText: Construct = {
  271. name: 'backslashMathText',
  272. previous: previousBackslash,
  273. tokenize: tokenizeBackslashMathText,
  274. }
  275. const backslashMathFlow = createMathFlow(
  276. codes.backslash,
  277. codes.leftSquareBracket,
  278. codes.rightSquareBracket,
  279. true,
  280. )
  281. const sameLineDollarMathFlow = createMathFlow(
  282. codes.dollarSign,
  283. codes.dollarSign,
  284. codes.dollarSign,
  285. false,
  286. )
  287. const backslashMath: Extension = {
  288. flow: {
  289. [codes.backslash]: backslashMathFlow,
  290. [codes.dollarSign]: sameLineDollarMathFlow,
  291. },
  292. text: { [codes.backslash]: backslashMathText },
  293. }
  294. /**
  295. * Add TeX backslash delimiters and same-line display-dollar blocks for remark-math.
  296. * The same processor must register remark-math to compile the emitted math tokens.
  297. * @returns Nothing.
  298. */
  299. export function remarkMathCompatibility(this: RemarkProcessor): undefined {
  300. const data = this.data()
  301. const extensions = data.micromarkExtensions ?? (data.micromarkExtensions = [])
  302. extensions.push(backslashMath)
  303. }