composer.js 8.4 KB

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