heading-atx.js 4.5 KB

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