heading-atx.js 4.9 KB

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