index.js 6.7 KB

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