123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import { Directives } from '../doc/directives.js';
- import { Document } from '../doc/Document.js';
- import { YAMLWarning, YAMLParseError } from '../errors.js';
- import { isCollection, isPair } from '../nodes/identity.js';
- import { composeDoc } from './compose-doc.js';
- import { resolveEnd } from './resolve-end.js';
- function getErrorPos(src) {
- if (typeof src === 'number')
- return [src, src + 1];
- if (Array.isArray(src))
- return src.length === 2 ? src : [src[0], src[1]];
- const { offset, source } = src;
- return [offset, offset + (typeof source === 'string' ? source.length : 1)];
- }
- function parsePrelude(prelude) {
- let comment = '';
- let atComment = false;
- let afterEmptyLine = false;
- for (let i = 0; i < prelude.length; ++i) {
- const source = prelude[i];
- switch (source[0]) {
- case '#':
- comment +=
- (comment === '' ? '' : afterEmptyLine ? '\n\n' : '\n') +
- (source.substring(1) || ' ');
- atComment = true;
- afterEmptyLine = false;
- break;
- case '%':
- if (prelude[i + 1]?.[0] !== '#')
- i += 1;
- atComment = false;
- break;
- default:
- // This may be wrong after doc-end, but in that case it doesn't matter
- if (!atComment)
- afterEmptyLine = true;
- atComment = false;
- }
- }
- return { comment, afterEmptyLine };
- }
- /**
- * Compose a stream of CST nodes into a stream of YAML Documents.
- *
- * ```ts
- * import { Composer, Parser } from 'yaml'
- *
- * const src: string = ...
- * const tokens = new Parser().parse(src)
- * const docs = new Composer().compose(tokens)
- * ```
- */
- class Composer {
- constructor(options = {}) {
- this.doc = null;
- this.atDirectives = false;
- this.prelude = [];
- this.errors = [];
- this.warnings = [];
- this.onError = (source, code, message, warning) => {
- const pos = getErrorPos(source);
- if (warning)
- this.warnings.push(new YAMLWarning(pos, code, message));
- else
- this.errors.push(new YAMLParseError(pos, code, message));
- };
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- this.directives = new Directives({ version: options.version || '1.2' });
- this.options = options;
- }
- decorate(doc, afterDoc) {
- const { comment, afterEmptyLine } = parsePrelude(this.prelude);
- //console.log({ dc: doc.comment, prelude, comment })
- if (comment) {
- const dc = doc.contents;
- if (afterDoc) {
- doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment;
- }
- else if (afterEmptyLine || doc.directives.docStart || !dc) {
- doc.commentBefore = comment;
- }
- else if (isCollection(dc) && !dc.flow && dc.items.length > 0) {
- let it = dc.items[0];
- if (isPair(it))
- it = it.key;
- const cb = it.commentBefore;
- it.commentBefore = cb ? `${comment}\n${cb}` : comment;
- }
- else {
- const cb = dc.commentBefore;
- dc.commentBefore = cb ? `${comment}\n${cb}` : comment;
- }
- }
- if (afterDoc) {
- Array.prototype.push.apply(doc.errors, this.errors);
- Array.prototype.push.apply(doc.warnings, this.warnings);
- }
- else {
- doc.errors = this.errors;
- doc.warnings = this.warnings;
- }
- this.prelude = [];
- this.errors = [];
- this.warnings = [];
- }
- /**
- * Current stream status information.
- *
- * Mostly useful at the end of input for an empty stream.
- */
- streamInfo() {
- return {
- comment: parsePrelude(this.prelude).comment,
- directives: this.directives,
- errors: this.errors,
- warnings: this.warnings
- };
- }
- /**
- * Compose tokens into documents.
- *
- * @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.
- * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
- */
- *compose(tokens, forceDoc = false, endOffset = -1) {
- for (const token of tokens)
- yield* this.next(token);
- yield* this.end(forceDoc, endOffset);
- }
- /** Advance the composer by one CST token. */
- *next(token) {
- switch (token.type) {
- case 'directive':
- this.directives.add(token.source, (offset, message, warning) => {
- const pos = getErrorPos(token);
- pos[0] += offset;
- this.onError(pos, 'BAD_DIRECTIVE', message, warning);
- });
- this.prelude.push(token.source);
- this.atDirectives = true;
- break;
- case 'document': {
- const doc = composeDoc(this.options, this.directives, token, this.onError);
- if (this.atDirectives && !doc.directives.docStart)
- this.onError(token, 'MISSING_CHAR', 'Missing directives-end/doc-start indicator line');
- this.decorate(doc, false);
- if (this.doc)
- yield this.doc;
- this.doc = doc;
- this.atDirectives = false;
- break;
- }
- case 'byte-order-mark':
- case 'space':
- break;
- case 'comment':
- case 'newline':
- this.prelude.push(token.source);
- break;
- case 'error': {
- const msg = token.source
- ? `${token.message}: ${JSON.stringify(token.source)}`
- : token.message;
- const error = new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg);
- if (this.atDirectives || !this.doc)
- this.errors.push(error);
- else
- this.doc.errors.push(error);
- break;
- }
- case 'doc-end': {
- if (!this.doc) {
- const msg = 'Unexpected doc-end without preceding document';
- this.errors.push(new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg));
- break;
- }
- this.doc.directives.docEnd = true;
- const end = resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError);
- this.decorate(this.doc, true);
- if (end.comment) {
- const dc = this.doc.comment;
- this.doc.comment = dc ? `${dc}\n${end.comment}` : end.comment;
- }
- this.doc.range[2] = end.offset;
- break;
- }
- default:
- this.errors.push(new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', `Unsupported token ${token.type}`));
- }
- }
- /**
- * Call at end of input to yield any remaining document.
- *
- * @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.
- * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
- */
- *end(forceDoc = false, endOffset = -1) {
- if (this.doc) {
- this.decorate(this.doc, true);
- yield this.doc;
- this.doc = null;
- }
- else if (forceDoc) {
- const opts = Object.assign({ _directives: this.directives }, this.options);
- const doc = new Document(undefined, opts);
- if (this.atDirectives)
- this.onError(endOffset, 'MISSING_CHAR', 'Missing directives-end indicator line');
- doc.range = [0, endOffset, endOffset];
- this.decorate(doc, false);
- yield doc;
- }
- }
- }
- export { Composer };
|