document.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /**
  2. * @import {
  3. * Construct,
  4. * ContainerState,
  5. * InitialConstruct,
  6. * Initializer,
  7. * Point,
  8. * State,
  9. * TokenizeContext,
  10. * Tokenizer,
  11. * Token
  12. * } from 'micromark-util-types'
  13. */
  14. /**
  15. * @typedef {[Construct, ContainerState]} StackItem
  16. * Construct and its state.
  17. */
  18. import { factorySpace } from 'micromark-factory-space';
  19. import { markdownLineEnding } from 'micromark-util-character';
  20. import { splice } from 'micromark-util-chunked';
  21. /** @type {InitialConstruct} */
  22. export const document = {
  23. tokenize: initializeDocument
  24. };
  25. /** @type {Construct} */
  26. const containerConstruct = {
  27. tokenize: tokenizeContainer
  28. };
  29. /**
  30. * @this {TokenizeContext}
  31. * Self.
  32. * @type {Initializer}
  33. * Initializer.
  34. */
  35. function initializeDocument(effects) {
  36. const self = this;
  37. /** @type {Array<StackItem>} */
  38. const stack = [];
  39. let continued = 0;
  40. /** @type {TokenizeContext | undefined} */
  41. let childFlow;
  42. /** @type {Token | undefined} */
  43. let childToken;
  44. /** @type {number} */
  45. let lineStartOffset;
  46. return start;
  47. /** @type {State} */
  48. function start(code) {
  49. // First we iterate through the open blocks, starting with the root
  50. // document, and descending through last children down to the last open
  51. // block.
  52. // Each block imposes a condition that the line must satisfy if the block is
  53. // to remain open.
  54. // For example, a block quote requires a `>` character.
  55. // A paragraph requires a non-blank line.
  56. // In this phase we may match all or just some of the open blocks.
  57. // But we cannot close unmatched blocks yet, because we may have a lazy
  58. // continuation line.
  59. if (continued < stack.length) {
  60. const item = stack[continued];
  61. self.containerState = item[1];
  62. return effects.attempt(item[0].continuation, documentContinue, checkNewContainers)(code);
  63. }
  64. // Done.
  65. return checkNewContainers(code);
  66. }
  67. /** @type {State} */
  68. function documentContinue(code) {
  69. continued++;
  70. // Note: this field is called `_closeFlow` but it also closes containers.
  71. // Perhaps a good idea to rename it but it’s already used in the wild by
  72. // extensions.
  73. if (self.containerState._closeFlow) {
  74. self.containerState._closeFlow = undefined;
  75. if (childFlow) {
  76. closeFlow();
  77. }
  78. // Note: this algorithm for moving events around is similar to the
  79. // algorithm when dealing with lazy lines in `writeToChild`.
  80. const indexBeforeExits = self.events.length;
  81. let indexBeforeFlow = indexBeforeExits;
  82. /** @type {Point | undefined} */
  83. let point;
  84. // Find the flow chunk.
  85. while (indexBeforeFlow--) {
  86. if (self.events[indexBeforeFlow][0] === 'exit' && self.events[indexBeforeFlow][1].type === "chunkFlow") {
  87. point = self.events[indexBeforeFlow][1].end;
  88. break;
  89. }
  90. }
  91. exitContainers(continued);
  92. // Fix positions.
  93. let index = indexBeforeExits;
  94. while (index < self.events.length) {
  95. self.events[index][1].end = {
  96. ...point
  97. };
  98. index++;
  99. }
  100. // Inject the exits earlier (they’re still also at the end).
  101. splice(self.events, indexBeforeFlow + 1, 0, self.events.slice(indexBeforeExits));
  102. // Discard the duplicate exits.
  103. self.events.length = index;
  104. return checkNewContainers(code);
  105. }
  106. return start(code);
  107. }
  108. /** @type {State} */
  109. function checkNewContainers(code) {
  110. // Next, after consuming the continuation markers for existing blocks, we
  111. // look for new block starts (e.g. `>` for a block quote).
  112. // If we encounter a new block start, we close any blocks unmatched in
  113. // step 1 before creating the new block as a child of the last matched
  114. // block.
  115. if (continued === stack.length) {
  116. // No need to `check` whether there’s a container, of `exitContainers`
  117. // would be moot.
  118. // We can instead immediately `attempt` to parse one.
  119. if (!childFlow) {
  120. return documentContinued(code);
  121. }
  122. // If we have concrete content, such as block HTML or fenced code,
  123. // we can’t have containers “pierce” into them, so we can immediately
  124. // start.
  125. if (childFlow.currentConstruct && childFlow.currentConstruct.concrete) {
  126. return flowStart(code);
  127. }
  128. // If we do have flow, it could still be a blank line,
  129. // but we’d be interrupting it w/ a new container if there’s a current
  130. // construct.
  131. // To do: next major: remove `_gfmTableDynamicInterruptHack` (no longer
  132. // needed in micromark-extension-gfm-table@1.0.6).
  133. self.interrupt = Boolean(childFlow.currentConstruct && !childFlow._gfmTableDynamicInterruptHack);
  134. }
  135. // Check if there is a new container.
  136. self.containerState = {};
  137. return effects.check(containerConstruct, thereIsANewContainer, thereIsNoNewContainer)(code);
  138. }
  139. /** @type {State} */
  140. function thereIsANewContainer(code) {
  141. if (childFlow) closeFlow();
  142. exitContainers(continued);
  143. return documentContinued(code);
  144. }
  145. /** @type {State} */
  146. function thereIsNoNewContainer(code) {
  147. self.parser.lazy[self.now().line] = continued !== stack.length;
  148. lineStartOffset = self.now().offset;
  149. return flowStart(code);
  150. }
  151. /** @type {State} */
  152. function documentContinued(code) {
  153. // Try new containers.
  154. self.containerState = {};
  155. return effects.attempt(containerConstruct, containerContinue, flowStart)(code);
  156. }
  157. /** @type {State} */
  158. function containerContinue(code) {
  159. continued++;
  160. stack.push([self.currentConstruct, self.containerState]);
  161. // Try another.
  162. return documentContinued(code);
  163. }
  164. /** @type {State} */
  165. function flowStart(code) {
  166. if (code === null) {
  167. if (childFlow) closeFlow();
  168. exitContainers(0);
  169. effects.consume(code);
  170. return;
  171. }
  172. childFlow = childFlow || self.parser.flow(self.now());
  173. effects.enter("chunkFlow", {
  174. _tokenizer: childFlow,
  175. contentType: "flow",
  176. previous: childToken
  177. });
  178. return flowContinue(code);
  179. }
  180. /** @type {State} */
  181. function flowContinue(code) {
  182. if (code === null) {
  183. writeToChild(effects.exit("chunkFlow"), true);
  184. exitContainers(0);
  185. effects.consume(code);
  186. return;
  187. }
  188. if (markdownLineEnding(code)) {
  189. effects.consume(code);
  190. writeToChild(effects.exit("chunkFlow"));
  191. // Get ready for the next line.
  192. continued = 0;
  193. self.interrupt = undefined;
  194. return start;
  195. }
  196. effects.consume(code);
  197. return flowContinue;
  198. }
  199. /**
  200. * @param {Token} token
  201. * Token.
  202. * @param {boolean | undefined} [endOfFile]
  203. * Whether the token is at the end of the file (default: `false`).
  204. * @returns {undefined}
  205. * Nothing.
  206. */
  207. function writeToChild(token, endOfFile) {
  208. const stream = self.sliceStream(token);
  209. if (endOfFile) stream.push(null);
  210. token.previous = childToken;
  211. if (childToken) childToken.next = token;
  212. childToken = token;
  213. childFlow.defineSkip(token.start);
  214. childFlow.write(stream);
  215. // Alright, so we just added a lazy line:
  216. //
  217. // ```markdown
  218. // > a
  219. // b.
  220. //
  221. // Or:
  222. //
  223. // > ~~~c
  224. // d
  225. //
  226. // Or:
  227. //
  228. // > | e |
  229. // f
  230. // ```
  231. //
  232. // The construct in the second example (fenced code) does not accept lazy
  233. // lines, so it marked itself as done at the end of its first line, and
  234. // then the content construct parses `d`.
  235. // Most constructs in markdown match on the first line: if the first line
  236. // forms a construct, a non-lazy line can’t “unmake” it.
  237. //
  238. // The construct in the third example is potentially a GFM table, and
  239. // those are *weird*.
  240. // It *could* be a table, from the first line, if the following line
  241. // matches a condition.
  242. // In this case, that second line is lazy, which “unmakes” the first line
  243. // and turns the whole into one content block.
  244. //
  245. // We’ve now parsed the non-lazy and the lazy line, and can figure out
  246. // whether the lazy line started a new flow block.
  247. // If it did, we exit the current containers between the two flow blocks.
  248. if (self.parser.lazy[token.start.line]) {
  249. let index = childFlow.events.length;
  250. while (index--) {
  251. if (
  252. // The token starts before the line ending…
  253. childFlow.events[index][1].start.offset < lineStartOffset && (
  254. // …and either is not ended yet…
  255. !childFlow.events[index][1].end ||
  256. // …or ends after it.
  257. childFlow.events[index][1].end.offset > lineStartOffset)) {
  258. // Exit: there’s still something open, which means it’s a lazy line
  259. // part of something.
  260. return;
  261. }
  262. }
  263. // Note: this algorithm for moving events around is similar to the
  264. // algorithm when closing flow in `documentContinue`.
  265. const indexBeforeExits = self.events.length;
  266. let indexBeforeFlow = indexBeforeExits;
  267. /** @type {boolean | undefined} */
  268. let seen;
  269. /** @type {Point | undefined} */
  270. let point;
  271. // Find the previous chunk (the one before the lazy line).
  272. while (indexBeforeFlow--) {
  273. if (self.events[indexBeforeFlow][0] === 'exit' && self.events[indexBeforeFlow][1].type === "chunkFlow") {
  274. if (seen) {
  275. point = self.events[indexBeforeFlow][1].end;
  276. break;
  277. }
  278. seen = true;
  279. }
  280. }
  281. exitContainers(continued);
  282. // Fix positions.
  283. index = indexBeforeExits;
  284. while (index < self.events.length) {
  285. self.events[index][1].end = {
  286. ...point
  287. };
  288. index++;
  289. }
  290. // Inject the exits earlier (they’re still also at the end).
  291. splice(self.events, indexBeforeFlow + 1, 0, self.events.slice(indexBeforeExits));
  292. // Discard the duplicate exits.
  293. self.events.length = index;
  294. }
  295. }
  296. /**
  297. * @param {number} size
  298. * Size.
  299. * @returns {undefined}
  300. * Nothing.
  301. */
  302. function exitContainers(size) {
  303. let index = stack.length;
  304. // Exit open containers.
  305. while (index-- > size) {
  306. const entry = stack[index];
  307. self.containerState = entry[1];
  308. entry[0].exit.call(self, effects);
  309. }
  310. stack.length = size;
  311. }
  312. function closeFlow() {
  313. childFlow.write([null]);
  314. childToken = undefined;
  315. childFlow = undefined;
  316. self.containerState._closeFlow = undefined;
  317. }
  318. }
  319. /**
  320. * @this {TokenizeContext}
  321. * Context.
  322. * @type {Tokenizer}
  323. * Tokenizer.
  324. */
  325. function tokenizeContainer(effects, ok, nok) {
  326. // Always populated by defaults.
  327. return factorySpace(effects, effects.attempt(this.parser.constructs.document, ok, nok), "linePrefix", this.parser.constructs.disable.null.includes('codeIndented') ? undefined : 4);
  328. }