UNPKG

8.56 kBJavaScriptView Raw
1'use strict';
2
3var directives = require('../doc/directives.js');
4var Document = require('../doc/Document.js');
5var errors = require('../errors.js');
6var identity = require('../nodes/identity.js');
7var composeDoc = require('./compose-doc.js');
8var resolveEnd = require('./resolve-end.js');
9
10function getErrorPos(src) {
11 if (typeof src === 'number')
12 return [src, src + 1];
13 if (Array.isArray(src))
14 return src.length === 2 ? src : [src[0], src[1]];
15 const { offset, source } = src;
16 return [offset, offset + (typeof source === 'string' ? source.length : 1)];
17}
18function parsePrelude(prelude) {
19 let comment = '';
20 let atComment = false;
21 let afterEmptyLine = false;
22 for (let i = 0; i < prelude.length; ++i) {
23 const source = prelude[i];
24 switch (source[0]) {
25 case '#':
26 comment +=
27 (comment === '' ? '' : afterEmptyLine ? '\n\n' : '\n') +
28 (source.substring(1) || ' ');
29 atComment = true;
30 afterEmptyLine = false;
31 break;
32 case '%':
33 if (prelude[i + 1]?.[0] !== '#')
34 i += 1;
35 atComment = false;
36 break;
37 default:
38 // This may be wrong after doc-end, but in that case it doesn't matter
39 if (!atComment)
40 afterEmptyLine = true;
41 atComment = false;
42 }
43 }
44 return { comment, afterEmptyLine };
45}
46/**
47 * Compose a stream of CST nodes into a stream of YAML Documents.
48 *
49 * ```ts
50 * import { Composer, Parser } from 'yaml'
51 *
52 * const src: string = ...
53 * const tokens = new Parser().parse(src)
54 * const docs = new Composer().compose(tokens)
55 * ```
56 */
57class Composer {
58 constructor(options = {}) {
59 this.doc = null;
60 this.atDirectives = false;
61 this.prelude = [];
62 this.errors = [];
63 this.warnings = [];
64 this.onError = (source, code, message, warning) => {
65 const pos = getErrorPos(source);
66 if (warning)
67 this.warnings.push(new errors.YAMLWarning(pos, code, message));
68 else
69 this.errors.push(new errors.YAMLParseError(pos, code, message));
70 };
71 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
72 this.directives = new directives.Directives({ version: options.version || '1.2' });
73 this.options = options;
74 }
75 decorate(doc, afterDoc) {
76 const { comment, afterEmptyLine } = parsePrelude(this.prelude);
77 //console.log({ dc: doc.comment, prelude, comment })
78 if (comment) {
79 const dc = doc.contents;
80 if (afterDoc) {
81 doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment;
82 }
83 else if (afterEmptyLine || doc.directives.docStart || !dc) {
84 doc.commentBefore = comment;
85 }
86 else if (identity.isCollection(dc) && !dc.flow && dc.items.length > 0) {
87 let it = dc.items[0];
88 if (identity.isPair(it))
89 it = it.key;
90 const cb = it.commentBefore;
91 it.commentBefore = cb ? `${comment}\n${cb}` : comment;
92 }
93 else {
94 const cb = dc.commentBefore;
95 dc.commentBefore = cb ? `${comment}\n${cb}` : comment;
96 }
97 }
98 if (afterDoc) {
99 Array.prototype.push.apply(doc.errors, this.errors);
100 Array.prototype.push.apply(doc.warnings, this.warnings);
101 }
102 else {
103 doc.errors = this.errors;
104 doc.warnings = this.warnings;
105 }
106 this.prelude = [];
107 this.errors = [];
108 this.warnings = [];
109 }
110 /**
111 * Current stream status information.
112 *
113 * Mostly useful at the end of input for an empty stream.
114 */
115 streamInfo() {
116 return {
117 comment: parsePrelude(this.prelude).comment,
118 directives: this.directives,
119 errors: this.errors,
120 warnings: this.warnings
121 };
122 }
123 /**
124 * Compose tokens into documents.
125 *
126 * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
127 * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
128 */
129 *compose(tokens, forceDoc = false, endOffset = -1) {
130 for (const token of tokens)
131 yield* this.next(token);
132 yield* this.end(forceDoc, endOffset);
133 }
134 /** Advance the composer by one CST token. */
135 *next(token) {
136 if (process.env.LOG_STREAM)
137 console.dir(token, { depth: null });
138 switch (token.type) {
139 case 'directive':
140 this.directives.add(token.source, (offset, message, warning) => {
141 const pos = getErrorPos(token);
142 pos[0] += offset;
143 this.onError(pos, 'BAD_DIRECTIVE', message, warning);
144 });
145 this.prelude.push(token.source);
146 this.atDirectives = true;
147 break;
148 case 'document': {
149 const doc = composeDoc.composeDoc(this.options, this.directives, token, this.onError);
150 if (this.atDirectives && !doc.directives.docStart)
151 this.onError(token, 'MISSING_CHAR', 'Missing directives-end/doc-start indicator line');
152 this.decorate(doc, false);
153 if (this.doc)
154 yield this.doc;
155 this.doc = doc;
156 this.atDirectives = false;
157 break;
158 }
159 case 'byte-order-mark':
160 case 'space':
161 break;
162 case 'comment':
163 case 'newline':
164 this.prelude.push(token.source);
165 break;
166 case 'error': {
167 const msg = token.source
168 ? `${token.message}: ${JSON.stringify(token.source)}`
169 : token.message;
170 const error = new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg);
171 if (this.atDirectives || !this.doc)
172 this.errors.push(error);
173 else
174 this.doc.errors.push(error);
175 break;
176 }
177 case 'doc-end': {
178 if (!this.doc) {
179 const msg = 'Unexpected doc-end without preceding document';
180 this.errors.push(new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg));
181 break;
182 }
183 this.doc.directives.docEnd = true;
184 const end = resolveEnd.resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError);
185 this.decorate(this.doc, true);
186 if (end.comment) {
187 const dc = this.doc.comment;
188 this.doc.comment = dc ? `${dc}\n${end.comment}` : end.comment;
189 }
190 this.doc.range[2] = end.offset;
191 break;
192 }
193 default:
194 this.errors.push(new errors.YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', `Unsupported token ${token.type}`));
195 }
196 }
197 /**
198 * Call at end of input to yield any remaining document.
199 *
200 * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
201 * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
202 */
203 *end(forceDoc = false, endOffset = -1) {
204 if (this.doc) {
205 this.decorate(this.doc, true);
206 yield this.doc;
207 this.doc = null;
208 }
209 else if (forceDoc) {
210 const opts = Object.assign({ _directives: this.directives }, this.options);
211 const doc = new Document.Document(undefined, opts);
212 if (this.atDirectives)
213 this.onError(endOffset, 'MISSING_CHAR', 'Missing directives-end indicator line');
214 doc.range = [0, endOffset, endOffset];
215 this.decorate(doc, false);
216 yield doc;
217 }
218 }
219}
220
221exports.Composer = Composer;