document.js 12 KB

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