math-text.js 5.3 KB

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