parser.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954
  1. import { tokenType } from './cst.js';
  2. import { Lexer } from './lexer.js';
  3. function includesToken(list, type) {
  4. for (let i = 0; i < list.length; ++i)
  5. if (list[i].type === type)
  6. return true;
  7. return false;
  8. }
  9. function findNonEmptyIndex(list) {
  10. for (let i = 0; i < list.length; ++i) {
  11. switch (list[i].type) {
  12. case 'space':
  13. case 'comment':
  14. case 'newline':
  15. break;
  16. default:
  17. return i;
  18. }
  19. }
  20. return -1;
  21. }
  22. function isFlowToken(token) {
  23. switch (token?.type) {
  24. case 'alias':
  25. case 'scalar':
  26. case 'single-quoted-scalar':
  27. case 'double-quoted-scalar':
  28. case 'flow-collection':
  29. return true;
  30. default:
  31. return false;
  32. }
  33. }
  34. function getPrevProps(parent) {
  35. switch (parent.type) {
  36. case 'document':
  37. return parent.start;
  38. case 'block-map': {
  39. const it = parent.items[parent.items.length - 1];
  40. return it.sep ?? it.start;
  41. }
  42. case 'block-seq':
  43. return parent.items[parent.items.length - 1].start;
  44. /* istanbul ignore next should not happen */
  45. default:
  46. return [];
  47. }
  48. }
  49. /** Note: May modify input array */
  50. function getFirstKeyStartProps(prev) {
  51. if (prev.length === 0)
  52. return [];
  53. let i = prev.length;
  54. loop: while (--i >= 0) {
  55. switch (prev[i].type) {
  56. case 'doc-start':
  57. case 'explicit-key-ind':
  58. case 'map-value-ind':
  59. case 'seq-item-ind':
  60. case 'newline':
  61. break loop;
  62. }
  63. }
  64. while (prev[++i]?.type === 'space') {
  65. /* loop */
  66. }
  67. return prev.splice(i, prev.length);
  68. }
  69. function fixFlowSeqItems(fc) {
  70. if (fc.start.type === 'flow-seq-start') {
  71. for (const it of fc.items) {
  72. if (it.sep &&
  73. !it.value &&
  74. !includesToken(it.start, 'explicit-key-ind') &&
  75. !includesToken(it.sep, 'map-value-ind')) {
  76. if (it.key)
  77. it.value = it.key;
  78. delete it.key;
  79. if (isFlowToken(it.value)) {
  80. if (it.value.end)
  81. Array.prototype.push.apply(it.value.end, it.sep);
  82. else
  83. it.value.end = it.sep;
  84. }
  85. else
  86. Array.prototype.push.apply(it.start, it.sep);
  87. delete it.sep;
  88. }
  89. }
  90. }
  91. }
  92. /**
  93. * A YAML concrete syntax tree (CST) parser
  94. *
  95. * ```ts
  96. * const src: string = ...
  97. * for (const token of new Parser().parse(src)) {
  98. * // token: Token
  99. * }
  100. * ```
  101. *
  102. * To use the parser with a user-provided lexer:
  103. *
  104. * ```ts
  105. * function* parse(source: string, lexer: Lexer) {
  106. * const parser = new Parser()
  107. * for (const lexeme of lexer.lex(source))
  108. * yield* parser.next(lexeme)
  109. * yield* parser.end()
  110. * }
  111. *
  112. * const src: string = ...
  113. * const lexer = new Lexer()
  114. * for (const token of parse(src, lexer)) {
  115. * // token: Token
  116. * }
  117. * ```
  118. */
  119. class Parser {
  120. /**
  121. * @param onNewLine - If defined, called separately with the start position of
  122. * each new line (in `parse()`, including the start of input).
  123. */
  124. constructor(onNewLine) {
  125. /** If true, space and sequence indicators count as indentation */
  126. this.atNewLine = true;
  127. /** If true, next token is a scalar value */
  128. this.atScalar = false;
  129. /** Current indentation level */
  130. this.indent = 0;
  131. /** Current offset since the start of parsing */
  132. this.offset = 0;
  133. /** On the same line with a block map key */
  134. this.onKeyLine = false;
  135. /** Top indicates the node that's currently being built */
  136. this.stack = [];
  137. /** The source of the current token, set in parse() */
  138. this.source = '';
  139. /** The type of the current token, set in parse() */
  140. this.type = '';
  141. // Must be defined after `next()`
  142. this.lexer = new Lexer();
  143. this.onNewLine = onNewLine;
  144. }
  145. /**
  146. * Parse `source` as a YAML stream.
  147. * If `incomplete`, a part of the last line may be left as a buffer for the next call.
  148. *
  149. * Errors are not thrown, but yielded as `{ type: 'error', message }` tokens.
  150. *
  151. * @returns A generator of tokens representing each directive, document, and other structure.
  152. */
  153. *parse(source, incomplete = false) {
  154. if (this.onNewLine && this.offset === 0)
  155. this.onNewLine(0);
  156. for (const lexeme of this.lexer.lex(source, incomplete))
  157. yield* this.next(lexeme);
  158. if (!incomplete)
  159. yield* this.end();
  160. }
  161. /**
  162. * Advance the parser by the `source` of one lexical token.
  163. */
  164. *next(source) {
  165. this.source = source;
  166. if (this.atScalar) {
  167. this.atScalar = false;
  168. yield* this.step();
  169. this.offset += source.length;
  170. return;
  171. }
  172. const type = tokenType(source);
  173. if (!type) {
  174. const message = `Not a YAML token: ${source}`;
  175. yield* this.pop({ type: 'error', offset: this.offset, message, source });
  176. this.offset += source.length;
  177. }
  178. else if (type === 'scalar') {
  179. this.atNewLine = false;
  180. this.atScalar = true;
  181. this.type = 'scalar';
  182. }
  183. else {
  184. this.type = type;
  185. yield* this.step();
  186. switch (type) {
  187. case 'newline':
  188. this.atNewLine = true;
  189. this.indent = 0;
  190. if (this.onNewLine)
  191. this.onNewLine(this.offset + source.length);
  192. break;
  193. case 'space':
  194. if (this.atNewLine && source[0] === ' ')
  195. this.indent += source.length;
  196. break;
  197. case 'explicit-key-ind':
  198. case 'map-value-ind':
  199. case 'seq-item-ind':
  200. if (this.atNewLine)
  201. this.indent += source.length;
  202. break;
  203. case 'doc-mode':
  204. case 'flow-error-end':
  205. return;
  206. default:
  207. this.atNewLine = false;
  208. }
  209. this.offset += source.length;
  210. }
  211. }
  212. /** Call at end of input to push out any remaining constructions */
  213. *end() {
  214. while (this.stack.length > 0)
  215. yield* this.pop();
  216. }
  217. get sourceToken() {
  218. const st = {
  219. type: this.type,
  220. offset: this.offset,
  221. indent: this.indent,
  222. source: this.source
  223. };
  224. return st;
  225. }
  226. *step() {
  227. const top = this.peek(1);
  228. if (this.type === 'doc-end' && (!top || top.type !== 'doc-end')) {
  229. while (this.stack.length > 0)
  230. yield* this.pop();
  231. this.stack.push({
  232. type: 'doc-end',
  233. offset: this.offset,
  234. source: this.source
  235. });
  236. return;
  237. }
  238. if (!top)
  239. return yield* this.stream();
  240. switch (top.type) {
  241. case 'document':
  242. return yield* this.document(top);
  243. case 'alias':
  244. case 'scalar':
  245. case 'single-quoted-scalar':
  246. case 'double-quoted-scalar':
  247. return yield* this.scalar(top);
  248. case 'block-scalar':
  249. return yield* this.blockScalar(top);
  250. case 'block-map':
  251. return yield* this.blockMap(top);
  252. case 'block-seq':
  253. return yield* this.blockSequence(top);
  254. case 'flow-collection':
  255. return yield* this.flowCollection(top);
  256. case 'doc-end':
  257. return yield* this.documentEnd(top);
  258. }
  259. /* istanbul ignore next should not happen */
  260. yield* this.pop();
  261. }
  262. peek(n) {
  263. return this.stack[this.stack.length - n];
  264. }
  265. *pop(error) {
  266. const token = error ?? this.stack.pop();
  267. /* istanbul ignore if should not happen */
  268. if (!token) {
  269. const message = 'Tried to pop an empty stack';
  270. yield { type: 'error', offset: this.offset, source: '', message };
  271. }
  272. else if (this.stack.length === 0) {
  273. yield token;
  274. }
  275. else {
  276. const top = this.peek(1);
  277. if (token.type === 'block-scalar') {
  278. // Block scalars use their parent rather than header indent
  279. token.indent = 'indent' in top ? top.indent : 0;
  280. }
  281. else if (token.type === 'flow-collection' && top.type === 'document') {
  282. // Ignore all indent for top-level flow collections
  283. token.indent = 0;
  284. }
  285. if (token.type === 'flow-collection')
  286. fixFlowSeqItems(token);
  287. switch (top.type) {
  288. case 'document':
  289. top.value = token;
  290. break;
  291. case 'block-scalar':
  292. top.props.push(token); // error
  293. break;
  294. case 'block-map': {
  295. const it = top.items[top.items.length - 1];
  296. if (it.value) {
  297. top.items.push({ start: [], key: token, sep: [] });
  298. this.onKeyLine = true;
  299. return;
  300. }
  301. else if (it.sep) {
  302. it.value = token;
  303. }
  304. else {
  305. Object.assign(it, { key: token, sep: [] });
  306. this.onKeyLine = !it.explicitKey;
  307. return;
  308. }
  309. break;
  310. }
  311. case 'block-seq': {
  312. const it = top.items[top.items.length - 1];
  313. if (it.value)
  314. top.items.push({ start: [], value: token });
  315. else
  316. it.value = token;
  317. break;
  318. }
  319. case 'flow-collection': {
  320. const it = top.items[top.items.length - 1];
  321. if (!it || it.value)
  322. top.items.push({ start: [], key: token, sep: [] });
  323. else if (it.sep)
  324. it.value = token;
  325. else
  326. Object.assign(it, { key: token, sep: [] });
  327. return;
  328. }
  329. /* istanbul ignore next should not happen */
  330. default:
  331. yield* this.pop();
  332. yield* this.pop(token);
  333. }
  334. if ((top.type === 'document' ||
  335. top.type === 'block-map' ||
  336. top.type === 'block-seq') &&
  337. (token.type === 'block-map' || token.type === 'block-seq')) {
  338. const last = token.items[token.items.length - 1];
  339. if (last &&
  340. !last.sep &&
  341. !last.value &&
  342. last.start.length > 0 &&
  343. findNonEmptyIndex(last.start) === -1 &&
  344. (token.indent === 0 ||
  345. last.start.every(st => st.type !== 'comment' || st.indent < token.indent))) {
  346. if (top.type === 'document')
  347. top.end = last.start;
  348. else
  349. top.items.push({ start: last.start });
  350. token.items.splice(-1, 1);
  351. }
  352. }
  353. }
  354. }
  355. *stream() {
  356. switch (this.type) {
  357. case 'directive-line':
  358. yield { type: 'directive', offset: this.offset, source: this.source };
  359. return;
  360. case 'byte-order-mark':
  361. case 'space':
  362. case 'comment':
  363. case 'newline':
  364. yield this.sourceToken;
  365. return;
  366. case 'doc-mode':
  367. case 'doc-start': {
  368. const doc = {
  369. type: 'document',
  370. offset: this.offset,
  371. start: []
  372. };
  373. if (this.type === 'doc-start')
  374. doc.start.push(this.sourceToken);
  375. this.stack.push(doc);
  376. return;
  377. }
  378. }
  379. yield {
  380. type: 'error',
  381. offset: this.offset,
  382. message: `Unexpected ${this.type} token in YAML stream`,
  383. source: this.source
  384. };
  385. }
  386. *document(doc) {
  387. if (doc.value)
  388. return yield* this.lineEnd(doc);
  389. switch (this.type) {
  390. case 'doc-start': {
  391. if (findNonEmptyIndex(doc.start) !== -1) {
  392. yield* this.pop();
  393. yield* this.step();
  394. }
  395. else
  396. doc.start.push(this.sourceToken);
  397. return;
  398. }
  399. case 'anchor':
  400. case 'tag':
  401. case 'space':
  402. case 'comment':
  403. case 'newline':
  404. doc.start.push(this.sourceToken);
  405. return;
  406. }
  407. const bv = this.startBlockValue(doc);
  408. if (bv)
  409. this.stack.push(bv);
  410. else {
  411. yield {
  412. type: 'error',
  413. offset: this.offset,
  414. message: `Unexpected ${this.type} token in YAML document`,
  415. source: this.source
  416. };
  417. }
  418. }
  419. *scalar(scalar) {
  420. if (this.type === 'map-value-ind') {
  421. const prev = getPrevProps(this.peek(2));
  422. const start = getFirstKeyStartProps(prev);
  423. let sep;
  424. if (scalar.end) {
  425. sep = scalar.end;
  426. sep.push(this.sourceToken);
  427. delete scalar.end;
  428. }
  429. else
  430. sep = [this.sourceToken];
  431. const map = {
  432. type: 'block-map',
  433. offset: scalar.offset,
  434. indent: scalar.indent,
  435. items: [{ start, key: scalar, sep }]
  436. };
  437. this.onKeyLine = true;
  438. this.stack[this.stack.length - 1] = map;
  439. }
  440. else
  441. yield* this.lineEnd(scalar);
  442. }
  443. *blockScalar(scalar) {
  444. switch (this.type) {
  445. case 'space':
  446. case 'comment':
  447. case 'newline':
  448. scalar.props.push(this.sourceToken);
  449. return;
  450. case 'scalar':
  451. scalar.source = this.source;
  452. // block-scalar source includes trailing newline
  453. this.atNewLine = true;
  454. this.indent = 0;
  455. if (this.onNewLine) {
  456. let nl = this.source.indexOf('\n') + 1;
  457. while (nl !== 0) {
  458. this.onNewLine(this.offset + nl);
  459. nl = this.source.indexOf('\n', nl) + 1;
  460. }
  461. }
  462. yield* this.pop();
  463. break;
  464. /* istanbul ignore next should not happen */
  465. default:
  466. yield* this.pop();
  467. yield* this.step();
  468. }
  469. }
  470. *blockMap(map) {
  471. const it = map.items[map.items.length - 1];
  472. // it.sep is true-ish if pair already has key or : separator
  473. switch (this.type) {
  474. case 'newline':
  475. this.onKeyLine = false;
  476. if (it.value) {
  477. const end = 'end' in it.value ? it.value.end : undefined;
  478. const last = Array.isArray(end) ? end[end.length - 1] : undefined;
  479. if (last?.type === 'comment')
  480. end?.push(this.sourceToken);
  481. else
  482. map.items.push({ start: [this.sourceToken] });
  483. }
  484. else if (it.sep) {
  485. it.sep.push(this.sourceToken);
  486. }
  487. else {
  488. it.start.push(this.sourceToken);
  489. }
  490. return;
  491. case 'space':
  492. case 'comment':
  493. if (it.value) {
  494. map.items.push({ start: [this.sourceToken] });
  495. }
  496. else if (it.sep) {
  497. it.sep.push(this.sourceToken);
  498. }
  499. else {
  500. if (this.atIndentedComment(it.start, map.indent)) {
  501. const prev = map.items[map.items.length - 2];
  502. const end = prev?.value?.end;
  503. if (Array.isArray(end)) {
  504. Array.prototype.push.apply(end, it.start);
  505. end.push(this.sourceToken);
  506. map.items.pop();
  507. return;
  508. }
  509. }
  510. it.start.push(this.sourceToken);
  511. }
  512. return;
  513. }
  514. if (this.indent >= map.indent) {
  515. const atMapIndent = !this.onKeyLine && this.indent === map.indent;
  516. const atNextItem = atMapIndent &&
  517. (it.sep || it.explicitKey) &&
  518. this.type !== 'seq-item-ind';
  519. // For empty nodes, assign newline-separated not indented empty tokens to following node
  520. let start = [];
  521. if (atNextItem && it.sep && !it.value) {
  522. const nl = [];
  523. for (let i = 0; i < it.sep.length; ++i) {
  524. const st = it.sep[i];
  525. switch (st.type) {
  526. case 'newline':
  527. nl.push(i);
  528. break;
  529. case 'space':
  530. break;
  531. case 'comment':
  532. if (st.indent > map.indent)
  533. nl.length = 0;
  534. break;
  535. default:
  536. nl.length = 0;
  537. }
  538. }
  539. if (nl.length >= 2)
  540. start = it.sep.splice(nl[1]);
  541. }
  542. switch (this.type) {
  543. case 'anchor':
  544. case 'tag':
  545. if (atNextItem || it.value) {
  546. start.push(this.sourceToken);
  547. map.items.push({ start });
  548. this.onKeyLine = true;
  549. }
  550. else if (it.sep) {
  551. it.sep.push(this.sourceToken);
  552. }
  553. else {
  554. it.start.push(this.sourceToken);
  555. }
  556. return;
  557. case 'explicit-key-ind':
  558. if (!it.sep && !it.explicitKey) {
  559. it.start.push(this.sourceToken);
  560. it.explicitKey = true;
  561. }
  562. else if (atNextItem || it.value) {
  563. start.push(this.sourceToken);
  564. map.items.push({ start, explicitKey: true });
  565. }
  566. else {
  567. this.stack.push({
  568. type: 'block-map',
  569. offset: this.offset,
  570. indent: this.indent,
  571. items: [{ start: [this.sourceToken], explicitKey: true }]
  572. });
  573. }
  574. this.onKeyLine = true;
  575. return;
  576. case 'map-value-ind':
  577. if (it.explicitKey) {
  578. if (!it.sep) {
  579. if (includesToken(it.start, 'newline')) {
  580. Object.assign(it, { key: null, sep: [this.sourceToken] });
  581. }
  582. else {
  583. const start = getFirstKeyStartProps(it.start);
  584. this.stack.push({
  585. type: 'block-map',
  586. offset: this.offset,
  587. indent: this.indent,
  588. items: [{ start, key: null, sep: [this.sourceToken] }]
  589. });
  590. }
  591. }
  592. else if (it.value) {
  593. map.items.push({ start: [], key: null, sep: [this.sourceToken] });
  594. }
  595. else if (includesToken(it.sep, 'map-value-ind')) {
  596. this.stack.push({
  597. type: 'block-map',
  598. offset: this.offset,
  599. indent: this.indent,
  600. items: [{ start, key: null, sep: [this.sourceToken] }]
  601. });
  602. }
  603. else if (isFlowToken(it.key) &&
  604. !includesToken(it.sep, 'newline')) {
  605. const start = getFirstKeyStartProps(it.start);
  606. const key = it.key;
  607. const sep = it.sep;
  608. sep.push(this.sourceToken);
  609. // @ts-expect-error type guard is wrong here
  610. delete it.key;
  611. // @ts-expect-error type guard is wrong here
  612. delete it.sep;
  613. this.stack.push({
  614. type: 'block-map',
  615. offset: this.offset,
  616. indent: this.indent,
  617. items: [{ start, key, sep }]
  618. });
  619. }
  620. else if (start.length > 0) {
  621. // Not actually at next item
  622. it.sep = it.sep.concat(start, this.sourceToken);
  623. }
  624. else {
  625. it.sep.push(this.sourceToken);
  626. }
  627. }
  628. else {
  629. if (!it.sep) {
  630. Object.assign(it, { key: null, sep: [this.sourceToken] });
  631. }
  632. else if (it.value || atNextItem) {
  633. map.items.push({ start, key: null, sep: [this.sourceToken] });
  634. }
  635. else if (includesToken(it.sep, 'map-value-ind')) {
  636. this.stack.push({
  637. type: 'block-map',
  638. offset: this.offset,
  639. indent: this.indent,
  640. items: [{ start: [], key: null, sep: [this.sourceToken] }]
  641. });
  642. }
  643. else {
  644. it.sep.push(this.sourceToken);
  645. }
  646. }
  647. this.onKeyLine = true;
  648. return;
  649. case 'alias':
  650. case 'scalar':
  651. case 'single-quoted-scalar':
  652. case 'double-quoted-scalar': {
  653. const fs = this.flowScalar(this.type);
  654. if (atNextItem || it.value) {
  655. map.items.push({ start, key: fs, sep: [] });
  656. this.onKeyLine = true;
  657. }
  658. else if (it.sep) {
  659. this.stack.push(fs);
  660. }
  661. else {
  662. Object.assign(it, { key: fs, sep: [] });
  663. this.onKeyLine = true;
  664. }
  665. return;
  666. }
  667. default: {
  668. const bv = this.startBlockValue(map);
  669. if (bv) {
  670. if (atMapIndent && bv.type !== 'block-seq') {
  671. map.items.push({ start });
  672. }
  673. this.stack.push(bv);
  674. return;
  675. }
  676. }
  677. }
  678. }
  679. yield* this.pop();
  680. yield* this.step();
  681. }
  682. *blockSequence(seq) {
  683. const it = seq.items[seq.items.length - 1];
  684. switch (this.type) {
  685. case 'newline':
  686. if (it.value) {
  687. const end = 'end' in it.value ? it.value.end : undefined;
  688. const last = Array.isArray(end) ? end[end.length - 1] : undefined;
  689. if (last?.type === 'comment')
  690. end?.push(this.sourceToken);
  691. else
  692. seq.items.push({ start: [this.sourceToken] });
  693. }
  694. else
  695. it.start.push(this.sourceToken);
  696. return;
  697. case 'space':
  698. case 'comment':
  699. if (it.value)
  700. seq.items.push({ start: [this.sourceToken] });
  701. else {
  702. if (this.atIndentedComment(it.start, seq.indent)) {
  703. const prev = seq.items[seq.items.length - 2];
  704. const end = prev?.value?.end;
  705. if (Array.isArray(end)) {
  706. Array.prototype.push.apply(end, it.start);
  707. end.push(this.sourceToken);
  708. seq.items.pop();
  709. return;
  710. }
  711. }
  712. it.start.push(this.sourceToken);
  713. }
  714. return;
  715. case 'anchor':
  716. case 'tag':
  717. if (it.value || this.indent <= seq.indent)
  718. break;
  719. it.start.push(this.sourceToken);
  720. return;
  721. case 'seq-item-ind':
  722. if (this.indent !== seq.indent)
  723. break;
  724. if (it.value || includesToken(it.start, 'seq-item-ind'))
  725. seq.items.push({ start: [this.sourceToken] });
  726. else
  727. it.start.push(this.sourceToken);
  728. return;
  729. }
  730. if (this.indent > seq.indent) {
  731. const bv = this.startBlockValue(seq);
  732. if (bv) {
  733. this.stack.push(bv);
  734. return;
  735. }
  736. }
  737. yield* this.pop();
  738. yield* this.step();
  739. }
  740. *flowCollection(fc) {
  741. const it = fc.items[fc.items.length - 1];
  742. if (this.type === 'flow-error-end') {
  743. let top;
  744. do {
  745. yield* this.pop();
  746. top = this.peek(1);
  747. } while (top && top.type === 'flow-collection');
  748. }
  749. else if (fc.end.length === 0) {
  750. switch (this.type) {
  751. case 'comma':
  752. case 'explicit-key-ind':
  753. if (!it || it.sep)
  754. fc.items.push({ start: [this.sourceToken] });
  755. else
  756. it.start.push(this.sourceToken);
  757. return;
  758. case 'map-value-ind':
  759. if (!it || it.value)
  760. fc.items.push({ start: [], key: null, sep: [this.sourceToken] });
  761. else if (it.sep)
  762. it.sep.push(this.sourceToken);
  763. else
  764. Object.assign(it, { key: null, sep: [this.sourceToken] });
  765. return;
  766. case 'space':
  767. case 'comment':
  768. case 'newline':
  769. case 'anchor':
  770. case 'tag':
  771. if (!it || it.value)
  772. fc.items.push({ start: [this.sourceToken] });
  773. else if (it.sep)
  774. it.sep.push(this.sourceToken);
  775. else
  776. it.start.push(this.sourceToken);
  777. return;
  778. case 'alias':
  779. case 'scalar':
  780. case 'single-quoted-scalar':
  781. case 'double-quoted-scalar': {
  782. const fs = this.flowScalar(this.type);
  783. if (!it || it.value)
  784. fc.items.push({ start: [], key: fs, sep: [] });
  785. else if (it.sep)
  786. this.stack.push(fs);
  787. else
  788. Object.assign(it, { key: fs, sep: [] });
  789. return;
  790. }
  791. case 'flow-map-end':
  792. case 'flow-seq-end':
  793. fc.end.push(this.sourceToken);
  794. return;
  795. }
  796. const bv = this.startBlockValue(fc);
  797. /* istanbul ignore else should not happen */
  798. if (bv)
  799. this.stack.push(bv);
  800. else {
  801. yield* this.pop();
  802. yield* this.step();
  803. }
  804. }
  805. else {
  806. const parent = this.peek(2);
  807. if (parent.type === 'block-map' &&
  808. ((this.type === 'map-value-ind' && parent.indent === fc.indent) ||
  809. (this.type === 'newline' &&
  810. !parent.items[parent.items.length - 1].sep))) {
  811. yield* this.pop();
  812. yield* this.step();
  813. }
  814. else if (this.type === 'map-value-ind' &&
  815. parent.type !== 'flow-collection') {
  816. const prev = getPrevProps(parent);
  817. const start = getFirstKeyStartProps(prev);
  818. fixFlowSeqItems(fc);
  819. const sep = fc.end.splice(1, fc.end.length);
  820. sep.push(this.sourceToken);
  821. const map = {
  822. type: 'block-map',
  823. offset: fc.offset,
  824. indent: fc.indent,
  825. items: [{ start, key: fc, sep }]
  826. };
  827. this.onKeyLine = true;
  828. this.stack[this.stack.length - 1] = map;
  829. }
  830. else {
  831. yield* this.lineEnd(fc);
  832. }
  833. }
  834. }
  835. flowScalar(type) {
  836. if (this.onNewLine) {
  837. let nl = this.source.indexOf('\n') + 1;
  838. while (nl !== 0) {
  839. this.onNewLine(this.offset + nl);
  840. nl = this.source.indexOf('\n', nl) + 1;
  841. }
  842. }
  843. return {
  844. type,
  845. offset: this.offset,
  846. indent: this.indent,
  847. source: this.source
  848. };
  849. }
  850. startBlockValue(parent) {
  851. switch (this.type) {
  852. case 'alias':
  853. case 'scalar':
  854. case 'single-quoted-scalar':
  855. case 'double-quoted-scalar':
  856. return this.flowScalar(this.type);
  857. case 'block-scalar-header':
  858. return {
  859. type: 'block-scalar',
  860. offset: this.offset,
  861. indent: this.indent,
  862. props: [this.sourceToken],
  863. source: ''
  864. };
  865. case 'flow-map-start':
  866. case 'flow-seq-start':
  867. return {
  868. type: 'flow-collection',
  869. offset: this.offset,
  870. indent: this.indent,
  871. start: this.sourceToken,
  872. items: [],
  873. end: []
  874. };
  875. case 'seq-item-ind':
  876. return {
  877. type: 'block-seq',
  878. offset: this.offset,
  879. indent: this.indent,
  880. items: [{ start: [this.sourceToken] }]
  881. };
  882. case 'explicit-key-ind': {
  883. this.onKeyLine = true;
  884. const prev = getPrevProps(parent);
  885. const start = getFirstKeyStartProps(prev);
  886. start.push(this.sourceToken);
  887. return {
  888. type: 'block-map',
  889. offset: this.offset,
  890. indent: this.indent,
  891. items: [{ start, explicitKey: true }]
  892. };
  893. }
  894. case 'map-value-ind': {
  895. this.onKeyLine = true;
  896. const prev = getPrevProps(parent);
  897. const start = getFirstKeyStartProps(prev);
  898. return {
  899. type: 'block-map',
  900. offset: this.offset,
  901. indent: this.indent,
  902. items: [{ start, key: null, sep: [this.sourceToken] }]
  903. };
  904. }
  905. }
  906. return null;
  907. }
  908. atIndentedComment(start, indent) {
  909. if (this.type !== 'comment')
  910. return false;
  911. if (this.indent <= indent)
  912. return false;
  913. return start.every(st => st.type === 'newline' || st.type === 'space');
  914. }
  915. *documentEnd(docEnd) {
  916. if (this.type !== 'doc-mode') {
  917. if (docEnd.end)
  918. docEnd.end.push(this.sourceToken);
  919. else
  920. docEnd.end = [this.sourceToken];
  921. if (this.type === 'newline')
  922. yield* this.pop();
  923. }
  924. }
  925. *lineEnd(token) {
  926. switch (this.type) {
  927. case 'comma':
  928. case 'doc-start':
  929. case 'doc-end':
  930. case 'flow-seq-end':
  931. case 'flow-map-end':
  932. case 'map-value-ind':
  933. yield* this.pop();
  934. yield* this.step();
  935. break;
  936. case 'newline':
  937. this.onKeyLine = false;
  938. // fallthrough
  939. case 'space':
  940. case 'comment':
  941. default:
  942. // all other values are errors
  943. if (token.end)
  944. token.end.push(this.sourceToken);
  945. else
  946. token.end = [this.sourceToken];
  947. if (this.type === 'newline')
  948. yield* this.pop();
  949. }
  950. }
  951. }
  952. export { Parser };