setext-underline.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * @import {
  3. * Code,
  4. * Construct,
  5. * Resolver,
  6. * State,
  7. * TokenizeContext,
  8. * Tokenizer
  9. * } from 'micromark-util-types'
  10. */
  11. import { factorySpace } from 'micromark-factory-space';
  12. import { markdownLineEnding, markdownSpace } from 'micromark-util-character';
  13. /** @type {Construct} */
  14. export const setextUnderline = {
  15. name: 'setextUnderline',
  16. resolveTo: resolveToSetextUnderline,
  17. tokenize: tokenizeSetextUnderline
  18. };
  19. /** @type {Resolver} */
  20. function resolveToSetextUnderline(events, context) {
  21. // To do: resolve like `markdown-rs`.
  22. let index = events.length;
  23. /** @type {number | undefined} */
  24. let content;
  25. /** @type {number | undefined} */
  26. let text;
  27. /** @type {number | undefined} */
  28. let definition;
  29. // Find the opening of the content.
  30. // It’ll always exist: we don’t tokenize if it isn’t there.
  31. while (index--) {
  32. if (events[index][0] === 'enter') {
  33. if (events[index][1].type === "content") {
  34. content = index;
  35. break;
  36. }
  37. if (events[index][1].type === "paragraph") {
  38. text = index;
  39. }
  40. }
  41. // Exit
  42. else {
  43. if (events[index][1].type === "content") {
  44. // Remove the content end (if needed we’ll add it later)
  45. events.splice(index, 1);
  46. }
  47. if (!definition && events[index][1].type === "definition") {
  48. definition = index;
  49. }
  50. }
  51. }
  52. const heading = {
  53. type: "setextHeading",
  54. start: {
  55. ...events[content][1].start
  56. },
  57. end: {
  58. ...events[events.length - 1][1].end
  59. }
  60. };
  61. // Change the paragraph to setext heading text.
  62. events[text][1].type = "setextHeadingText";
  63. // If we have definitions in the content, we’ll keep on having content,
  64. // but we need move it.
  65. if (definition) {
  66. events.splice(text, 0, ['enter', heading, context]);
  67. events.splice(definition + 1, 0, ['exit', events[content][1], context]);
  68. events[content][1].end = {
  69. ...events[definition][1].end
  70. };
  71. } else {
  72. events[content][1] = heading;
  73. }
  74. // Add the heading exit at the end.
  75. events.push(['exit', heading, context]);
  76. return events;
  77. }
  78. /**
  79. * @this {TokenizeContext}
  80. * Context.
  81. * @type {Tokenizer}
  82. */
  83. function tokenizeSetextUnderline(effects, ok, nok) {
  84. const self = this;
  85. /** @type {NonNullable<Code>} */
  86. let marker;
  87. return start;
  88. /**
  89. * At start of heading (setext) underline.
  90. *
  91. * ```markdown
  92. * | aa
  93. * > | ==
  94. * ^
  95. * ```
  96. *
  97. * @type {State}
  98. */
  99. function start(code) {
  100. let index = self.events.length;
  101. /** @type {boolean | undefined} */
  102. let paragraph;
  103. // Find an opening.
  104. while (index--) {
  105. // Skip enter/exit of line ending, line prefix, and content.
  106. // We can now either have a definition or a paragraph.
  107. if (self.events[index][1].type !== "lineEnding" && self.events[index][1].type !== "linePrefix" && self.events[index][1].type !== "content") {
  108. paragraph = self.events[index][1].type === "paragraph";
  109. break;
  110. }
  111. }
  112. // To do: handle lazy/pierce like `markdown-rs`.
  113. // To do: parse indent like `markdown-rs`.
  114. if (!self.parser.lazy[self.now().line] && (self.interrupt || paragraph)) {
  115. effects.enter("setextHeadingLine");
  116. marker = code;
  117. return before(code);
  118. }
  119. return nok(code);
  120. }
  121. /**
  122. * After optional whitespace, at `-` or `=`.
  123. *
  124. * ```markdown
  125. * | aa
  126. * > | ==
  127. * ^
  128. * ```
  129. *
  130. * @type {State}
  131. */
  132. function before(code) {
  133. effects.enter("setextHeadingLineSequence");
  134. return inside(code);
  135. }
  136. /**
  137. * In sequence.
  138. *
  139. * ```markdown
  140. * | aa
  141. * > | ==
  142. * ^
  143. * ```
  144. *
  145. * @type {State}
  146. */
  147. function inside(code) {
  148. if (code === marker) {
  149. effects.consume(code);
  150. return inside;
  151. }
  152. effects.exit("setextHeadingLineSequence");
  153. return markdownSpace(code) ? factorySpace(effects, after, "lineSuffix")(code) : after(code);
  154. }
  155. /**
  156. * After sequence, after optional whitespace.
  157. *
  158. * ```markdown
  159. * | aa
  160. * > | ==
  161. * ^
  162. * ```
  163. *
  164. * @type {State}
  165. */
  166. function after(code) {
  167. if (code === null || markdownLineEnding(code)) {
  168. effects.exit("setextHeadingLine");
  169. return ok(code);
  170. }
  171. return nok(code);
  172. }
  173. }