source: frontend/node_modules/yaml/dist/parse-cst.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

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