heading-atx.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * @import {
  3. * Construct,
  4. * Resolver,
  5. * State,
  6. * TokenizeContext,
  7. * Tokenizer,
  8. * Token
  9. * } from 'micromark-util-types'
  10. */
  11. import { factorySpace } from 'micromark-factory-space';
  12. import { markdownLineEndingOrSpace, markdownLineEnding, markdownSpace } from 'micromark-util-character';
  13. import { splice } from 'micromark-util-chunked';
  14. /** @type {Construct} */
  15. export const headingAtx = {
  16. name: 'headingAtx',
  17. resolve: resolveHeadingAtx,
  18. tokenize: tokenizeHeadingAtx
  19. };
  20. /** @type {Resolver} */
  21. function resolveHeadingAtx(events, context) {
  22. let contentEnd = events.length - 2;
  23. let contentStart = 3;
  24. /** @type {Token} */
  25. let content;
  26. /** @type {Token} */
  27. let text;
  28. // Prefix whitespace, part of the opening.
  29. if (events[contentStart][1].type === "whitespace") {
  30. contentStart += 2;
  31. }
  32. // Suffix whitespace, part of the closing.
  33. if (contentEnd - 2 > contentStart && events[contentEnd][1].type === "whitespace") {
  34. contentEnd -= 2;
  35. }
  36. if (events[contentEnd][1].type === "atxHeadingSequence" && (contentStart === contentEnd - 1 || contentEnd - 4 > contentStart && events[contentEnd - 2][1].type === "whitespace")) {
  37. contentEnd -= contentStart + 1 === contentEnd ? 2 : 4;
  38. }
  39. if (contentEnd > contentStart) {
  40. content = {
  41. type: "atxHeadingText",
  42. start: events[contentStart][1].start,
  43. end: events[contentEnd][1].end
  44. };
  45. text = {
  46. type: "chunkText",
  47. start: events[contentStart][1].start,
  48. end: events[contentEnd][1].end,
  49. contentType: "text"
  50. };
  51. splice(events, contentStart, contentEnd - contentStart + 1, [['enter', content, context], ['enter', text, context], ['exit', text, context], ['exit', content, context]]);
  52. }
  53. return events;
  54. }
  55. /**
  56. * @this {TokenizeContext}
  57. * Context.
  58. * @type {Tokenizer}
  59. */
  60. function tokenizeHeadingAtx(effects, ok, nok) {
  61. let size = 0;
  62. return start;
  63. /**
  64. * Start of a heading (atx).
  65. *
  66. * ```markdown
  67. * > | ## aa
  68. * ^
  69. * ```
  70. *
  71. * @type {State}
  72. */
  73. function start(code) {
  74. // To do: parse indent like `markdown-rs`.
  75. effects.enter("atxHeading");
  76. return before(code);
  77. }
  78. /**
  79. * After optional whitespace, at `#`.
  80. *
  81. * ```markdown
  82. * > | ## aa
  83. * ^
  84. * ```
  85. *
  86. * @type {State}
  87. */
  88. function before(code) {
  89. effects.enter("atxHeadingSequence");
  90. return sequenceOpen(code);
  91. }
  92. /**
  93. * In opening sequence.
  94. *
  95. * ```markdown
  96. * > | ## aa
  97. * ^
  98. * ```
  99. *
  100. * @type {State}
  101. */
  102. function sequenceOpen(code) {
  103. if (code === 35 && size++ < 6) {
  104. effects.consume(code);
  105. return sequenceOpen;
  106. }
  107. // Always at least one `#`.
  108. if (code === null || markdownLineEndingOrSpace(code)) {
  109. effects.exit("atxHeadingSequence");
  110. return atBreak(code);
  111. }
  112. return nok(code);
  113. }
  114. /**
  115. * After something, before something else.
  116. *
  117. * ```markdown
  118. * > | ## aa
  119. * ^
  120. * ```
  121. *
  122. * @type {State}
  123. */
  124. function atBreak(code) {
  125. if (code === 35) {
  126. effects.enter("atxHeadingSequence");
  127. return sequenceFurther(code);
  128. }
  129. if (code === null || markdownLineEnding(code)) {
  130. effects.exit("atxHeading");
  131. // To do: interrupt like `markdown-rs`.
  132. // // Feel free to interrupt.
  133. // tokenizer.interrupt = false
  134. return ok(code);
  135. }
  136. if (markdownSpace(code)) {
  137. return factorySpace(effects, atBreak, "whitespace")(code);
  138. }
  139. // To do: generate `data` tokens, add the `text` token later.
  140. // Needs edit map, see: `markdown.rs`.
  141. effects.enter("atxHeadingText");
  142. return data(code);
  143. }
  144. /**
  145. * In further sequence (after whitespace).
  146. *
  147. * Could be normal “visible” hashes in the heading or a final sequence.
  148. *
  149. * ```markdown
  150. * > | ## aa ##
  151. * ^
  152. * ```
  153. *
  154. * @type {State}
  155. */
  156. function sequenceFurther(code) {
  157. if (code === 35) {
  158. effects.consume(code);
  159. return sequenceFurther;
  160. }
  161. effects.exit("atxHeadingSequence");
  162. return atBreak(code);
  163. }
  164. /**
  165. * In text.
  166. *
  167. * ```markdown
  168. * > | ## aa
  169. * ^
  170. * ```
  171. *
  172. * @type {State}
  173. */
  174. function data(code) {
  175. if (code === null || code === 35 || markdownLineEndingOrSpace(code)) {
  176. effects.exit("atxHeadingText");
  177. return atBreak(code);
  178. }
  179. effects.consume(code);
  180. return data;
  181. }
  182. }