create-tokenizer.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /**
  2. * @import {
  3. * Chunk,
  4. * Code,
  5. * ConstructRecord,
  6. * Construct,
  7. * Effects,
  8. * InitialConstruct,
  9. * ParseContext,
  10. * Point,
  11. * State,
  12. * TokenizeContext,
  13. * Token
  14. * } from 'micromark-util-types'
  15. */
  16. /**
  17. * @callback Restore
  18. * Restore the state.
  19. * @returns {undefined}
  20. * Nothing.
  21. *
  22. * @typedef Info
  23. * Info.
  24. * @property {Restore} restore
  25. * Restore.
  26. * @property {number} from
  27. * From.
  28. *
  29. * @callback ReturnHandle
  30. * Handle a successful run.
  31. * @param {Construct} construct
  32. * Construct.
  33. * @param {Info} info
  34. * Info.
  35. * @returns {undefined}
  36. * Nothing.
  37. */
  38. import createDebug from 'debug'
  39. import {ok as assert} from 'devlop'
  40. import {markdownLineEnding} from 'micromark-util-character'
  41. import {push, splice} from 'micromark-util-chunked'
  42. import {resolveAll} from 'micromark-util-resolve-all'
  43. import {codes, values} from 'micromark-util-symbol'
  44. const debug = createDebug('micromark')
  45. /**
  46. * Create a tokenizer.
  47. * Tokenizers deal with one type of data (e.g., containers, flow, text).
  48. * The parser is the object dealing with it all.
  49. * `initialize` works like other constructs, except that only its `tokenize`
  50. * function is used, in which case it doesn’t receive an `ok` or `nok`.
  51. * `from` can be given to set the point before the first character, although
  52. * when further lines are indented, they must be set with `defineSkip`.
  53. *
  54. * @param {ParseContext} parser
  55. * Parser.
  56. * @param {InitialConstruct} initialize
  57. * Construct.
  58. * @param {Omit<Point, '_bufferIndex' | '_index'> | undefined} [from]
  59. * Point (optional).
  60. * @returns {TokenizeContext}
  61. * Context.
  62. */
  63. export function createTokenizer(parser, initialize, from) {
  64. /** @type {Point} */
  65. let point = {
  66. _bufferIndex: -1,
  67. _index: 0,
  68. line: (from && from.line) || 1,
  69. column: (from && from.column) || 1,
  70. offset: (from && from.offset) || 0
  71. }
  72. /** @type {Record<string, number>} */
  73. const columnStart = {}
  74. /** @type {Array<Construct>} */
  75. const resolveAllConstructs = []
  76. /** @type {Array<Chunk>} */
  77. let chunks = []
  78. /** @type {Array<Token>} */
  79. let stack = []
  80. /** @type {boolean | undefined} */
  81. let consumed = true
  82. /**
  83. * Tools used for tokenizing.
  84. *
  85. * @type {Effects}
  86. */
  87. const effects = {
  88. attempt: constructFactory(onsuccessfulconstruct),
  89. check: constructFactory(onsuccessfulcheck),
  90. consume,
  91. enter,
  92. exit,
  93. interrupt: constructFactory(onsuccessfulcheck, {interrupt: true})
  94. }
  95. /**
  96. * State and tools for resolving and serializing.
  97. *
  98. * @type {TokenizeContext}
  99. */
  100. const context = {
  101. code: codes.eof,
  102. containerState: {},
  103. defineSkip,
  104. events: [],
  105. now,
  106. parser,
  107. previous: codes.eof,
  108. sliceSerialize,
  109. sliceStream,
  110. write
  111. }
  112. /**
  113. * The state function.
  114. *
  115. * @type {State | undefined}
  116. */
  117. let state = initialize.tokenize.call(context, effects)
  118. /**
  119. * Track which character we expect to be consumed, to catch bugs.
  120. *
  121. * @type {Code}
  122. */
  123. let expectedCode
  124. if (initialize.resolveAll) {
  125. resolveAllConstructs.push(initialize)
  126. }
  127. return context
  128. /** @type {TokenizeContext['write']} */
  129. function write(slice) {
  130. chunks = push(chunks, slice)
  131. main()
  132. // Exit if we’re not done, resolve might change stuff.
  133. if (chunks[chunks.length - 1] !== codes.eof) {
  134. return []
  135. }
  136. addResult(initialize, 0)
  137. // Otherwise, resolve, and exit.
  138. context.events = resolveAll(resolveAllConstructs, context.events, context)
  139. return context.events
  140. }
  141. //
  142. // Tools.
  143. //
  144. /** @type {TokenizeContext['sliceSerialize']} */
  145. function sliceSerialize(token, expandTabs) {
  146. return serializeChunks(sliceStream(token), expandTabs)
  147. }
  148. /** @type {TokenizeContext['sliceStream']} */
  149. function sliceStream(token) {
  150. return sliceChunks(chunks, token)
  151. }
  152. /** @type {TokenizeContext['now']} */
  153. function now() {
  154. // This is a hot path, so we clone manually instead of `Object.assign({}, point)`
  155. const {_bufferIndex, _index, line, column, offset} = point
  156. return {_bufferIndex, _index, line, column, offset}
  157. }
  158. /** @type {TokenizeContext['defineSkip']} */
  159. function defineSkip(value) {
  160. columnStart[value.line] = value.column
  161. accountForPotentialSkip()
  162. debug('position: define skip: `%j`', point)
  163. }
  164. //
  165. // State management.
  166. //
  167. /**
  168. * Main loop (note that `_index` and `_bufferIndex` in `point` are modified by
  169. * `consume`).
  170. * Here is where we walk through the chunks, which either include strings of
  171. * several characters, or numerical character codes.
  172. * The reason to do this in a loop instead of a call is so the stack can
  173. * drain.
  174. *
  175. * @returns {undefined}
  176. * Nothing.
  177. */
  178. function main() {
  179. /** @type {number} */
  180. let chunkIndex
  181. while (point._index < chunks.length) {
  182. const chunk = chunks[point._index]
  183. // If we’re in a buffer chunk, loop through it.
  184. if (typeof chunk === 'string') {
  185. chunkIndex = point._index
  186. if (point._bufferIndex < 0) {
  187. point._bufferIndex = 0
  188. }
  189. while (
  190. point._index === chunkIndex &&
  191. point._bufferIndex < chunk.length
  192. ) {
  193. go(chunk.charCodeAt(point._bufferIndex))
  194. }
  195. } else {
  196. go(chunk)
  197. }
  198. }
  199. }
  200. /**
  201. * Deal with one code.
  202. *
  203. * @param {Code} code
  204. * Code.
  205. * @returns {undefined}
  206. * Nothing.
  207. */
  208. function go(code) {
  209. assert(consumed === true, 'expected character to be consumed')
  210. consumed = undefined
  211. debug('main: passing `%s` to %s', code, state && state.name)
  212. expectedCode = code
  213. assert(typeof state === 'function', 'expected state')
  214. state = state(code)
  215. }
  216. /** @type {Effects['consume']} */
  217. function consume(code) {
  218. assert(code === expectedCode, 'expected given code to equal expected code')
  219. debug('consume: `%s`', code)
  220. assert(
  221. consumed === undefined,
  222. 'expected code to not have been consumed: this might be because `return x(code)` instead of `return x` was used'
  223. )
  224. assert(
  225. code === null
  226. ? context.events.length === 0 ||
  227. context.events[context.events.length - 1][0] === 'exit'
  228. : context.events[context.events.length - 1][0] === 'enter',
  229. 'expected last token to be open'
  230. )
  231. if (markdownLineEnding(code)) {
  232. point.line++
  233. point.column = 1
  234. point.offset += code === codes.carriageReturnLineFeed ? 2 : 1
  235. accountForPotentialSkip()
  236. debug('position: after eol: `%j`', point)
  237. } else if (code !== codes.virtualSpace) {
  238. point.column++
  239. point.offset++
  240. }
  241. // Not in a string chunk.
  242. if (point._bufferIndex < 0) {
  243. point._index++
  244. } else {
  245. point._bufferIndex++
  246. // At end of string chunk.
  247. if (
  248. point._bufferIndex ===
  249. // Points w/ non-negative `_bufferIndex` reference
  250. // strings.
  251. /** @type {string} */ (chunks[point._index]).length
  252. ) {
  253. point._bufferIndex = -1
  254. point._index++
  255. }
  256. }
  257. // Expose the previous character.
  258. context.previous = code
  259. // Mark as consumed.
  260. consumed = true
  261. }
  262. /** @type {Effects['enter']} */
  263. function enter(type, fields) {
  264. /** @type {Token} */
  265. // @ts-expect-error Patch instead of assign required fields to help GC.
  266. const token = fields || {}
  267. token.type = type
  268. token.start = now()
  269. assert(typeof type === 'string', 'expected string type')
  270. assert(type.length > 0, 'expected non-empty string')
  271. debug('enter: `%s`', type)
  272. context.events.push(['enter', token, context])
  273. stack.push(token)
  274. return token
  275. }
  276. /** @type {Effects['exit']} */
  277. function exit(type) {
  278. assert(typeof type === 'string', 'expected string type')
  279. assert(type.length > 0, 'expected non-empty string')
  280. const token = stack.pop()
  281. assert(token, 'cannot close w/o open tokens')
  282. token.end = now()
  283. assert(type === token.type, 'expected exit token to match current token')
  284. assert(
  285. !(
  286. token.start._index === token.end._index &&
  287. token.start._bufferIndex === token.end._bufferIndex
  288. ),
  289. 'expected non-empty token (`' + type + '`)'
  290. )
  291. debug('exit: `%s`', token.type)
  292. context.events.push(['exit', token, context])
  293. return token
  294. }
  295. /**
  296. * Use results.
  297. *
  298. * @type {ReturnHandle}
  299. */
  300. function onsuccessfulconstruct(construct, info) {
  301. addResult(construct, info.from)
  302. }
  303. /**
  304. * Discard results.
  305. *
  306. * @type {ReturnHandle}
  307. */
  308. function onsuccessfulcheck(_, info) {
  309. info.restore()
  310. }
  311. /**
  312. * Factory to attempt/check/interrupt.
  313. *
  314. * @param {ReturnHandle} onreturn
  315. * Callback.
  316. * @param {{interrupt?: boolean | undefined} | undefined} [fields]
  317. * Fields.
  318. */
  319. function constructFactory(onreturn, fields) {
  320. return hook
  321. /**
  322. * Handle either an object mapping codes to constructs, a list of
  323. * constructs, or a single construct.
  324. *
  325. * @param {Array<Construct> | ConstructRecord | Construct} constructs
  326. * Constructs.
  327. * @param {State} returnState
  328. * State.
  329. * @param {State | undefined} [bogusState]
  330. * State.
  331. * @returns {State}
  332. * State.
  333. */
  334. function hook(constructs, returnState, bogusState) {
  335. /** @type {ReadonlyArray<Construct>} */
  336. let listOfConstructs
  337. /** @type {number} */
  338. let constructIndex
  339. /** @type {Construct} */
  340. let currentConstruct
  341. /** @type {Info} */
  342. let info
  343. return Array.isArray(constructs)
  344. ? /* c8 ignore next 1 */
  345. handleListOfConstructs(constructs)
  346. : 'tokenize' in constructs
  347. ? // Looks like a construct.
  348. handleListOfConstructs([/** @type {Construct} */ (constructs)])
  349. : handleMapOfConstructs(constructs)
  350. /**
  351. * Handle a list of construct.
  352. *
  353. * @param {ConstructRecord} map
  354. * Constructs.
  355. * @returns {State}
  356. * State.
  357. */
  358. function handleMapOfConstructs(map) {
  359. return start
  360. /** @type {State} */
  361. function start(code) {
  362. const left = code !== null && map[code]
  363. const all = code !== null && map.null
  364. const list = [
  365. // To do: add more extension tests.
  366. /* c8 ignore next 2 */
  367. ...(Array.isArray(left) ? left : left ? [left] : []),
  368. ...(Array.isArray(all) ? all : all ? [all] : [])
  369. ]
  370. return handleListOfConstructs(list)(code)
  371. }
  372. }
  373. /**
  374. * Handle a list of construct.
  375. *
  376. * @param {ReadonlyArray<Construct>} list
  377. * Constructs.
  378. * @returns {State}
  379. * State.
  380. */
  381. function handleListOfConstructs(list) {
  382. listOfConstructs = list
  383. constructIndex = 0
  384. if (list.length === 0) {
  385. assert(bogusState, 'expected `bogusState` to be given')
  386. return bogusState
  387. }
  388. return handleConstruct(list[constructIndex])
  389. }
  390. /**
  391. * Handle a single construct.
  392. *
  393. * @param {Construct} construct
  394. * Construct.
  395. * @returns {State}
  396. * State.
  397. */
  398. function handleConstruct(construct) {
  399. return start
  400. /** @type {State} */
  401. function start(code) {
  402. // To do: not needed to store if there is no bogus state, probably?
  403. // Currently doesn’t work because `inspect` in document does a check
  404. // w/o a bogus, which doesn’t make sense. But it does seem to help perf
  405. // by not storing.
  406. info = store()
  407. currentConstruct = construct
  408. if (!construct.partial) {
  409. context.currentConstruct = construct
  410. }
  411. // Always populated by defaults.
  412. assert(
  413. context.parser.constructs.disable.null,
  414. 'expected `disable.null` to be populated'
  415. )
  416. if (
  417. construct.name &&
  418. context.parser.constructs.disable.null.includes(construct.name)
  419. ) {
  420. return nok(code)
  421. }
  422. return construct.tokenize.call(
  423. // If we do have fields, create an object w/ `context` as its
  424. // prototype.
  425. // This allows a “live binding”, which is needed for `interrupt`.
  426. fields ? Object.assign(Object.create(context), fields) : context,
  427. effects,
  428. ok,
  429. nok
  430. )(code)
  431. }
  432. }
  433. /** @type {State} */
  434. function ok(code) {
  435. assert(code === expectedCode, 'expected code')
  436. consumed = true
  437. onreturn(currentConstruct, info)
  438. return returnState
  439. }
  440. /** @type {State} */
  441. function nok(code) {
  442. assert(code === expectedCode, 'expected code')
  443. consumed = true
  444. info.restore()
  445. if (++constructIndex < listOfConstructs.length) {
  446. return handleConstruct(listOfConstructs[constructIndex])
  447. }
  448. return bogusState
  449. }
  450. }
  451. }
  452. /**
  453. * @param {Construct} construct
  454. * Construct.
  455. * @param {number} from
  456. * From.
  457. * @returns {undefined}
  458. * Nothing.
  459. */
  460. function addResult(construct, from) {
  461. if (construct.resolveAll && !resolveAllConstructs.includes(construct)) {
  462. resolveAllConstructs.push(construct)
  463. }
  464. if (construct.resolve) {
  465. splice(
  466. context.events,
  467. from,
  468. context.events.length - from,
  469. construct.resolve(context.events.slice(from), context)
  470. )
  471. }
  472. if (construct.resolveTo) {
  473. context.events = construct.resolveTo(context.events, context)
  474. }
  475. assert(
  476. construct.partial ||
  477. context.events.length === 0 ||
  478. context.events[context.events.length - 1][0] === 'exit',
  479. 'expected last token to end'
  480. )
  481. }
  482. /**
  483. * Store state.
  484. *
  485. * @returns {Info}
  486. * Info.
  487. */
  488. function store() {
  489. const startPoint = now()
  490. const startPrevious = context.previous
  491. const startCurrentConstruct = context.currentConstruct
  492. const startEventsIndex = context.events.length
  493. const startStack = Array.from(stack)
  494. return {from: startEventsIndex, restore}
  495. /**
  496. * Restore state.
  497. *
  498. * @returns {undefined}
  499. * Nothing.
  500. */
  501. function restore() {
  502. point = startPoint
  503. context.previous = startPrevious
  504. context.currentConstruct = startCurrentConstruct
  505. context.events.length = startEventsIndex
  506. stack = startStack
  507. accountForPotentialSkip()
  508. debug('position: restore: `%j`', point)
  509. }
  510. }
  511. /**
  512. * Move the current point a bit forward in the line when it’s on a column
  513. * skip.
  514. *
  515. * @returns {undefined}
  516. * Nothing.
  517. */
  518. function accountForPotentialSkip() {
  519. if (point.line in columnStart && point.column < 2) {
  520. point.column = columnStart[point.line]
  521. point.offset += columnStart[point.line] - 1
  522. }
  523. }
  524. }
  525. /**
  526. * Get the chunks from a slice of chunks in the range of a token.
  527. *
  528. * @param {ReadonlyArray<Chunk>} chunks
  529. * Chunks.
  530. * @param {Pick<Token, 'end' | 'start'>} token
  531. * Token.
  532. * @returns {Array<Chunk>}
  533. * Chunks.
  534. */
  535. function sliceChunks(chunks, token) {
  536. const startIndex = token.start._index
  537. const startBufferIndex = token.start._bufferIndex
  538. const endIndex = token.end._index
  539. const endBufferIndex = token.end._bufferIndex
  540. /** @type {Array<Chunk>} */
  541. let view
  542. if (startIndex === endIndex) {
  543. assert(endBufferIndex > -1, 'expected non-negative end buffer index')
  544. assert(startBufferIndex > -1, 'expected non-negative start buffer index')
  545. // @ts-expect-error `_bufferIndex` is used on string chunks.
  546. view = [chunks[startIndex].slice(startBufferIndex, endBufferIndex)]
  547. } else {
  548. view = chunks.slice(startIndex, endIndex)
  549. if (startBufferIndex > -1) {
  550. const head = view[0]
  551. if (typeof head === 'string') {
  552. view[0] = head.slice(startBufferIndex)
  553. /* c8 ignore next 4 -- used to be used, no longer */
  554. } else {
  555. assert(startBufferIndex === 0, 'expected `startBufferIndex` to be `0`')
  556. view.shift()
  557. }
  558. }
  559. if (endBufferIndex > 0) {
  560. // @ts-expect-error `_bufferIndex` is used on string chunks.
  561. view.push(chunks[endIndex].slice(0, endBufferIndex))
  562. }
  563. }
  564. return view
  565. }
  566. /**
  567. * Get the string value of a slice of chunks.
  568. *
  569. * @param {ReadonlyArray<Chunk>} chunks
  570. * Chunks.
  571. * @param {boolean | undefined} [expandTabs=false]
  572. * Whether to expand tabs (default: `false`).
  573. * @returns {string}
  574. * Result.
  575. */
  576. function serializeChunks(chunks, expandTabs) {
  577. let index = -1
  578. /** @type {Array<string>} */
  579. const result = []
  580. /** @type {boolean | undefined} */
  581. let atTab
  582. while (++index < chunks.length) {
  583. const chunk = chunks[index]
  584. /** @type {string} */
  585. let value
  586. if (typeof chunk === 'string') {
  587. value = chunk
  588. } else
  589. switch (chunk) {
  590. case codes.carriageReturn: {
  591. value = values.cr
  592. break
  593. }
  594. case codes.lineFeed: {
  595. value = values.lf
  596. break
  597. }
  598. case codes.carriageReturnLineFeed: {
  599. value = values.cr + values.lf
  600. break
  601. }
  602. case codes.horizontalTab: {
  603. value = expandTabs ? values.space : values.ht
  604. break
  605. }
  606. case codes.virtualSpace: {
  607. if (!expandTabs && atTab) continue
  608. value = values.space
  609. break
  610. }
  611. default: {
  612. assert(typeof chunk === 'number', 'expected number')
  613. // Currently only replacement character.
  614. value = String.fromCharCode(chunk)
  615. }
  616. }
  617. atTab = chunk === codes.horizontalTab
  618. result.push(value)
  619. }
  620. return result.join('')
  621. }