parse-cst.js 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753
  1. 'use strict';
  2. var PlainValue = require('./PlainValue-ec8e588e.js');
  3. class BlankLine extends PlainValue.Node {
  4. constructor() {
  5. super(PlainValue.Type.BLANK_LINE);
  6. }
  7. /* istanbul ignore next */
  8. get includesTrailingLines() {
  9. // This is never called from anywhere, but if it were,
  10. // this is the value it should return.
  11. return true;
  12. }
  13. /**
  14. * Parses a blank line from the source
  15. *
  16. * @param {ParseContext} context
  17. * @param {number} start - Index of first \n character
  18. * @returns {number} - Index of the character after this
  19. */
  20. parse(context, start) {
  21. this.context = context;
  22. this.range = new PlainValue.Range(start, start + 1);
  23. return start + 1;
  24. }
  25. }
  26. class CollectionItem extends PlainValue.Node {
  27. constructor(type, props) {
  28. super(type, props);
  29. this.node = null;
  30. }
  31. get includesTrailingLines() {
  32. return !!this.node && this.node.includesTrailingLines;
  33. }
  34. /**
  35. * @param {ParseContext} context
  36. * @param {number} start - Index of first character
  37. * @returns {number} - Index of the character after this
  38. */
  39. parse(context, start) {
  40. this.context = context;
  41. const {
  42. parseNode,
  43. src
  44. } = context;
  45. let {
  46. atLineStart,
  47. lineStart
  48. } = context;
  49. if (!atLineStart && this.type === PlainValue.Type.SEQ_ITEM) this.error = new PlainValue.YAMLSemanticError(this, 'Sequence items must not have preceding content on the same line');
  50. const indent = atLineStart ? start - lineStart : context.indent;
  51. let offset = PlainValue.Node.endOfWhiteSpace(src, start + 1);
  52. let ch = src[offset];
  53. const inlineComment = ch === '#';
  54. const comments = [];
  55. let blankLine = null;
  56. while (ch === '\n' || ch === '#') {
  57. if (ch === '#') {
  58. const end = PlainValue.Node.endOfLine(src, offset + 1);
  59. comments.push(new PlainValue.Range(offset, end));
  60. offset = end;
  61. } else {
  62. atLineStart = true;
  63. lineStart = offset + 1;
  64. const wsEnd = PlainValue.Node.endOfWhiteSpace(src, lineStart);
  65. if (src[wsEnd] === '\n' && comments.length === 0) {
  66. blankLine = new BlankLine();
  67. lineStart = blankLine.parse({
  68. src
  69. }, lineStart);
  70. }
  71. offset = PlainValue.Node.endOfIndent(src, lineStart);
  72. }
  73. ch = src[offset];
  74. }
  75. if (PlainValue.Node.nextNodeIsIndented(ch, offset - (lineStart + indent), this.type !== PlainValue.Type.SEQ_ITEM)) {
  76. this.node = parseNode({
  77. atLineStart,
  78. inCollection: false,
  79. indent,
  80. lineStart,
  81. parent: this
  82. }, offset);
  83. } else if (ch && lineStart > start + 1) {
  84. offset = lineStart - 1;
  85. }
  86. if (this.node) {
  87. if (blankLine) {
  88. // Only blank lines preceding non-empty nodes are captured. Note that
  89. // this means that collection item range start indices do not always
  90. // increase monotonically. -- eemeli/yaml#126
  91. const items = context.parent.items || context.parent.contents;
  92. if (items) items.push(blankLine);
  93. }
  94. if (comments.length) Array.prototype.push.apply(this.props, comments);
  95. offset = this.node.range.end;
  96. } else {
  97. if (inlineComment) {
  98. const c = comments[0];
  99. this.props.push(c);
  100. offset = c.end;
  101. } else {
  102. offset = PlainValue.Node.endOfLine(src, start + 1);
  103. }
  104. }
  105. const end = this.node ? this.node.valueRange.end : offset;
  106. this.valueRange = new PlainValue.Range(start, end);
  107. return offset;
  108. }
  109. setOrigRanges(cr, offset) {
  110. offset = super.setOrigRanges(cr, offset);
  111. return this.node ? this.node.setOrigRanges(cr, offset) : offset;
  112. }
  113. toString() {
  114. const {
  115. context: {
  116. src
  117. },
  118. node,
  119. range,
  120. value
  121. } = this;
  122. if (value != null) return value;
  123. const str = node ? src.slice(range.start, node.range.start) + String(node) : src.slice(range.start, range.end);
  124. return PlainValue.Node.addStringTerminator(src, range.end, str);
  125. }
  126. }
  127. class Comment extends PlainValue.Node {
  128. constructor() {
  129. super(PlainValue.Type.COMMENT);
  130. }
  131. /**
  132. * Parses a comment line from the source
  133. *
  134. * @param {ParseContext} context
  135. * @param {number} start - Index of first character
  136. * @returns {number} - Index of the character after this scalar
  137. */
  138. parse(context, start) {
  139. this.context = context;
  140. const offset = this.parseComment(start);
  141. this.range = new PlainValue.Range(start, offset);
  142. return offset;
  143. }
  144. }
  145. function grabCollectionEndComments(node) {
  146. let cnode = node;
  147. while (cnode instanceof CollectionItem) cnode = cnode.node;
  148. if (!(cnode instanceof Collection)) return null;
  149. const len = cnode.items.length;
  150. let ci = -1;
  151. for (let i = len - 1; i >= 0; --i) {
  152. const n = cnode.items[i];
  153. if (n.type === PlainValue.Type.COMMENT) {
  154. // Keep sufficiently indented comments with preceding node
  155. const {
  156. indent,
  157. lineStart
  158. } = n.context;
  159. if (indent > 0 && n.range.start >= lineStart + indent) break;
  160. ci = i;
  161. } else if (n.type === PlainValue.Type.BLANK_LINE) ci = i;else break;
  162. }
  163. if (ci === -1) return null;
  164. const ca = cnode.items.splice(ci, len - ci);
  165. const prevEnd = ca[0].range.start;
  166. while (true) {
  167. cnode.range.end = prevEnd;
  168. if (cnode.valueRange && cnode.valueRange.end > prevEnd) cnode.valueRange.end = prevEnd;
  169. if (cnode === node) break;
  170. cnode = cnode.context.parent;
  171. }
  172. return ca;
  173. }
  174. class Collection extends PlainValue.Node {
  175. static nextContentHasIndent(src, offset, indent) {
  176. const lineStart = PlainValue.Node.endOfLine(src, offset) + 1;
  177. offset = PlainValue.Node.endOfWhiteSpace(src, lineStart);
  178. const ch = src[offset];
  179. if (!ch) return false;
  180. if (offset >= lineStart + indent) return true;
  181. if (ch !== '#' && ch !== '\n') return false;
  182. return Collection.nextContentHasIndent(src, offset, indent);
  183. }
  184. constructor(firstItem) {
  185. super(firstItem.type === PlainValue.Type.SEQ_ITEM ? PlainValue.Type.SEQ : PlainValue.Type.MAP);
  186. for (let i = firstItem.props.length - 1; i >= 0; --i) {
  187. if (firstItem.props[i].start < firstItem.context.lineStart) {
  188. // props on previous line are assumed by the collection
  189. this.props = firstItem.props.slice(0, i + 1);
  190. firstItem.props = firstItem.props.slice(i + 1);
  191. const itemRange = firstItem.props[0] || firstItem.valueRange;
  192. firstItem.range.start = itemRange.start;
  193. break;
  194. }
  195. }
  196. this.items = [firstItem];
  197. const ec = grabCollectionEndComments(firstItem);
  198. if (ec) Array.prototype.push.apply(this.items, ec);
  199. }
  200. get includesTrailingLines() {
  201. return this.items.length > 0;
  202. }
  203. /**
  204. * @param {ParseContext} context
  205. * @param {number} start - Index of first character
  206. * @returns {number} - Index of the character after this
  207. */
  208. parse(context, start) {
  209. this.context = context;
  210. const {
  211. parseNode,
  212. src
  213. } = context; // It's easier to recalculate lineStart here rather than tracking down the
  214. // last context from which to read it -- eemeli/yaml#2
  215. let lineStart = PlainValue.Node.startOfLine(src, start);
  216. const firstItem = this.items[0]; // First-item context needs to be correct for later comment handling
  217. // -- eemeli/yaml#17
  218. firstItem.context.parent = this;
  219. this.valueRange = PlainValue.Range.copy(firstItem.valueRange);
  220. const indent = firstItem.range.start - firstItem.context.lineStart;
  221. let offset = start;
  222. offset = PlainValue.Node.normalizeOffset(src, offset);
  223. let ch = src[offset];
  224. let atLineStart = PlainValue.Node.endOfWhiteSpace(src, lineStart) === offset;
  225. let prevIncludesTrailingLines = false;
  226. while (ch) {
  227. while (ch === '\n' || ch === '#') {
  228. if (atLineStart && ch === '\n' && !prevIncludesTrailingLines) {
  229. const blankLine = new BlankLine();
  230. offset = blankLine.parse({
  231. src
  232. }, offset);
  233. this.valueRange.end = offset;
  234. if (offset >= src.length) {
  235. ch = null;
  236. break;
  237. }
  238. this.items.push(blankLine);
  239. offset -= 1; // blankLine.parse() consumes terminal newline
  240. } else if (ch === '#') {
  241. if (offset < lineStart + indent && !Collection.nextContentHasIndent(src, offset, indent)) {
  242. return offset;
  243. }
  244. const comment = new Comment();
  245. offset = comment.parse({
  246. indent,
  247. lineStart,
  248. src
  249. }, offset);
  250. this.items.push(comment);
  251. this.valueRange.end = offset;
  252. if (offset >= src.length) {
  253. ch = null;
  254. break;
  255. }
  256. }
  257. lineStart = offset + 1;
  258. offset = PlainValue.Node.endOfIndent(src, lineStart);
  259. if (PlainValue.Node.atBlank(src, offset)) {
  260. const wsEnd = PlainValue.Node.endOfWhiteSpace(src, offset);
  261. const next = src[wsEnd];
  262. if (!next || next === '\n' || next === '#') {
  263. offset = wsEnd;
  264. }
  265. }
  266. ch = src[offset];
  267. atLineStart = true;
  268. }
  269. if (!ch) {
  270. break;
  271. }
  272. if (offset !== lineStart + indent && (atLineStart || ch !== ':')) {
  273. if (offset < lineStart + indent) {
  274. if (lineStart > start) offset = lineStart;
  275. break;
  276. } else if (!this.error) {
  277. const msg = 'All collection items must start at the same column';
  278. this.error = new PlainValue.YAMLSyntaxError(this, msg);
  279. }
  280. }
  281. if (firstItem.type === PlainValue.Type.SEQ_ITEM) {
  282. if (ch !== '-') {
  283. if (lineStart > start) offset = lineStart;
  284. break;
  285. }
  286. } else if (ch === '-' && !this.error) {
  287. // map key may start with -, as long as it's followed by a non-whitespace char
  288. const next = src[offset + 1];
  289. if (!next || next === '\n' || next === '\t' || next === ' ') {
  290. const msg = 'A collection cannot be both a mapping and a sequence';
  291. this.error = new PlainValue.YAMLSyntaxError(this, msg);
  292. }
  293. }
  294. const node = parseNode({
  295. atLineStart,
  296. inCollection: true,
  297. indent,
  298. lineStart,
  299. parent: this
  300. }, offset);
  301. if (!node) return offset; // at next document start
  302. this.items.push(node);
  303. this.valueRange.end = node.valueRange.end;
  304. offset = PlainValue.Node.normalizeOffset(src, node.range.end);
  305. ch = src[offset];
  306. atLineStart = false;
  307. prevIncludesTrailingLines = node.includesTrailingLines; // Need to reset lineStart and atLineStart here if preceding node's range
  308. // has advanced to check the current line's indentation level
  309. // -- eemeli/yaml#10 & eemeli/yaml#38
  310. if (ch) {
  311. let ls = offset - 1;
  312. let prev = src[ls];
  313. while (prev === ' ' || prev === '\t') prev = src[--ls];
  314. if (prev === '\n') {
  315. lineStart = ls + 1;
  316. atLineStart = true;
  317. }
  318. }
  319. const ec = grabCollectionEndComments(node);
  320. if (ec) Array.prototype.push.apply(this.items, ec);
  321. }
  322. return offset;
  323. }
  324. setOrigRanges(cr, offset) {
  325. offset = super.setOrigRanges(cr, offset);
  326. this.items.forEach(node => {
  327. offset = node.setOrigRanges(cr, offset);
  328. });
  329. return offset;
  330. }
  331. toString() {
  332. const {
  333. context: {
  334. src
  335. },
  336. items,
  337. range,
  338. value
  339. } = this;
  340. if (value != null) return value;
  341. let str = src.slice(range.start, items[0].range.start) + String(items[0]);
  342. for (let i = 1; i < items.length; ++i) {
  343. const item = items[i];
  344. const {
  345. atLineStart,
  346. indent
  347. } = item.context;
  348. if (atLineStart) for (let i = 0; i < indent; ++i) str += ' ';
  349. str += String(item);
  350. }
  351. return PlainValue.Node.addStringTerminator(src, range.end, str);
  352. }
  353. }
  354. class Directive extends PlainValue.Node {
  355. constructor() {
  356. super(PlainValue.Type.DIRECTIVE);
  357. this.name = null;
  358. }
  359. get parameters() {
  360. const raw = this.rawValue;
  361. return raw ? raw.trim().split(/[ \t]+/) : [];
  362. }
  363. parseName(start) {
  364. const {
  365. src
  366. } = this.context;
  367. let offset = start;
  368. let ch = src[offset];
  369. while (ch && ch !== '\n' && ch !== '\t' && ch !== ' ') ch = src[offset += 1];
  370. this.name = src.slice(start, offset);
  371. return offset;
  372. }
  373. parseParameters(start) {
  374. const {
  375. src
  376. } = this.context;
  377. let offset = start;
  378. let ch = src[offset];
  379. while (ch && ch !== '\n' && ch !== '#') ch = src[offset += 1];
  380. this.valueRange = new PlainValue.Range(start, offset);
  381. return offset;
  382. }
  383. parse(context, start) {
  384. this.context = context;
  385. let offset = this.parseName(start + 1);
  386. offset = this.parseParameters(offset);
  387. offset = this.parseComment(offset);
  388. this.range = new PlainValue.Range(start, offset);
  389. return offset;
  390. }
  391. }
  392. class Document extends PlainValue.Node {
  393. static startCommentOrEndBlankLine(src, start) {
  394. const offset = PlainValue.Node.endOfWhiteSpace(src, start);
  395. const ch = src[offset];
  396. return ch === '#' || ch === '\n' ? offset : start;
  397. }
  398. constructor() {
  399. super(PlainValue.Type.DOCUMENT);
  400. this.directives = null;
  401. this.contents = null;
  402. this.directivesEndMarker = null;
  403. this.documentEndMarker = null;
  404. }
  405. parseDirectives(start) {
  406. const {
  407. src
  408. } = this.context;
  409. this.directives = [];
  410. let atLineStart = true;
  411. let hasDirectives = false;
  412. let offset = start;
  413. while (!PlainValue.Node.atDocumentBoundary(src, offset, PlainValue.Char.DIRECTIVES_END)) {
  414. offset = Document.startCommentOrEndBlankLine(src, offset);
  415. switch (src[offset]) {
  416. case '\n':
  417. if (atLineStart) {
  418. const blankLine = new BlankLine();
  419. offset = blankLine.parse({
  420. src
  421. }, offset);
  422. if (offset < src.length) {
  423. this.directives.push(blankLine);
  424. }
  425. } else {
  426. offset += 1;
  427. atLineStart = true;
  428. }
  429. break;
  430. case '#':
  431. {
  432. const comment = new Comment();
  433. offset = comment.parse({
  434. src
  435. }, offset);
  436. this.directives.push(comment);
  437. atLineStart = false;
  438. }
  439. break;
  440. case '%':
  441. {
  442. const directive = new Directive();
  443. offset = directive.parse({
  444. parent: this,
  445. src
  446. }, offset);
  447. this.directives.push(directive);
  448. hasDirectives = true;
  449. atLineStart = false;
  450. }
  451. break;
  452. default:
  453. if (hasDirectives) {
  454. this.error = new PlainValue.YAMLSemanticError(this, 'Missing directives-end indicator line');
  455. } else if (this.directives.length > 0) {
  456. this.contents = this.directives;
  457. this.directives = [];
  458. }
  459. return offset;
  460. }
  461. }
  462. if (src[offset]) {
  463. this.directivesEndMarker = new PlainValue.Range(offset, offset + 3);
  464. return offset + 3;
  465. }
  466. if (hasDirectives) {
  467. this.error = new PlainValue.YAMLSemanticError(this, 'Missing directives-end indicator line');
  468. } else if (this.directives.length > 0) {
  469. this.contents = this.directives;
  470. this.directives = [];
  471. }
  472. return offset;
  473. }
  474. parseContents(start) {
  475. const {
  476. parseNode,
  477. src
  478. } = this.context;
  479. if (!this.contents) this.contents = [];
  480. let lineStart = start;
  481. while (src[lineStart - 1] === '-') lineStart -= 1;
  482. let offset = PlainValue.Node.endOfWhiteSpace(src, start);
  483. let atLineStart = lineStart === start;
  484. this.valueRange = new PlainValue.Range(offset);
  485. while (!PlainValue.Node.atDocumentBoundary(src, offset, PlainValue.Char.DOCUMENT_END)) {
  486. switch (src[offset]) {
  487. case '\n':
  488. if (atLineStart) {
  489. const blankLine = new BlankLine();
  490. offset = blankLine.parse({
  491. src
  492. }, offset);
  493. if (offset < src.length) {
  494. this.contents.push(blankLine);
  495. }
  496. } else {
  497. offset += 1;
  498. atLineStart = true;
  499. }
  500. lineStart = offset;
  501. break;
  502. case '#':
  503. {
  504. const comment = new Comment();
  505. offset = comment.parse({
  506. src
  507. }, offset);
  508. this.contents.push(comment);
  509. atLineStart = false;
  510. }
  511. break;
  512. default:
  513. {
  514. const iEnd = PlainValue.Node.endOfIndent(src, offset);
  515. const context = {
  516. atLineStart,
  517. indent: -1,
  518. inFlow: false,
  519. inCollection: false,
  520. lineStart,
  521. parent: this
  522. };
  523. const node = parseNode(context, iEnd);
  524. if (!node) return this.valueRange.end = iEnd; // at next document start
  525. this.contents.push(node);
  526. offset = node.range.end;
  527. atLineStart = false;
  528. const ec = grabCollectionEndComments(node);
  529. if (ec) Array.prototype.push.apply(this.contents, ec);
  530. }
  531. }
  532. offset = Document.startCommentOrEndBlankLine(src, offset);
  533. }
  534. this.valueRange.end = offset;
  535. if (src[offset]) {
  536. this.documentEndMarker = new PlainValue.Range(offset, offset + 3);
  537. offset += 3;
  538. if (src[offset]) {
  539. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  540. if (src[offset] === '#') {
  541. const comment = new Comment();
  542. offset = comment.parse({
  543. src
  544. }, offset);
  545. this.contents.push(comment);
  546. }
  547. switch (src[offset]) {
  548. case '\n':
  549. offset += 1;
  550. break;
  551. case undefined:
  552. break;
  553. default:
  554. this.error = new PlainValue.YAMLSyntaxError(this, 'Document end marker line cannot have a non-comment suffix');
  555. }
  556. }
  557. }
  558. return offset;
  559. }
  560. /**
  561. * @param {ParseContext} context
  562. * @param {number} start - Index of first character
  563. * @returns {number} - Index of the character after this
  564. */
  565. parse(context, start) {
  566. context.root = this;
  567. this.context = context;
  568. const {
  569. src
  570. } = context;
  571. let offset = src.charCodeAt(start) === 0xfeff ? start + 1 : start; // skip BOM
  572. offset = this.parseDirectives(offset);
  573. offset = this.parseContents(offset);
  574. return offset;
  575. }
  576. setOrigRanges(cr, offset) {
  577. offset = super.setOrigRanges(cr, offset);
  578. this.directives.forEach(node => {
  579. offset = node.setOrigRanges(cr, offset);
  580. });
  581. if (this.directivesEndMarker) offset = this.directivesEndMarker.setOrigRange(cr, offset);
  582. this.contents.forEach(node => {
  583. offset = node.setOrigRanges(cr, offset);
  584. });
  585. if (this.documentEndMarker) offset = this.documentEndMarker.setOrigRange(cr, offset);
  586. return offset;
  587. }
  588. toString() {
  589. const {
  590. contents,
  591. directives,
  592. value
  593. } = this;
  594. if (value != null) return value;
  595. let str = directives.join('');
  596. if (contents.length > 0) {
  597. if (directives.length > 0 || contents[0].type === PlainValue.Type.COMMENT) str += '---\n';
  598. str += contents.join('');
  599. }
  600. if (str[str.length - 1] !== '\n') str += '\n';
  601. return str;
  602. }
  603. }
  604. class Alias extends PlainValue.Node {
  605. /**
  606. * Parses an *alias from the source
  607. *
  608. * @param {ParseContext} context
  609. * @param {number} start - Index of first character
  610. * @returns {number} - Index of the character after this scalar
  611. */
  612. parse(context, start) {
  613. this.context = context;
  614. const {
  615. src
  616. } = context;
  617. let offset = PlainValue.Node.endOfIdentifier(src, start + 1);
  618. this.valueRange = new PlainValue.Range(start + 1, offset);
  619. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  620. offset = this.parseComment(offset);
  621. return offset;
  622. }
  623. }
  624. const Chomp = {
  625. CLIP: 'CLIP',
  626. KEEP: 'KEEP',
  627. STRIP: 'STRIP'
  628. };
  629. class BlockValue extends PlainValue.Node {
  630. constructor(type, props) {
  631. super(type, props);
  632. this.blockIndent = null;
  633. this.chomping = Chomp.CLIP;
  634. this.header = null;
  635. }
  636. get includesTrailingLines() {
  637. return this.chomping === Chomp.KEEP;
  638. }
  639. get strValue() {
  640. if (!this.valueRange || !this.context) return null;
  641. let {
  642. start,
  643. end
  644. } = this.valueRange;
  645. const {
  646. indent,
  647. src
  648. } = this.context;
  649. if (this.valueRange.isEmpty()) return '';
  650. let lastNewLine = null;
  651. let ch = src[end - 1];
  652. while (ch === '\n' || ch === '\t' || ch === ' ') {
  653. end -= 1;
  654. if (end <= start) {
  655. if (this.chomping === Chomp.KEEP) break;else return ''; // probably never happens
  656. }
  657. if (ch === '\n') lastNewLine = end;
  658. ch = src[end - 1];
  659. }
  660. let keepStart = end + 1;
  661. if (lastNewLine) {
  662. if (this.chomping === Chomp.KEEP) {
  663. keepStart = lastNewLine;
  664. end = this.valueRange.end;
  665. } else {
  666. end = lastNewLine;
  667. }
  668. }
  669. const bi = indent + this.blockIndent;
  670. const folded = this.type === PlainValue.Type.BLOCK_FOLDED;
  671. let atStart = true;
  672. let str = '';
  673. let sep = '';
  674. let prevMoreIndented = false;
  675. for (let i = start; i < end; ++i) {
  676. for (let j = 0; j < bi; ++j) {
  677. if (src[i] !== ' ') break;
  678. i += 1;
  679. }
  680. const ch = src[i];
  681. if (ch === '\n') {
  682. if (sep === '\n') str += '\n';else sep = '\n';
  683. } else {
  684. const lineEnd = PlainValue.Node.endOfLine(src, i);
  685. const line = src.slice(i, lineEnd);
  686. i = lineEnd;
  687. if (folded && (ch === ' ' || ch === '\t') && i < keepStart) {
  688. if (sep === ' ') sep = '\n';else if (!prevMoreIndented && !atStart && sep === '\n') sep = '\n\n';
  689. str += sep + line; //+ ((lineEnd < end && src[lineEnd]) || '')
  690. sep = lineEnd < end && src[lineEnd] || '';
  691. prevMoreIndented = true;
  692. } else {
  693. str += sep + line;
  694. sep = folded && i < keepStart ? ' ' : '\n';
  695. prevMoreIndented = false;
  696. }
  697. if (atStart && line !== '') atStart = false;
  698. }
  699. }
  700. return this.chomping === Chomp.STRIP ? str : str + '\n';
  701. }
  702. parseBlockHeader(start) {
  703. const {
  704. src
  705. } = this.context;
  706. let offset = start + 1;
  707. let bi = '';
  708. while (true) {
  709. const ch = src[offset];
  710. switch (ch) {
  711. case '-':
  712. this.chomping = Chomp.STRIP;
  713. break;
  714. case '+':
  715. this.chomping = Chomp.KEEP;
  716. break;
  717. case '0':
  718. case '1':
  719. case '2':
  720. case '3':
  721. case '4':
  722. case '5':
  723. case '6':
  724. case '7':
  725. case '8':
  726. case '9':
  727. bi += ch;
  728. break;
  729. default:
  730. this.blockIndent = Number(bi) || null;
  731. this.header = new PlainValue.Range(start, offset);
  732. return offset;
  733. }
  734. offset += 1;
  735. }
  736. }
  737. parseBlockValue(start) {
  738. const {
  739. indent,
  740. src
  741. } = this.context;
  742. const explicit = !!this.blockIndent;
  743. let offset = start;
  744. let valueEnd = start;
  745. let minBlockIndent = 1;
  746. for (let ch = src[offset]; ch === '\n'; ch = src[offset]) {
  747. offset += 1;
  748. if (PlainValue.Node.atDocumentBoundary(src, offset)) break;
  749. const end = PlainValue.Node.endOfBlockIndent(src, indent, offset); // should not include tab?
  750. if (end === null) break;
  751. const ch = src[end];
  752. const lineIndent = end - (offset + indent);
  753. if (!this.blockIndent) {
  754. // no explicit block indent, none yet detected
  755. if (src[end] !== '\n') {
  756. // first line with non-whitespace content
  757. if (lineIndent < minBlockIndent) {
  758. const msg = 'Block scalars with more-indented leading empty lines must use an explicit indentation indicator';
  759. this.error = new PlainValue.YAMLSemanticError(this, msg);
  760. }
  761. this.blockIndent = lineIndent;
  762. } else if (lineIndent > minBlockIndent) {
  763. // empty line with more whitespace
  764. minBlockIndent = lineIndent;
  765. }
  766. } else if (ch && ch !== '\n' && lineIndent < this.blockIndent) {
  767. if (src[end] === '#') break;
  768. if (!this.error) {
  769. const src = explicit ? 'explicit indentation indicator' : 'first line';
  770. const msg = `Block scalars must not be less indented than their ${src}`;
  771. this.error = new PlainValue.YAMLSemanticError(this, msg);
  772. }
  773. }
  774. if (src[end] === '\n') {
  775. offset = end;
  776. } else {
  777. offset = valueEnd = PlainValue.Node.endOfLine(src, end);
  778. }
  779. }
  780. if (this.chomping !== Chomp.KEEP) {
  781. offset = src[valueEnd] ? valueEnd + 1 : valueEnd;
  782. }
  783. this.valueRange = new PlainValue.Range(start + 1, offset);
  784. return offset;
  785. }
  786. /**
  787. * Parses a block value from the source
  788. *
  789. * Accepted forms are:
  790. * ```
  791. * BS
  792. * block
  793. * lines
  794. *
  795. * BS #comment
  796. * block
  797. * lines
  798. * ```
  799. * where the block style BS matches the regexp `[|>][-+1-9]*` and block lines
  800. * are empty or have an indent level greater than `indent`.
  801. *
  802. * @param {ParseContext} context
  803. * @param {number} start - Index of first character
  804. * @returns {number} - Index of the character after this block
  805. */
  806. parse(context, start) {
  807. this.context = context;
  808. const {
  809. src
  810. } = context;
  811. let offset = this.parseBlockHeader(start);
  812. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  813. offset = this.parseComment(offset);
  814. offset = this.parseBlockValue(offset);
  815. return offset;
  816. }
  817. setOrigRanges(cr, offset) {
  818. offset = super.setOrigRanges(cr, offset);
  819. return this.header ? this.header.setOrigRange(cr, offset) : offset;
  820. }
  821. }
  822. class FlowCollection extends PlainValue.Node {
  823. constructor(type, props) {
  824. super(type, props);
  825. this.items = null;
  826. }
  827. prevNodeIsJsonLike(idx = this.items.length) {
  828. const node = this.items[idx - 1];
  829. return !!node && (node.jsonLike || node.type === PlainValue.Type.COMMENT && this.prevNodeIsJsonLike(idx - 1));
  830. }
  831. /**
  832. * @param {ParseContext} context
  833. * @param {number} start - Index of first character
  834. * @returns {number} - Index of the character after this
  835. */
  836. parse(context, start) {
  837. this.context = context;
  838. const {
  839. parseNode,
  840. src
  841. } = context;
  842. let {
  843. indent,
  844. lineStart
  845. } = context;
  846. let char = src[start]; // { or [
  847. this.items = [{
  848. char,
  849. offset: start
  850. }];
  851. let offset = PlainValue.Node.endOfWhiteSpace(src, start + 1);
  852. char = src[offset];
  853. while (char && char !== ']' && char !== '}') {
  854. switch (char) {
  855. case '\n':
  856. {
  857. lineStart = offset + 1;
  858. const wsEnd = PlainValue.Node.endOfWhiteSpace(src, lineStart);
  859. if (src[wsEnd] === '\n') {
  860. const blankLine = new BlankLine();
  861. lineStart = blankLine.parse({
  862. src
  863. }, lineStart);
  864. this.items.push(blankLine);
  865. }
  866. offset = PlainValue.Node.endOfIndent(src, lineStart);
  867. if (offset <= lineStart + indent) {
  868. char = src[offset];
  869. if (offset < lineStart + indent || char !== ']' && char !== '}') {
  870. const msg = 'Insufficient indentation in flow collection';
  871. this.error = new PlainValue.YAMLSemanticError(this, msg);
  872. }
  873. }
  874. }
  875. break;
  876. case ',':
  877. {
  878. this.items.push({
  879. char,
  880. offset
  881. });
  882. offset += 1;
  883. }
  884. break;
  885. case '#':
  886. {
  887. const comment = new Comment();
  888. offset = comment.parse({
  889. src
  890. }, offset);
  891. this.items.push(comment);
  892. }
  893. break;
  894. case '?':
  895. case ':':
  896. {
  897. const next = src[offset + 1];
  898. if (next === '\n' || next === '\t' || next === ' ' || next === ',' || // in-flow : after JSON-like key does not need to be followed by whitespace
  899. char === ':' && this.prevNodeIsJsonLike()) {
  900. this.items.push({
  901. char,
  902. offset
  903. });
  904. offset += 1;
  905. break;
  906. }
  907. }
  908. // fallthrough
  909. default:
  910. {
  911. const node = parseNode({
  912. atLineStart: false,
  913. inCollection: false,
  914. inFlow: true,
  915. indent: -1,
  916. lineStart,
  917. parent: this
  918. }, offset);
  919. if (!node) {
  920. // at next document start
  921. this.valueRange = new PlainValue.Range(start, offset);
  922. return offset;
  923. }
  924. this.items.push(node);
  925. offset = PlainValue.Node.normalizeOffset(src, node.range.end);
  926. }
  927. }
  928. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  929. char = src[offset];
  930. }
  931. this.valueRange = new PlainValue.Range(start, offset + 1);
  932. if (char) {
  933. this.items.push({
  934. char,
  935. offset
  936. });
  937. offset = PlainValue.Node.endOfWhiteSpace(src, offset + 1);
  938. offset = this.parseComment(offset);
  939. }
  940. return offset;
  941. }
  942. setOrigRanges(cr, offset) {
  943. offset = super.setOrigRanges(cr, offset);
  944. this.items.forEach(node => {
  945. if (node instanceof PlainValue.Node) {
  946. offset = node.setOrigRanges(cr, offset);
  947. } else if (cr.length === 0) {
  948. node.origOffset = node.offset;
  949. } else {
  950. let i = offset;
  951. while (i < cr.length) {
  952. if (cr[i] > node.offset) break;else ++i;
  953. }
  954. node.origOffset = node.offset + i;
  955. offset = i;
  956. }
  957. });
  958. return offset;
  959. }
  960. toString() {
  961. const {
  962. context: {
  963. src
  964. },
  965. items,
  966. range,
  967. value
  968. } = this;
  969. if (value != null) return value;
  970. const nodes = items.filter(item => item instanceof PlainValue.Node);
  971. let str = '';
  972. let prevEnd = range.start;
  973. nodes.forEach(node => {
  974. const prefix = src.slice(prevEnd, node.range.start);
  975. prevEnd = node.range.end;
  976. str += prefix + String(node);
  977. if (str[str.length - 1] === '\n' && src[prevEnd - 1] !== '\n' && src[prevEnd] === '\n') {
  978. // Comment range does not include the terminal newline, but its
  979. // stringified value does. Without this fix, newlines at comment ends
  980. // get duplicated.
  981. prevEnd += 1;
  982. }
  983. });
  984. str += src.slice(prevEnd, range.end);
  985. return PlainValue.Node.addStringTerminator(src, range.end, str);
  986. }
  987. }
  988. class QuoteDouble extends PlainValue.Node {
  989. static endOfQuote(src, offset) {
  990. let ch = src[offset];
  991. while (ch && ch !== '"') {
  992. offset += ch === '\\' ? 2 : 1;
  993. ch = src[offset];
  994. }
  995. return offset + 1;
  996. }
  997. /**
  998. * @returns {string | { str: string, errors: YAMLSyntaxError[] }}
  999. */
  1000. get strValue() {
  1001. if (!this.valueRange || !this.context) return null;
  1002. const errors = [];
  1003. const {
  1004. start,
  1005. end
  1006. } = this.valueRange;
  1007. const {
  1008. indent,
  1009. src
  1010. } = this.context;
  1011. if (src[end - 1] !== '"') errors.push(new PlainValue.YAMLSyntaxError(this, 'Missing closing "quote')); // Using String#replace is too painful with escaped newlines preceded by
  1012. // escaped backslashes; also, this should be faster.
  1013. let str = '';
  1014. for (let i = start + 1; i < end - 1; ++i) {
  1015. const ch = src[i];
  1016. if (ch === '\n') {
  1017. if (PlainValue.Node.atDocumentBoundary(src, i + 1)) errors.push(new PlainValue.YAMLSemanticError(this, 'Document boundary indicators are not allowed within string values'));
  1018. const {
  1019. fold,
  1020. offset,
  1021. error
  1022. } = PlainValue.Node.foldNewline(src, i, indent);
  1023. str += fold;
  1024. i = offset;
  1025. if (error) errors.push(new PlainValue.YAMLSemanticError(this, 'Multi-line double-quoted string needs to be sufficiently indented'));
  1026. } else if (ch === '\\') {
  1027. i += 1;
  1028. switch (src[i]) {
  1029. case '0':
  1030. str += '\0';
  1031. break;
  1032. // null character
  1033. case 'a':
  1034. str += '\x07';
  1035. break;
  1036. // bell character
  1037. case 'b':
  1038. str += '\b';
  1039. break;
  1040. // backspace
  1041. case 'e':
  1042. str += '\x1b';
  1043. break;
  1044. // escape character
  1045. case 'f':
  1046. str += '\f';
  1047. break;
  1048. // form feed
  1049. case 'n':
  1050. str += '\n';
  1051. break;
  1052. // line feed
  1053. case 'r':
  1054. str += '\r';
  1055. break;
  1056. // carriage return
  1057. case 't':
  1058. str += '\t';
  1059. break;
  1060. // horizontal tab
  1061. case 'v':
  1062. str += '\v';
  1063. break;
  1064. // vertical tab
  1065. case 'N':
  1066. str += '\u0085';
  1067. break;
  1068. // Unicode next line
  1069. case '_':
  1070. str += '\u00a0';
  1071. break;
  1072. // Unicode non-breaking space
  1073. case 'L':
  1074. str += '\u2028';
  1075. break;
  1076. // Unicode line separator
  1077. case 'P':
  1078. str += '\u2029';
  1079. break;
  1080. // Unicode paragraph separator
  1081. case ' ':
  1082. str += ' ';
  1083. break;
  1084. case '"':
  1085. str += '"';
  1086. break;
  1087. case '/':
  1088. str += '/';
  1089. break;
  1090. case '\\':
  1091. str += '\\';
  1092. break;
  1093. case '\t':
  1094. str += '\t';
  1095. break;
  1096. case 'x':
  1097. str += this.parseCharCode(i + 1, 2, errors);
  1098. i += 2;
  1099. break;
  1100. case 'u':
  1101. str += this.parseCharCode(i + 1, 4, errors);
  1102. i += 4;
  1103. break;
  1104. case 'U':
  1105. str += this.parseCharCode(i + 1, 8, errors);
  1106. i += 8;
  1107. break;
  1108. case '\n':
  1109. // skip escaped newlines, but still trim the following line
  1110. while (src[i + 1] === ' ' || src[i + 1] === '\t') i += 1;
  1111. break;
  1112. default:
  1113. errors.push(new PlainValue.YAMLSyntaxError(this, `Invalid escape sequence ${src.substr(i - 1, 2)}`));
  1114. str += '\\' + src[i];
  1115. }
  1116. } else if (ch === ' ' || ch === '\t') {
  1117. // trim trailing whitespace
  1118. const wsStart = i;
  1119. let next = src[i + 1];
  1120. while (next === ' ' || next === '\t') {
  1121. i += 1;
  1122. next = src[i + 1];
  1123. }
  1124. if (next !== '\n') str += i > wsStart ? src.slice(wsStart, i + 1) : ch;
  1125. } else {
  1126. str += ch;
  1127. }
  1128. }
  1129. return errors.length > 0 ? {
  1130. errors,
  1131. str
  1132. } : str;
  1133. }
  1134. parseCharCode(offset, length, errors) {
  1135. const {
  1136. src
  1137. } = this.context;
  1138. const cc = src.substr(offset, length);
  1139. const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc);
  1140. const code = ok ? parseInt(cc, 16) : NaN;
  1141. if (isNaN(code)) {
  1142. errors.push(new PlainValue.YAMLSyntaxError(this, `Invalid escape sequence ${src.substr(offset - 2, length + 2)}`));
  1143. return src.substr(offset - 2, length + 2);
  1144. }
  1145. return String.fromCodePoint(code);
  1146. }
  1147. /**
  1148. * Parses a "double quoted" value from the source
  1149. *
  1150. * @param {ParseContext} context
  1151. * @param {number} start - Index of first character
  1152. * @returns {number} - Index of the character after this scalar
  1153. */
  1154. parse(context, start) {
  1155. this.context = context;
  1156. const {
  1157. src
  1158. } = context;
  1159. let offset = QuoteDouble.endOfQuote(src, start + 1);
  1160. this.valueRange = new PlainValue.Range(start, offset);
  1161. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  1162. offset = this.parseComment(offset);
  1163. return offset;
  1164. }
  1165. }
  1166. class QuoteSingle extends PlainValue.Node {
  1167. static endOfQuote(src, offset) {
  1168. let ch = src[offset];
  1169. while (ch) {
  1170. if (ch === "'") {
  1171. if (src[offset + 1] !== "'") break;
  1172. ch = src[offset += 2];
  1173. } else {
  1174. ch = src[offset += 1];
  1175. }
  1176. }
  1177. return offset + 1;
  1178. }
  1179. /**
  1180. * @returns {string | { str: string, errors: YAMLSyntaxError[] }}
  1181. */
  1182. get strValue() {
  1183. if (!this.valueRange || !this.context) return null;
  1184. const errors = [];
  1185. const {
  1186. start,
  1187. end
  1188. } = this.valueRange;
  1189. const {
  1190. indent,
  1191. src
  1192. } = this.context;
  1193. if (src[end - 1] !== "'") errors.push(new PlainValue.YAMLSyntaxError(this, "Missing closing 'quote"));
  1194. let str = '';
  1195. for (let i = start + 1; i < end - 1; ++i) {
  1196. const ch = src[i];
  1197. if (ch === '\n') {
  1198. if (PlainValue.Node.atDocumentBoundary(src, i + 1)) errors.push(new PlainValue.YAMLSemanticError(this, 'Document boundary indicators are not allowed within string values'));
  1199. const {
  1200. fold,
  1201. offset,
  1202. error
  1203. } = PlainValue.Node.foldNewline(src, i, indent);
  1204. str += fold;
  1205. i = offset;
  1206. if (error) errors.push(new PlainValue.YAMLSemanticError(this, 'Multi-line single-quoted string needs to be sufficiently indented'));
  1207. } else if (ch === "'") {
  1208. str += ch;
  1209. i += 1;
  1210. if (src[i] !== "'") errors.push(new PlainValue.YAMLSyntaxError(this, 'Unescaped single quote? This should not happen.'));
  1211. } else if (ch === ' ' || ch === '\t') {
  1212. // trim trailing whitespace
  1213. const wsStart = i;
  1214. let next = src[i + 1];
  1215. while (next === ' ' || next === '\t') {
  1216. i += 1;
  1217. next = src[i + 1];
  1218. }
  1219. if (next !== '\n') str += i > wsStart ? src.slice(wsStart, i + 1) : ch;
  1220. } else {
  1221. str += ch;
  1222. }
  1223. }
  1224. return errors.length > 0 ? {
  1225. errors,
  1226. str
  1227. } : str;
  1228. }
  1229. /**
  1230. * Parses a 'single quoted' value from the source
  1231. *
  1232. * @param {ParseContext} context
  1233. * @param {number} start - Index of first character
  1234. * @returns {number} - Index of the character after this scalar
  1235. */
  1236. parse(context, start) {
  1237. this.context = context;
  1238. const {
  1239. src
  1240. } = context;
  1241. let offset = QuoteSingle.endOfQuote(src, start + 1);
  1242. this.valueRange = new PlainValue.Range(start, offset);
  1243. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  1244. offset = this.parseComment(offset);
  1245. return offset;
  1246. }
  1247. }
  1248. function createNewNode(type, props) {
  1249. switch (type) {
  1250. case PlainValue.Type.ALIAS:
  1251. return new Alias(type, props);
  1252. case PlainValue.Type.BLOCK_FOLDED:
  1253. case PlainValue.Type.BLOCK_LITERAL:
  1254. return new BlockValue(type, props);
  1255. case PlainValue.Type.FLOW_MAP:
  1256. case PlainValue.Type.FLOW_SEQ:
  1257. return new FlowCollection(type, props);
  1258. case PlainValue.Type.MAP_KEY:
  1259. case PlainValue.Type.MAP_VALUE:
  1260. case PlainValue.Type.SEQ_ITEM:
  1261. return new CollectionItem(type, props);
  1262. case PlainValue.Type.COMMENT:
  1263. case PlainValue.Type.PLAIN:
  1264. return new PlainValue.PlainValue(type, props);
  1265. case PlainValue.Type.QUOTE_DOUBLE:
  1266. return new QuoteDouble(type, props);
  1267. case PlainValue.Type.QUOTE_SINGLE:
  1268. return new QuoteSingle(type, props);
  1269. /* istanbul ignore next */
  1270. default:
  1271. return null;
  1272. // should never happen
  1273. }
  1274. }
  1275. /**
  1276. * @param {boolean} atLineStart - Node starts at beginning of line
  1277. * @param {boolean} inFlow - true if currently in a flow context
  1278. * @param {boolean} inCollection - true if currently in a collection context
  1279. * @param {number} indent - Current level of indentation
  1280. * @param {number} lineStart - Start of the current line
  1281. * @param {Node} parent - The parent of the node
  1282. * @param {string} src - Source of the YAML document
  1283. */
  1284. class ParseContext {
  1285. static parseType(src, offset, inFlow) {
  1286. switch (src[offset]) {
  1287. case '*':
  1288. return PlainValue.Type.ALIAS;
  1289. case '>':
  1290. return PlainValue.Type.BLOCK_FOLDED;
  1291. case '|':
  1292. return PlainValue.Type.BLOCK_LITERAL;
  1293. case '{':
  1294. return PlainValue.Type.FLOW_MAP;
  1295. case '[':
  1296. return PlainValue.Type.FLOW_SEQ;
  1297. case '?':
  1298. return !inFlow && PlainValue.Node.atBlank(src, offset + 1, true) ? PlainValue.Type.MAP_KEY : PlainValue.Type.PLAIN;
  1299. case ':':
  1300. return !inFlow && PlainValue.Node.atBlank(src, offset + 1, true) ? PlainValue.Type.MAP_VALUE : PlainValue.Type.PLAIN;
  1301. case '-':
  1302. return !inFlow && PlainValue.Node.atBlank(src, offset + 1, true) ? PlainValue.Type.SEQ_ITEM : PlainValue.Type.PLAIN;
  1303. case '"':
  1304. return PlainValue.Type.QUOTE_DOUBLE;
  1305. case "'":
  1306. return PlainValue.Type.QUOTE_SINGLE;
  1307. default:
  1308. return PlainValue.Type.PLAIN;
  1309. }
  1310. }
  1311. constructor(orig = {}, {
  1312. atLineStart,
  1313. inCollection,
  1314. inFlow,
  1315. indent,
  1316. lineStart,
  1317. parent
  1318. } = {}) {
  1319. PlainValue._defineProperty(this, "parseNode", (overlay, start) => {
  1320. if (PlainValue.Node.atDocumentBoundary(this.src, start)) return null;
  1321. const context = new ParseContext(this, overlay);
  1322. const {
  1323. props,
  1324. type,
  1325. valueStart
  1326. } = context.parseProps(start);
  1327. const node = createNewNode(type, props);
  1328. let offset = node.parse(context, valueStart);
  1329. node.range = new PlainValue.Range(start, offset);
  1330. /* istanbul ignore if */
  1331. if (offset <= start) {
  1332. // This should never happen, but if it does, let's make sure to at least
  1333. // step one character forward to avoid a busy loop.
  1334. node.error = new Error(`Node#parse consumed no characters`);
  1335. node.error.parseEnd = offset;
  1336. node.error.source = node;
  1337. node.range.end = start + 1;
  1338. }
  1339. if (context.nodeStartsCollection(node)) {
  1340. if (!node.error && !context.atLineStart && context.parent.type === PlainValue.Type.DOCUMENT) {
  1341. node.error = new PlainValue.YAMLSyntaxError(node, 'Block collection must not have preceding content here (e.g. directives-end indicator)');
  1342. }
  1343. const collection = new Collection(node);
  1344. offset = collection.parse(new ParseContext(context), offset);
  1345. collection.range = new PlainValue.Range(start, offset);
  1346. return collection;
  1347. }
  1348. return node;
  1349. });
  1350. this.atLineStart = atLineStart != null ? atLineStart : orig.atLineStart || false;
  1351. this.inCollection = inCollection != null ? inCollection : orig.inCollection || false;
  1352. this.inFlow = inFlow != null ? inFlow : orig.inFlow || false;
  1353. this.indent = indent != null ? indent : orig.indent;
  1354. this.lineStart = lineStart != null ? lineStart : orig.lineStart;
  1355. this.parent = parent != null ? parent : orig.parent || {};
  1356. this.root = orig.root;
  1357. this.src = orig.src;
  1358. }
  1359. nodeStartsCollection(node) {
  1360. const {
  1361. inCollection,
  1362. inFlow,
  1363. src
  1364. } = this;
  1365. if (inCollection || inFlow) return false;
  1366. if (node instanceof CollectionItem) return true; // check for implicit key
  1367. let offset = node.range.end;
  1368. if (src[offset] === '\n' || src[offset - 1] === '\n') return false;
  1369. offset = PlainValue.Node.endOfWhiteSpace(src, offset);
  1370. return src[offset] === ':';
  1371. } // Anchor and tag are before type, which determines the node implementation
  1372. // class; hence this intermediate step.
  1373. parseProps(offset) {
  1374. const {
  1375. inFlow,
  1376. parent,
  1377. src
  1378. } = this;
  1379. const props = [];
  1380. let lineHasProps = false;
  1381. offset = this.atLineStart ? PlainValue.Node.endOfIndent(src, offset) : PlainValue.Node.endOfWhiteSpace(src, offset);
  1382. let ch = src[offset];
  1383. while (ch === PlainValue.Char.ANCHOR || ch === PlainValue.Char.COMMENT || ch === PlainValue.Char.TAG || ch === '\n') {
  1384. if (ch === '\n') {
  1385. let inEnd = offset;
  1386. let lineStart;
  1387. do {
  1388. lineStart = inEnd + 1;
  1389. inEnd = PlainValue.Node.endOfIndent(src, lineStart);
  1390. } while (src[inEnd] === '\n');
  1391. const indentDiff = inEnd - (lineStart + this.indent);
  1392. const noIndicatorAsIndent = parent.type === PlainValue.Type.SEQ_ITEM && parent.context.atLineStart;
  1393. if (src[inEnd] !== '#' && !PlainValue.Node.nextNodeIsIndented(src[inEnd], indentDiff, !noIndicatorAsIndent)) break;
  1394. this.atLineStart = true;
  1395. this.lineStart = lineStart;
  1396. lineHasProps = false;
  1397. offset = inEnd;
  1398. } else if (ch === PlainValue.Char.COMMENT) {
  1399. const end = PlainValue.Node.endOfLine(src, offset + 1);
  1400. props.push(new PlainValue.Range(offset, end));
  1401. offset = end;
  1402. } else {
  1403. let end = PlainValue.Node.endOfIdentifier(src, offset + 1);
  1404. if (ch === PlainValue.Char.TAG && src[end] === ',' && /^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+,\d\d\d\d(-\d\d){0,2}\/\S/.test(src.slice(offset + 1, end + 13))) {
  1405. // Let's presume we're dealing with a YAML 1.0 domain tag here, rather
  1406. // than an empty but 'foo.bar' private-tagged node in a flow collection
  1407. // followed without whitespace by a plain string starting with a year
  1408. // or date divided by something.
  1409. end = PlainValue.Node.endOfIdentifier(src, end + 5);
  1410. }
  1411. props.push(new PlainValue.Range(offset, end));
  1412. lineHasProps = true;
  1413. offset = PlainValue.Node.endOfWhiteSpace(src, end);
  1414. }
  1415. ch = src[offset];
  1416. } // '- &a : b' has an anchor on an empty node
  1417. if (lineHasProps && ch === ':' && PlainValue.Node.atBlank(src, offset + 1, true)) offset -= 1;
  1418. const type = ParseContext.parseType(src, offset, inFlow);
  1419. return {
  1420. props,
  1421. type,
  1422. valueStart: offset
  1423. };
  1424. }
  1425. /**
  1426. * Parses a node from the source
  1427. * @param {ParseContext} overlay
  1428. * @param {number} start - Index of first non-whitespace character for the node
  1429. * @returns {?Node} - null if at a document boundary
  1430. */
  1431. }
  1432. // Published as 'yaml/parse-cst'
  1433. function parse(src) {
  1434. const cr = [];
  1435. if (src.indexOf('\r') !== -1) {
  1436. src = src.replace(/\r\n?/g, (match, offset) => {
  1437. if (match.length > 1) cr.push(offset);
  1438. return '\n';
  1439. });
  1440. }
  1441. const documents = [];
  1442. let offset = 0;
  1443. do {
  1444. const doc = new Document();
  1445. const context = new ParseContext({
  1446. src
  1447. });
  1448. offset = doc.parse(context, offset);
  1449. documents.push(doc);
  1450. } while (offset < src.length);
  1451. documents.setOrigRanges = () => {
  1452. if (cr.length === 0) return false;
  1453. for (let i = 1; i < cr.length; ++i) cr[i] -= i;
  1454. let crOffset = 0;
  1455. for (let i = 0; i < documents.length; ++i) {
  1456. crOffset = documents[i].setOrigRanges(cr, crOffset);
  1457. }
  1458. cr.splice(0, cr.length);
  1459. return true;
  1460. };
  1461. documents.toString = () => documents.join('...\n');
  1462. return documents;
  1463. }
  1464. exports.parse = parse;