create-tokenizer.js 15 KB

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