math-text.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. /**
  2. * @import {Options} from 'micromark-extension-math'
  3. * @import {Construct, Previous, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types'
  4. */
  5. // To do: next major: clean spaces in HTML compiler.
  6. // This has to be coordinated together with `mdast-util-math`.
  7. import {ok as assert} from 'devlop'
  8. import {markdownLineEnding} from 'micromark-util-character'
  9. import {codes, types} from 'micromark-util-symbol'
  10. /**
  11. * @param {Options | null | undefined} [options={}]
  12. * Configuration (default: `{}`).
  13. * @returns {Construct}
  14. * Construct.
  15. */
  16. export function mathText(options) {
  17. const options_ = options || {}
  18. let single = options_.singleDollarTextMath
  19. if (single === null || single === undefined) {
  20. single = true
  21. }
  22. return {
  23. tokenize: tokenizeMathText,
  24. resolve: resolveMathText,
  25. previous,
  26. name: 'mathText'
  27. }
  28. /**
  29. * @this {TokenizeContext}
  30. * @type {Tokenizer}
  31. */
  32. function tokenizeMathText(effects, ok, nok) {
  33. const self = this
  34. let sizeOpen = 0
  35. /** @type {number} */
  36. let size
  37. /** @type {Token} */
  38. let token
  39. return start
  40. /**
  41. * Start of math (text).
  42. *
  43. * ```markdown
  44. * > | $a$
  45. * ^
  46. * > | \$a$
  47. * ^
  48. * ```
  49. *
  50. * @type {State}
  51. */
  52. function start(code) {
  53. assert(code === codes.dollarSign, 'expected `$`')
  54. assert(previous.call(self, self.previous), 'expected correct previous')
  55. effects.enter('mathText')
  56. effects.enter('mathTextSequence')
  57. return sequenceOpen(code)
  58. }
  59. /**
  60. * In opening sequence.
  61. *
  62. * ```markdown
  63. * > | $a$
  64. * ^
  65. * ```
  66. *
  67. * @type {State}
  68. */
  69. function sequenceOpen(code) {
  70. if (code === codes.dollarSign) {
  71. effects.consume(code)
  72. sizeOpen++
  73. return sequenceOpen
  74. }
  75. // Not enough markers in the sequence.
  76. if (sizeOpen < 2 && !single) {
  77. return nok(code)
  78. }
  79. effects.exit('mathTextSequence')
  80. return between(code)
  81. }
  82. /**
  83. * Between something and something else.
  84. *
  85. * ```markdown
  86. * > | $a$
  87. * ^^
  88. * ```
  89. *
  90. * @type {State}
  91. */
  92. function between(code) {
  93. if (code === codes.eof) {
  94. return nok(code)
  95. }
  96. if (code === codes.dollarSign) {
  97. token = effects.enter('mathTextSequence')
  98. size = 0
  99. return sequenceClose(code)
  100. }
  101. // Tabs don’t work, and virtual spaces don’t make sense.
  102. if (code === codes.space) {
  103. effects.enter('space')
  104. effects.consume(code)
  105. effects.exit('space')
  106. return between
  107. }
  108. if (markdownLineEnding(code)) {
  109. effects.enter(types.lineEnding)
  110. effects.consume(code)
  111. effects.exit(types.lineEnding)
  112. return between
  113. }
  114. // Data.
  115. effects.enter('mathTextData')
  116. return data(code)
  117. }
  118. /**
  119. * In data.
  120. *
  121. * ```markdown
  122. * > | $a$
  123. * ^
  124. * ```
  125. *
  126. * @type {State}
  127. */
  128. function data(code) {
  129. if (
  130. code === codes.eof ||
  131. code === codes.space ||
  132. code === codes.dollarSign ||
  133. markdownLineEnding(code)
  134. ) {
  135. effects.exit('mathTextData')
  136. return between(code)
  137. }
  138. effects.consume(code)
  139. return data
  140. }
  141. /**
  142. * In closing sequence.
  143. *
  144. * ```markdown
  145. * > | `a`
  146. * ^
  147. * ```
  148. *
  149. * @type {State}
  150. */
  151. function sequenceClose(code) {
  152. // More.
  153. if (code === codes.dollarSign) {
  154. effects.consume(code)
  155. size++
  156. return sequenceClose
  157. }
  158. // Done!
  159. if (size === sizeOpen) {
  160. effects.exit('mathTextSequence')
  161. effects.exit('mathText')
  162. return ok(code)
  163. }
  164. // More or less accents: mark as data.
  165. token.type = 'mathTextData'
  166. return data(code)
  167. }
  168. }
  169. }
  170. /** @type {Resolver} */
  171. function resolveMathText(events) {
  172. let tailExitIndex = events.length - 4
  173. let headEnterIndex = 3
  174. /** @type {number} */
  175. let index
  176. /** @type {number | undefined} */
  177. let enter
  178. // If we start and end with an EOL or a space.
  179. if (
  180. (events[headEnterIndex][1].type === types.lineEnding ||
  181. events[headEnterIndex][1].type === 'space') &&
  182. (events[tailExitIndex][1].type === types.lineEnding ||
  183. events[tailExitIndex][1].type === 'space')
  184. ) {
  185. index = headEnterIndex
  186. // And we have data.
  187. while (++index < tailExitIndex) {
  188. if (events[index][1].type === 'mathTextData') {
  189. // Then we have padding.
  190. events[tailExitIndex][1].type = 'mathTextPadding'
  191. events[headEnterIndex][1].type = 'mathTextPadding'
  192. headEnterIndex += 2
  193. tailExitIndex -= 2
  194. break
  195. }
  196. }
  197. }
  198. // Merge adjacent spaces and data.
  199. index = headEnterIndex - 1
  200. tailExitIndex++
  201. while (++index <= tailExitIndex) {
  202. if (enter === undefined) {
  203. if (
  204. index !== tailExitIndex &&
  205. events[index][1].type !== types.lineEnding
  206. ) {
  207. enter = index
  208. }
  209. } else if (
  210. index === tailExitIndex ||
  211. events[index][1].type === types.lineEnding
  212. ) {
  213. events[enter][1].type = 'mathTextData'
  214. if (index !== enter + 2) {
  215. events[enter][1].end = events[index - 1][1].end
  216. events.splice(enter + 2, index - enter - 2)
  217. tailExitIndex -= index - enter - 2
  218. index = enter + 2
  219. }
  220. enter = undefined
  221. }
  222. }
  223. return events
  224. }
  225. /**
  226. * @this {TokenizeContext}
  227. * @type {Previous}
  228. */
  229. function previous(code) {
  230. // If there is a previous code, there will always be a tail.
  231. return (
  232. code !== codes.dollarSign ||
  233. this.events[this.events.length - 1][1].type === types.characterEscape
  234. )
  235. }