index.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. /**
  2. * @import {Chunk, Event, Token} from 'micromark-util-types'
  3. */
  4. import {ok as assert} from 'devlop'
  5. import {splice} from 'micromark-util-chunked'
  6. import {codes, types} from 'micromark-util-symbol'
  7. import {SpliceBuffer} from './lib/splice-buffer.js'
  8. // Hidden API exposed for testing.
  9. export {SpliceBuffer} from './lib/splice-buffer.js'
  10. /**
  11. * Tokenize subcontent.
  12. *
  13. * @param {Array<Event>} eventsArray
  14. * List of events.
  15. * @returns {boolean}
  16. * Whether subtokens were found.
  17. */
  18. // eslint-disable-next-line complexity
  19. export function subtokenize(eventsArray) {
  20. /** @type {Record<string, number>} */
  21. const jumps = {}
  22. let index = -1
  23. /** @type {Event} */
  24. let event
  25. /** @type {number | undefined} */
  26. let lineIndex
  27. /** @type {number} */
  28. let otherIndex
  29. /** @type {Event} */
  30. let otherEvent
  31. /** @type {Array<Event>} */
  32. let parameters
  33. /** @type {Array<Event>} */
  34. let subevents
  35. /** @type {boolean | undefined} */
  36. let more
  37. const events = new SpliceBuffer(eventsArray)
  38. while (++index < events.length) {
  39. while (index in jumps) {
  40. index = jumps[index]
  41. }
  42. event = events.get(index)
  43. // Add a hook for the GFM tasklist extension, which needs to know if text
  44. // is in the first content of a list item.
  45. if (
  46. index &&
  47. event[1].type === types.chunkFlow &&
  48. events.get(index - 1)[1].type === types.listItemPrefix
  49. ) {
  50. assert(event[1]._tokenizer, 'expected `_tokenizer` on subtokens')
  51. subevents = event[1]._tokenizer.events
  52. otherIndex = 0
  53. if (
  54. otherIndex < subevents.length &&
  55. subevents[otherIndex][1].type === types.lineEndingBlank
  56. ) {
  57. otherIndex += 2
  58. }
  59. if (
  60. otherIndex < subevents.length &&
  61. subevents[otherIndex][1].type === types.content
  62. ) {
  63. while (++otherIndex < subevents.length) {
  64. if (subevents[otherIndex][1].type === types.content) {
  65. break
  66. }
  67. if (subevents[otherIndex][1].type === types.chunkText) {
  68. subevents[otherIndex][1]._isInFirstContentOfListItem = true
  69. otherIndex++
  70. }
  71. }
  72. }
  73. }
  74. // Enter.
  75. if (event[0] === 'enter') {
  76. if (event[1].contentType) {
  77. Object.assign(jumps, subcontent(events, index))
  78. index = jumps[index]
  79. more = true
  80. }
  81. }
  82. // Exit.
  83. else if (event[1]._container) {
  84. otherIndex = index
  85. lineIndex = undefined
  86. while (otherIndex--) {
  87. otherEvent = events.get(otherIndex)
  88. if (
  89. otherEvent[1].type === types.lineEnding ||
  90. otherEvent[1].type === types.lineEndingBlank
  91. ) {
  92. if (otherEvent[0] === 'enter') {
  93. if (lineIndex) {
  94. events.get(lineIndex)[1].type = types.lineEndingBlank
  95. }
  96. otherEvent[1].type = types.lineEnding
  97. lineIndex = otherIndex
  98. }
  99. } else if (
  100. otherEvent[1].type === types.linePrefix ||
  101. otherEvent[1].type === types.listItemIndent
  102. ) {
  103. // Move past.
  104. } else {
  105. break
  106. }
  107. }
  108. if (lineIndex) {
  109. // Fix position.
  110. event[1].end = {...events.get(lineIndex)[1].start}
  111. // Switch container exit w/ line endings.
  112. parameters = events.slice(lineIndex, index)
  113. parameters.unshift(event)
  114. events.splice(lineIndex, index - lineIndex + 1, parameters)
  115. }
  116. }
  117. }
  118. // The changes to the `events` buffer must be copied back into the eventsArray
  119. splice(eventsArray, 0, Number.POSITIVE_INFINITY, events.slice(0))
  120. return !more
  121. }
  122. /**
  123. * Tokenize embedded tokens.
  124. *
  125. * @param {SpliceBuffer<Event>} events
  126. * Events.
  127. * @param {number} eventIndex
  128. * Index.
  129. * @returns {Record<string, number>}
  130. * Gaps.
  131. */
  132. function subcontent(events, eventIndex) {
  133. const token = events.get(eventIndex)[1]
  134. const context = events.get(eventIndex)[2]
  135. let startPosition = eventIndex - 1
  136. /** @type {Array<number>} */
  137. const startPositions = []
  138. assert(token.contentType, 'expected `contentType` on subtokens')
  139. let tokenizer = token._tokenizer
  140. if (!tokenizer) {
  141. tokenizer = context.parser[token.contentType](token.start)
  142. if (token._contentTypeTextTrailing) {
  143. tokenizer._contentTypeTextTrailing = true
  144. }
  145. }
  146. const childEvents = tokenizer.events
  147. /** @type {Array<[number, number]>} */
  148. const jumps = []
  149. /** @type {Record<string, number>} */
  150. const gaps = {}
  151. /** @type {Array<Chunk>} */
  152. let stream
  153. /** @type {Token | undefined} */
  154. let previous
  155. let index = -1
  156. /** @type {Token | undefined} */
  157. let current = token
  158. let adjust = 0
  159. let start = 0
  160. const breaks = [start]
  161. // Loop forward through the linked tokens to pass them in order to the
  162. // subtokenizer.
  163. while (current) {
  164. // Find the position of the event for this token.
  165. while (events.get(++startPosition)[1] !== current) {
  166. // Empty.
  167. }
  168. assert(
  169. !previous || current.previous === previous,
  170. 'expected previous to match'
  171. )
  172. assert(!previous || previous.next === current, 'expected next to match')
  173. startPositions.push(startPosition)
  174. if (!current._tokenizer) {
  175. stream = context.sliceStream(current)
  176. if (!current.next) {
  177. stream.push(codes.eof)
  178. }
  179. if (previous) {
  180. tokenizer.defineSkip(current.start)
  181. }
  182. if (current._isInFirstContentOfListItem) {
  183. tokenizer._gfmTasklistFirstContentOfListItem = true
  184. }
  185. tokenizer.write(stream)
  186. if (current._isInFirstContentOfListItem) {
  187. tokenizer._gfmTasklistFirstContentOfListItem = undefined
  188. }
  189. }
  190. // Unravel the next token.
  191. previous = current
  192. current = current.next
  193. }
  194. // Now, loop back through all events (and linked tokens), to figure out which
  195. // parts belong where.
  196. current = token
  197. while (++index < childEvents.length) {
  198. if (
  199. // Find a void token that includes a break.
  200. childEvents[index][0] === 'exit' &&
  201. childEvents[index - 1][0] === 'enter' &&
  202. childEvents[index][1].type === childEvents[index - 1][1].type &&
  203. childEvents[index][1].start.line !== childEvents[index][1].end.line
  204. ) {
  205. assert(current, 'expected a current token')
  206. start = index + 1
  207. breaks.push(start)
  208. // Help GC.
  209. current._tokenizer = undefined
  210. current.previous = undefined
  211. current = current.next
  212. }
  213. }
  214. // Help GC.
  215. tokenizer.events = []
  216. // If there’s one more token (which is the cases for lines that end in an
  217. // EOF), that’s perfect: the last point we found starts it.
  218. // If there isn’t then make sure any remaining content is added to it.
  219. if (current) {
  220. // Help GC.
  221. current._tokenizer = undefined
  222. current.previous = undefined
  223. assert(!current.next, 'expected no next token')
  224. } else {
  225. breaks.pop()
  226. }
  227. // Now splice the events from the subtokenizer into the current events,
  228. // moving back to front so that splice indices aren’t affected.
  229. index = breaks.length
  230. while (index--) {
  231. const slice = childEvents.slice(breaks[index], breaks[index + 1])
  232. const start = startPositions.pop()
  233. assert(start !== undefined, 'expected a start position when splicing')
  234. jumps.push([start, start + slice.length - 1])
  235. events.splice(start, 2, slice)
  236. }
  237. jumps.reverse()
  238. index = -1
  239. while (++index < jumps.length) {
  240. gaps[adjust + jumps[index][0]] = adjust + jumps[index][1]
  241. adjust += jumps[index][1] - jumps[index][0] - 1
  242. }
  243. return gaps
  244. }