'use strict'; var PlainValue = require('./PlainValue-ec8e588e.js'); function addCommentBefore(str, indent, comment) { if (!comment) return str; const cc = comment.replace(/[\s\S]^/gm, `$&${indent}#`); return `#${cc}\n${indent}${str}`; } function addComment(str, indent, comment) { return !comment ? str : comment.indexOf('\n') === -1 ? `${str} #${comment}` : `${str}\n` + comment.replace(/^/gm, `${indent || ''}#`); } class Node {} function toJSON(value, arg, ctx) { if (Array.isArray(value)) return value.map((v, i) => toJSON(v, String(i), ctx)); if (value && typeof value.toJSON === 'function') { const anchor = ctx && ctx.anchors && ctx.anchors.get(value); if (anchor) ctx.onCreate = res => { anchor.res = res; delete ctx.onCreate; }; const res = value.toJSON(arg, ctx); if (anchor && ctx.onCreate) ctx.onCreate(res); return res; } if ((!ctx || !ctx.keep) && typeof value === 'bigint') return Number(value); return value; } class Scalar extends Node { constructor(value) { super(); this.value = value; } toJSON(arg, ctx) { return ctx && ctx.keep ? this.value : toJSON(this.value, arg, ctx); } toString() { return String(this.value); } } function collectionFromPath(schema, path, value) { let v = value; for (let i = path.length - 1; i >= 0; --i) { const k = path[i]; if (Number.isInteger(k) && k >= 0) { const a = []; a[k] = v; v = a; } else { const o = {}; Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true }); v = o; } } return schema.createNode(v, false); } // null, undefined, or an empty non-string iterable (e.g. []) const isEmptyPath = path => path == null || typeof path === 'object' && path[Symbol.iterator]().next().done; class Collection extends Node { constructor(schema) { super(); PlainValue._defineProperty(this, "items", []); this.schema = schema; } addIn(path, value) { if (isEmptyPath(path)) this.add(value);else { const [key, ...rest] = path; const node = this.get(key, true); if (node instanceof Collection) node.addIn(rest, value);else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value));else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); } } deleteIn([key, ...rest]) { if (rest.length === 0) return this.delete(key); const node = this.get(key, true); if (node instanceof Collection) return node.deleteIn(rest);else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); } getIn([key, ...rest], keepScalar) { const node = this.get(key, true); if (rest.length === 0) return !keepScalar && node instanceof Scalar ? node.value : node;else return node instanceof Collection ? node.getIn(rest, keepScalar) : undefined; } hasAllNullValues() { return this.items.every(node => { if (!node || node.type !== 'PAIR') return false; const n = node.value; return n == null || n instanceof Scalar && n.value == null && !n.commentBefore && !n.comment && !n.tag; }); } hasIn([key, ...rest]) { if (rest.length === 0) return this.has(key); const node = this.get(key, true); return node instanceof Collection ? node.hasIn(rest) : false; } setIn([key, ...rest], value) { if (rest.length === 0) { this.set(key, value); } else { const node = this.get(key, true); if (node instanceof Collection) node.setIn(rest, value);else if (node === undefined && this.schema) this.set(key, collectionFromPath(this.schema, rest, value));else throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); } } // overridden in implementations /* istanbul ignore next */ toJSON() { return null; } toString(ctx, { blockItem, flowChars, isMap, itemIndent }, onComment, onChompKeep) { const { indent, indentStep, stringify } = ctx; const inFlow = this.type === PlainValue.Type.FLOW_MAP || this.type === PlainValue.Type.FLOW_SEQ || ctx.inFlow; if (inFlow) itemIndent += indentStep; const allNullValues = isMap && this.hasAllNullValues(); ctx = Object.assign({}, ctx, { allNullValues, indent: itemIndent, inFlow, type: null }); let chompKeep = false; let hasItemWithNewLine = false; const nodes = this.items.reduce((nodes, item, i) => { let comment; if (item) { if (!chompKeep && item.spaceBefore) nodes.push({ type: 'comment', str: '' }); if (item.commentBefore) item.commentBefore.match(/^.*$/gm).forEach(line => { nodes.push({ type: 'comment', str: `#${line}` }); }); if (item.comment) comment = item.comment; if (inFlow && (!chompKeep && item.spaceBefore || item.commentBefore || item.comment || item.key && (item.key.commentBefore || item.key.comment) || item.value && (item.value.commentBefore || item.value.comment))) hasItemWithNewLine = true; } chompKeep = false; let str = stringify(item, ctx, () => comment = null, () => chompKeep = true); if (inFlow && !hasItemWithNewLine && str.includes('\n')) hasItemWithNewLine = true; if (inFlow && i < this.items.length - 1) str += ','; str = addComment(str, itemIndent, comment); if (chompKeep && (comment || inFlow)) chompKeep = false; nodes.push({ type: 'item', str }); return nodes; }, []); let str; if (nodes.length === 0) { str = flowChars.start + flowChars.end; } else if (inFlow) { const { start, end } = flowChars; const strings = nodes.map(n => n.str); if (hasItemWithNewLine || strings.reduce((sum, str) => sum + str.length + 2, 2) > Collection.maxFlowStringSingleLineLength) { str = start; for (const s of strings) { str += s ? `\n${indentStep}${indent}${s}` : '\n'; } str += `\n${indent}${end}`; } else { str = `${start} ${strings.join(' ')} ${end}`; } } else { const strings = nodes.map(blockItem); str = strings.shift(); for (const s of strings) str += s ? `\n${indent}${s}` : '\n'; } if (this.comment) { str += '\n' + this.comment.replace(/^/gm, `${indent}#`); if (onComment) onComment(); } else if (chompKeep && onChompKeep) onChompKeep(); return str; } } PlainValue._defineProperty(Collection, "maxFlowStringSingleLineLength", 60); function asItemIndex(key) { let idx = key instanceof Scalar ? key.value : key; if (idx && typeof idx === 'string') idx = Number(idx); return Number.isInteger(idx) && idx >= 0 ? idx : null; } class YAMLSeq extends Collection { add(value) { this.items.push(value); } delete(key) { const idx = asItemIndex(key); if (typeof idx !== 'number') return false; const del = this.items.splice(idx, 1); return del.length > 0; } get(key, keepScalar) { const idx = asItemIndex(key); if (typeof idx !== 'number') return undefined; const it = this.items[idx]; return !keepScalar && it instanceof Scalar ? it.value : it; } has(key) { const idx = asItemIndex(key); return typeof idx === 'number' && idx < this.items.length; } set(key, value) { const idx = asItemIndex(key); if (typeof idx !== 'number') throw new Error(`Expected a valid index, not ${key}.`); this.items[idx] = value; } toJSON(_, ctx) { const seq = []; if (ctx && ctx.onCreate) ctx.onCreate(seq); let i = 0; for (const item of this.items) seq.push(toJSON(item, String(i++), ctx)); return seq; } toString(ctx, onComment, onChompKeep) { if (!ctx) return JSON.stringify(this); return super.toString(ctx, { blockItem: n => n.type === 'comment' ? n.str : `- ${n.str}`, flowChars: { start: '[', end: ']' }, isMap: false, itemIndent: (ctx.indent || '') + ' ' }, onComment, onChompKeep); } } const stringifyKey = (key, jsKey, ctx) => { if (jsKey === null) return ''; if (typeof jsKey !== 'object') return String(jsKey); if (key instanceof Node && ctx && ctx.doc) return key.toString({ anchors: Object.create(null), doc: ctx.doc, indent: '', indentStep: ctx.indentStep, inFlow: true, inStringifyKey: true, stringify: ctx.stringify }); return JSON.stringify(jsKey); }; class Pair extends Node { constructor(key, value = null) { super(); this.key = key; this.value = value; this.type = Pair.Type.PAIR; } get commentBefore() { return this.key instanceof Node ? this.key.commentBefore : undefined; } set commentBefore(cb) { if (this.key == null) this.key = new Scalar(null); if (this.key instanceof Node) this.key.commentBefore = cb;else { const msg = 'Pair.commentBefore is an alias for Pair.key.commentBefore. To set it, the key must be a Node.'; throw new Error(msg); } } addToJSMap(ctx, map) { const key = toJSON(this.key, '', ctx); if (map instanceof Map) { const value = toJSON(this.value, key, ctx); map.set(key, value); } else if (map instanceof Set) { map.add(key); } else { const stringKey = stringifyKey(this.key, key, ctx); const value = toJSON(this.value, stringKey, ctx); if (stringKey in map) Object.defineProperty(map, stringKey, { value, writable: true, enumerable: true, configurable: true });else map[stringKey] = value; } return map; } toJSON(_, ctx) { const pair = ctx && ctx.mapAsMap ? new Map() : {}; return this.addToJSMap(ctx, pair); } toString(ctx, onComment, onChompKeep) { if (!ctx || !ctx.doc) return JSON.stringify(this); const { indent: indentSize, indentSeq, simpleKeys } = ctx.doc.options; let { key, value } = this; let keyComment = key instanceof Node && key.comment; if (simpleKeys) { if (keyComment) { throw new Error('With simple keys, key nodes cannot have comments'); } if (key instanceof Collection) { const msg = 'With simple keys, collection cannot be used as a key value'; throw new Error(msg); } } let explicitKey = !simpleKeys && (!key || keyComment || (key instanceof Node ? key instanceof Collection || key.type === PlainValue.Type.BLOCK_FOLDED || key.type === PlainValue.Type.BLOCK_LITERAL : typeof key === 'object')); const { doc, indent, indentStep, stringify } = ctx; ctx = Object.assign({}, ctx, { implicitKey: !explicitKey, indent: indent + indentStep }); let chompKeep = false; let str = stringify(key, ctx, () => keyComment = null, () => chompKeep = true); str = addComment(str, ctx.indent, keyComment); if (!explicitKey && str.length > 1024) { if (simpleKeys) throw new Error('With simple keys, single line scalar must not span more than 1024 characters'); explicitKey = true; } if (ctx.allNullValues && !simpleKeys) { if (this.comment) { str = addComment(str, ctx.indent, this.comment); if (onComment) onComment(); } else if (chompKeep && !keyComment && onChompKeep) onChompKeep(); return ctx.inFlow && !explicitKey ? str : `? ${str}`; } str = explicitKey ? `? ${str}\n${indent}:` : `${str}:`; if (this.comment) { // expected (but not strictly required) to be a single-line comment str = addComment(str, ctx.indent, this.comment); if (onComment) onComment(); } let vcb = ''; let valueComment = null; if (value instanceof Node) { if (value.spaceBefore) vcb = '\n'; if (value.commentBefore) { const cs = value.commentBefore.replace(/^/gm, `${ctx.indent}#`); vcb += `\n${cs}`; } valueComment = value.comment; } else if (value && typeof value === 'object') { value = doc.schema.createNode(value, true); } ctx.implicitKey = false; if (!explicitKey && !this.comment && value instanceof Scalar) ctx.indentAtStart = str.length + 1; chompKeep = false; if (!indentSeq && indentSize >= 2 && !ctx.inFlow && !explicitKey && value instanceof YAMLSeq && value.type !== PlainValue.Type.FLOW_SEQ && !value.tag && !doc.anchors.getName(value)) { // If indentSeq === false, consider '- ' as part of indentation where possible ctx.indent = ctx.indent.substr(2); } const valueStr = stringify(value, ctx, () => valueComment = null, () => chompKeep = true); let ws = ' '; if (vcb || this.comment) { ws = `${vcb}\n${ctx.indent}`; } else if (!explicitKey && value instanceof Collection) { const flow = valueStr[0] === '[' || valueStr[0] === '{'; if (!flow || valueStr.includes('\n')) ws = `\n${ctx.indent}`; } else if (valueStr[0] === '\n') ws = ''; if (chompKeep && !valueComment && onChompKeep) onChompKeep(); return addComment(str + ws + valueStr, ctx.indent, valueComment); } } PlainValue._defineProperty(Pair, "Type", { PAIR: 'PAIR', MERGE_PAIR: 'MERGE_PAIR' }); const getAliasCount = (node, anchors) => { if (node instanceof Alias) { const anchor = anchors.get(node.source); return anchor.count * anchor.aliasCount; } else if (node instanceof Collection) { let count = 0; for (const item of node.items) { const c = getAliasCount(item, anchors); if (c > count) count = c; } return count; } else if (node instanceof Pair) { const kc = getAliasCount(node.key, anchors); const vc = getAliasCount(node.value, anchors); return Math.max(kc, vc); } return 1; }; class Alias extends Node { static stringify({ range, source }, { anchors, doc, implicitKey, inStringifyKey }) { let anchor = Object.keys(anchors).find(a => anchors[a] === source); if (!anchor && inStringifyKey) anchor = doc.anchors.getName(source) || doc.anchors.newName(); if (anchor) return `*${anchor}${implicitKey ? ' ' : ''}`; const msg = doc.anchors.getName(source) ? 'Alias node must be after source node' : 'Source node not found for alias node'; throw new Error(`${msg} [${range}]`); } constructor(source) { super(); this.source = source; this.type = PlainValue.Type.ALIAS; } set tag(t) { throw new Error('Alias nodes cannot have tags'); } toJSON(arg, ctx) { if (!ctx) return toJSON(this.source, arg, ctx); const { anchors, maxAliasCount } = ctx; const anchor = anchors.get(this.source); /* istanbul ignore if */ if (!anchor || anchor.res === undefined) { const msg = 'This should not happen: Alias anchor was not resolved?'; if (this.cstNode) throw new PlainValue.YAMLReferenceError(this.cstNode, msg);else throw new ReferenceError(msg); } if (maxAliasCount >= 0) { anchor.count += 1; if (anchor.aliasCount === 0) anchor.aliasCount = getAliasCount(this.source, anchors); if (anchor.count * anchor.aliasCount > maxAliasCount) { const msg = 'Excessive alias count indicates a resource exhaustion attack'; if (this.cstNode) throw new PlainValue.YAMLReferenceError(this.cstNode, msg);else throw new ReferenceError(msg); } } return anchor.res; } // Only called when stringifying an alias mapping key while constructing // Object output. toString(ctx) { return Alias.stringify(this, ctx); } } PlainValue._defineProperty(Alias, "default", true); function findPair(items, key) { const k = key instanceof Scalar ? key.value : key; for (const it of items) { if (it instanceof Pair) { if (it.key === key || it.key === k) return it; if (it.key && it.key.value === k) return it; } } return undefined; } class YAMLMap extends Collection { add(pair, overwrite) { if (!pair) pair = new Pair(pair);else if (!(pair instanceof Pair)) pair = new Pair(pair.key || pair, pair.value); const prev = findPair(this.items, pair.key); const sortEntries = this.schema && this.schema.sortMapEntries; if (prev) { if (overwrite) prev.value = pair.value;else throw new Error(`Key ${pair.key} already set`); } else if (sortEntries) { const i = this.items.findIndex(item => sortEntries(pair, item) < 0); if (i === -1) this.items.push(pair);else this.items.splice(i, 0, pair); } else { this.items.push(pair); } } delete(key) { const it = findPair(this.items, key); if (!it) return false; const del = this.items.splice(this.items.indexOf(it), 1); return del.length > 0; } get(key, keepScalar) { const it = findPair(this.items, key); const node = it && it.value; return !keepScalar && node instanceof Scalar ? node.value : node; } has(key) { return !!findPair(this.items, key); } set(key, value) { this.add(new Pair(key, value), true); } /** * @param {*} arg ignored * @param {*} ctx Conversion context, originally set in Document#toJSON() * @param {Class} Type If set, forces the returned collection type * @returns {*} Instance of Type, Map, or Object */ toJSON(_, ctx, Type) { const map = Type ? new Type() : ctx && ctx.mapAsMap ? new Map() : {}; if (ctx && ctx.onCreate) ctx.onCreate(map); for (const item of this.items) item.addToJSMap(ctx, map); return map; } toString(ctx, onComment, onChompKeep) { if (!ctx) return JSON.stringify(this); for (const item of this.items) { if (!(item instanceof Pair)) throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`); } return super.toString(ctx, { blockItem: n => n.str, flowChars: { start: '{', end: '}' }, isMap: true, itemIndent: ctx.indent || '' }, onComment, onChompKeep); } } const MERGE_KEY = '<<'; class Merge extends Pair { constructor(pair) { if (pair instanceof Pair) { let seq = pair.value; if (!(seq instanceof YAMLSeq)) { seq = new YAMLSeq(); seq.items.push(pair.value); seq.range = pair.value.range; } super(pair.key, seq); this.range = pair.range; } else { super(new Scalar(MERGE_KEY), new YAMLSeq()); } this.type = Pair.Type.MERGE_PAIR; } // If the value associated with a merge key is a single mapping node, each of // its key/value pairs is inserted into the current mapping, unless the key // already exists in it. If the value associated with the merge key is a // sequence, then this sequence is expected to contain mapping nodes and each // of these nodes is merged in turn according to its order in the sequence. // Keys in mapping nodes earlier in the sequence override keys specified in // later mapping nodes. -- http://yaml.org/type/merge.html addToJSMap(ctx, map) { for (const { source } of this.value.items) { if (!(source instanceof YAMLMap)) throw new Error('Merge sources must be maps'); const srcMap = source.toJSON(null, ctx, Map); for (const [key, value] of srcMap) { if (map instanceof Map) { if (!map.has(key)) map.set(key, value); } else if (map instanceof Set) { map.add(key); } else if (!Object.prototype.hasOwnProperty.call(map, key)) { Object.defineProperty(map, key, { value, writable: true, enumerable: true, configurable: true }); } } } return map; } toString(ctx, onComment) { const seq = this.value; if (seq.items.length > 1) return super.toString(ctx, onComment); this.value = seq.items[0]; const str = super.toString(ctx, onComment); this.value = seq; return str; } } const binaryOptions = { defaultType: PlainValue.Type.BLOCK_LITERAL, lineWidth: 76 }; const boolOptions = { trueStr: 'true', falseStr: 'false' }; const intOptions = { asBigInt: false }; const nullOptions = { nullStr: 'null' }; const strOptions = { defaultType: PlainValue.Type.PLAIN, doubleQuoted: { jsonEncoding: false, minMultiLineLength: 40 }, fold: { lineWidth: 80, minContentWidth: 20 } }; function resolveScalar(str, tags, scalarFallback) { for (const { format, test, resolve } of tags) { if (test) { const match = str.match(test); if (match) { let res = resolve.apply(null, match); if (!(res instanceof Scalar)) res = new Scalar(res); if (format) res.format = format; return res; } } } if (scalarFallback) str = scalarFallback(str); return new Scalar(str); } const FOLD_FLOW = 'flow'; const FOLD_BLOCK = 'block'; const FOLD_QUOTED = 'quoted'; // presumes i+1 is at the start of a line // returns index of last newline in more-indented block const consumeMoreIndentedLines = (text, i) => { let ch = text[i + 1]; while (ch === ' ' || ch === '\t') { do { ch = text[i += 1]; } while (ch && ch !== '\n'); ch = text[i + 1]; } return i; }; /** * Tries to keep input at up to `lineWidth` characters, splitting only on spaces * not followed by newlines or spaces unless `mode` is `'quoted'`. Lines are * terminated with `\n` and started with `indent`. * * @param {string} text * @param {string} indent * @param {string} [mode='flow'] `'block'` prevents more-indented lines * from being folded; `'quoted'` allows for `\` escapes, including escaped * newlines * @param {Object} options * @param {number} [options.indentAtStart] Accounts for leading contents on * the first line, defaulting to `indent.length` * @param {number} [options.lineWidth=80] * @param {number} [options.minContentWidth=20] Allow highly indented lines to * stretch the line width or indent content from the start * @param {function} options.onFold Called once if the text is folded * @param {function} options.onFold Called once if any line of text exceeds * lineWidth characters */ function foldFlowLines(text, indent, mode, { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow }) { if (!lineWidth || lineWidth < 0) return text; const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length); if (text.length <= endStep) return text; const folds = []; const escapedFolds = {}; let end = lineWidth - indent.length; if (typeof indentAtStart === 'number') { if (indentAtStart > lineWidth - Math.max(2, minContentWidth)) folds.push(0);else end = lineWidth - indentAtStart; } let split = undefined; let prev = undefined; let overflow = false; let i = -1; let escStart = -1; let escEnd = -1; if (mode === FOLD_BLOCK) { i = consumeMoreIndentedLines(text, i); if (i !== -1) end = i + endStep; } for (let ch; ch = text[i += 1];) { if (mode === FOLD_QUOTED && ch === '\\') { escStart = i; switch (text[i + 1]) { case 'x': i += 3; break; case 'u': i += 5; break; case 'U': i += 9; break; default: i += 1; } escEnd = i; } if (ch === '\n') { if (mode === FOLD_BLOCK) i = consumeMoreIndentedLines(text, i); end = i + endStep; split = undefined; } else { if (ch === ' ' && prev && prev !== ' ' && prev !== '\n' && prev !== '\t') { // space surrounded by non-space can be replaced with newline + indent const next = text[i + 1]; if (next && next !== ' ' && next !== '\n' && next !== '\t') split = i; } if (i >= end) { if (split) { folds.push(split); end = split + endStep; split = undefined; } else if (mode === FOLD_QUOTED) { // white-space collected at end may stretch past lineWidth while (prev === ' ' || prev === '\t') { prev = ch; ch = text[i += 1]; overflow = true; } // Account for newline escape, but don't break preceding escape const j = i > escEnd + 1 ? i - 2 : escStart - 1; // Bail out if lineWidth & minContentWidth are shorter than an escape string if (escapedFolds[j]) return text; folds.push(j); escapedFolds[j] = true; end = j + endStep; split = undefined; } else { overflow = true; } } } prev = ch; } if (overflow && onOverflow) onOverflow(); if (folds.length === 0) return text; if (onFold) onFold(); let res = text.slice(0, folds[0]); for (let i = 0; i < folds.length; ++i) { const fold = folds[i]; const end = folds[i + 1] || text.length; if (fold === 0) res = `\n${indent}${text.slice(0, end)}`;else { if (mode === FOLD_QUOTED && escapedFolds[fold]) res += `${text[fold]}\\`; res += `\n${indent}${text.slice(fold + 1, end)}`; } } return res; } const getFoldOptions = ({ indentAtStart }) => indentAtStart ? Object.assign({ indentAtStart }, strOptions.fold) : strOptions.fold; // Also checks for lines starting with %, as parsing the output as YAML 1.1 will // presume that's starting a new document. const containsDocumentMarker = str => /^(%|---|\.\.\.)/m.test(str); function lineLengthOverLimit(str, lineWidth, indentLength) { if (!lineWidth || lineWidth < 0) return false; const limit = lineWidth - indentLength; const strLen = str.length; if (strLen <= limit) return false; for (let i = 0, start = 0; i < strLen; ++i) { if (str[i] === '\n') { if (i - start > limit) return true; start = i + 1; if (strLen - start <= limit) return false; } } return true; } function doubleQuotedString(value, ctx) { const { implicitKey } = ctx; const { jsonEncoding, minMultiLineLength } = strOptions.doubleQuoted; const json = JSON.stringify(value); if (jsonEncoding) return json; const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : ''); let str = ''; let start = 0; for (let i = 0, ch = json[i]; ch; ch = json[++i]) { if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') { // space before newline needs to be escaped to not be folded str += json.slice(start, i) + '\\ '; i += 1; start = i; ch = '\\'; } if (ch === '\\') switch (json[i + 1]) { case 'u': { str += json.slice(start, i); const code = json.substr(i + 2, 4); switch (code) { case '0000': str += '\\0'; break; case '0007': str += '\\a'; break; case '000b': str += '\\v'; break; case '001b': str += '\\e'; break; case '0085': str += '\\N'; break; case '00a0': str += '\\_'; break; case '2028': str += '\\L'; break; case '2029': str += '\\P'; break; default: if (code.substr(0, 2) === '00') str += '\\x' + code.substr(2);else str += json.substr(i, 6); } i += 5; start = i + 1; } break; case 'n': if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) { i += 1; } else { // folding will eat first newline str += json.slice(start, i) + '\n\n'; while (json[i + 2] === '\\' && json[i + 3] === 'n' && json[i + 4] !== '"') { str += '\n'; i += 2; } str += indent; // space after newline needs to be escaped to not be folded if (json[i + 2] === ' ') str += '\\'; i += 1; start = i + 1; } break; default: i += 1; } } str = start ? str + json.slice(start) : json; return implicitKey ? str : foldFlowLines(str, indent, FOLD_QUOTED, getFoldOptions(ctx)); } function singleQuotedString(value, ctx) { if (ctx.implicitKey) { if (/\n/.test(value)) return doubleQuotedString(value, ctx); } else { // single quoted string can't have leading or trailing whitespace around newline if (/[ \t]\n|\n[ \t]/.test(value)) return doubleQuotedString(value, ctx); } const indent = ctx.indent || (containsDocumentMarker(value) ? ' ' : ''); const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'"; return ctx.implicitKey ? res : foldFlowLines(res, indent, FOLD_FLOW, getFoldOptions(ctx)); } function blockString({ comment, type, value }, ctx, onComment, onChompKeep) { // 1. Block can't end in whitespace unless the last line is non-empty. // 2. Strings consisting of only whitespace are best rendered explicitly. if (/\n[\t ]+$/.test(value) || /^\s*$/.test(value)) { return doubleQuotedString(value, ctx); } const indent = ctx.indent || (ctx.forceBlockIndent || containsDocumentMarker(value) ? ' ' : ''); const indentSize = indent ? '2' : '1'; // root is at -1 const literal = type === PlainValue.Type.BLOCK_FOLDED ? false : type === PlainValue.Type.BLOCK_LITERAL ? true : !lineLengthOverLimit(value, strOptions.fold.lineWidth, indent.length); let header = literal ? '|' : '>'; if (!value) return header + '\n'; let wsStart = ''; let wsEnd = ''; value = value.replace(/[\n\t ]*$/, ws => { const n = ws.indexOf('\n'); if (n === -1) { header += '-'; // strip } else if (value === ws || n !== ws.length - 1) { header += '+'; // keep if (onChompKeep) onChompKeep(); } wsEnd = ws.replace(/\n$/, ''); return ''; }).replace(/^[\n ]*/, ws => { if (ws.indexOf(' ') !== -1) header += indentSize; const m = ws.match(/ +$/); if (m) { wsStart = ws.slice(0, -m[0].length); return m[0]; } else { wsStart = ws; return ''; } }); if (wsEnd) wsEnd = wsEnd.replace(/\n+(?!\n|$)/g, `$&${indent}`); if (wsStart) wsStart = wsStart.replace(/\n+/g, `$&${indent}`); if (comment) { header += ' #' + comment.replace(/ ?[\r\n]+/g, ' '); if (onComment) onComment(); } if (!value) return `${header}${indentSize}\n${indent}${wsEnd}`; if (literal) { value = value.replace(/\n+/g, `$&${indent}`); return `${header}\n${indent}${wsStart}${value}${wsEnd}`; } value = value.replace(/\n+/g, '\n$&').replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded // ^ ind.line ^ empty ^ capture next empty lines only at end of indent .replace(/\n+/g, `$&${indent}`); const body = foldFlowLines(`${wsStart}${value}${wsEnd}`, indent, FOLD_BLOCK, strOptions.fold); return `${header}\n${indent}${body}`; } function plainString(item, ctx, onComment, onChompKeep) { const { comment, type, value } = item; const { actualString, implicitKey, indent, inFlow } = ctx; if (implicitKey && /[\n[\]{},]/.test(value) || inFlow && /[[\]{},]/.test(value)) { return doubleQuotedString(value, ctx); } if (!value || /^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) { // not allowed: // - empty string, '-' or '?' // - start with an indicator character (except [?:-]) or /[?-] / // - '\n ', ': ' or ' \n' anywhere // - '#' not preceded by a non-space char // - end with ' ' or ':' return implicitKey || inFlow || value.indexOf('\n') === -1 ? value.indexOf('"') !== -1 && value.indexOf("'") === -1 ? singleQuotedString(value, ctx) : doubleQuotedString(value, ctx) : blockString(item, ctx, onComment, onChompKeep); } if (!implicitKey && !inFlow && type !== PlainValue.Type.PLAIN && value.indexOf('\n') !== -1) { // Where allowed & type not set explicitly, prefer block style for multiline strings return blockString(item, ctx, onComment, onChompKeep); } if (indent === '' && containsDocumentMarker(value)) { ctx.forceBlockIndent = true; return blockString(item, ctx, onComment, onChompKeep); } const str = value.replace(/\n+/g, `$&\n${indent}`); // Verify that output will be parsed as a string, as e.g. plain numbers and // booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'), // and others in v1.1. if (actualString) { const { tags } = ctx.doc.schema; const resolved = resolveScalar(str, tags, tags.scalarFallback).value; if (typeof resolved !== 'string') return doubleQuotedString(value, ctx); } const body = implicitKey ? str : foldFlowLines(str, indent, FOLD_FLOW, getFoldOptions(ctx)); if (comment && !inFlow && (body.indexOf('\n') !== -1 || comment.indexOf('\n') !== -1)) { if (onComment) onComment(); return addCommentBefore(body, indent, comment); } return body; } function stringifyString(item, ctx, onComment, onChompKeep) { const { defaultType } = strOptions; const { implicitKey, inFlow } = ctx; let { type, value } = item; if (typeof value !== 'string') { value = String(value); item = Object.assign({}, item, { value }); } const _stringify = _type => { switch (_type) { case PlainValue.Type.BLOCK_FOLDED: case PlainValue.Type.BLOCK_LITERAL: return blockString(item, ctx, onComment, onChompKeep); case PlainValue.Type.QUOTE_DOUBLE: return doubleQuotedString(value, ctx); case PlainValue.Type.QUOTE_SINGLE: return singleQuotedString(value, ctx); case PlainValue.Type.PLAIN: return plainString(item, ctx, onComment, onChompKeep); default: return null; } }; if (type !== PlainValue.Type.QUOTE_DOUBLE && /[\x00-\x08\x0b-\x1f\x7f-\x9f]/.test(value)) { // force double quotes on control characters type = PlainValue.Type.QUOTE_DOUBLE; } else if ((implicitKey || inFlow) && (type === PlainValue.Type.BLOCK_FOLDED || type === PlainValue.Type.BLOCK_LITERAL)) { // should not happen; blocks are not valid inside flow containers type = PlainValue.Type.QUOTE_DOUBLE; } let res = _stringify(type); if (res === null) { res = _stringify(defaultType); if (res === null) throw new Error(`Unsupported default string type ${defaultType}`); } return res; } function stringifyNumber({ format, minFractionDigits, tag, value }) { if (typeof value === 'bigint') return String(value); if (!isFinite(value)) return isNaN(value) ? '.nan' : value < 0 ? '-.inf' : '.inf'; let n = JSON.stringify(value); if (!format && minFractionDigits && (!tag || tag === 'tag:yaml.org,2002:float') && /^\d/.test(n)) { let i = n.indexOf('.'); if (i < 0) { i = n.length; n += '.'; } let d = minFractionDigits - (n.length - i - 1); while (d-- > 0) n += '0'; } return n; } function checkFlowCollectionEnd(errors, cst) { let char, name; switch (cst.type) { case PlainValue.Type.FLOW_MAP: char = '}'; name = 'flow map'; break; case PlainValue.Type.FLOW_SEQ: char = ']'; name = 'flow sequence'; break; default: errors.push(new PlainValue.YAMLSemanticError(cst, 'Not a flow collection!?')); return; } let lastItem; for (let i = cst.items.length - 1; i >= 0; --i) { const item = cst.items[i]; if (!item || item.type !== PlainValue.Type.COMMENT) { lastItem = item; break; } } if (lastItem && lastItem.char !== char) { const msg = `Expected ${name} to end with ${char}`; let err; if (typeof lastItem.offset === 'number') { err = new PlainValue.YAMLSemanticError(cst, msg); err.offset = lastItem.offset + 1; } else { err = new PlainValue.YAMLSemanticError(lastItem, msg); if (lastItem.range && lastItem.range.end) err.offset = lastItem.range.end - lastItem.range.start; } errors.push(err); } } function checkFlowCommentSpace(errors, comment) { const prev = comment.context.src[comment.range.start - 1]; if (prev !== '\n' && prev !== '\t' && prev !== ' ') { const msg = 'Comments must be separated from other tokens by white space characters'; errors.push(new PlainValue.YAMLSemanticError(comment, msg)); } } function getLongKeyError(source, key) { const sk = String(key); const k = sk.substr(0, 8) + '...' + sk.substr(-8); return new PlainValue.YAMLSemanticError(source, `The "${k}" key is too long`); } function resolveComments(collection, comments) { for (const { afterKey, before, comment } of comments) { let item = collection.items[before]; if (!item) { if (comment !== undefined) { if (collection.comment) collection.comment += '\n' + comment;else collection.comment = comment; } } else { if (afterKey && item.value) item = item.value; if (comment === undefined) { if (afterKey || !item.commentBefore) item.spaceBefore = true; } else { if (item.commentBefore) item.commentBefore += '\n' + comment;else item.commentBefore = comment; } } } } // on error, will return { str: string, errors: Error[] } function resolveString(doc, node) { const res = node.strValue; if (!res) return ''; if (typeof res === 'string') return res; res.errors.forEach(error => { if (!error.source) error.source = node; doc.errors.push(error); }); return res.str; } function resolveTagHandle(doc, node) { const { handle, suffix } = node.tag; let prefix = doc.tagPrefixes.find(p => p.handle === handle); if (!prefix) { const dtp = doc.getDefaults().tagPrefixes; if (dtp) prefix = dtp.find(p => p.handle === handle); if (!prefix) throw new PlainValue.YAMLSemanticError(node, `The ${handle} tag handle is non-default and was not declared.`); } if (!suffix) throw new PlainValue.YAMLSemanticError(node, `The ${handle} tag has no suffix.`); if (handle === '!' && (doc.version || doc.options.version) === '1.0') { if (suffix[0] === '^') { doc.warnings.push(new PlainValue.YAMLWarning(node, 'YAML 1.0 ^ tag expansion is not supported')); return suffix; } if (/[:/]/.test(suffix)) { // word/foo -> tag:word.yaml.org,2002:foo const vocab = suffix.match(/^([a-z0-9-]+)\/(.*)/i); return vocab ? `tag:${vocab[1]}.yaml.org,2002:${vocab[2]}` : `tag:${suffix}`; } } return prefix.prefix + decodeURIComponent(suffix); } function resolveTagName(doc, node) { const { tag, type } = node; let nonSpecific = false; if (tag) { const { handle, suffix, verbatim } = tag; if (verbatim) { if (verbatim !== '!' && verbatim !== '!!') return verbatim; const msg = `Verbatim tags aren't resolved, so ${verbatim} is invalid.`; doc.errors.push(new PlainValue.YAMLSemanticError(node, msg)); } else if (handle === '!' && !suffix) { nonSpecific = true; } else { try { return resolveTagHandle(doc, node); } catch (error) { doc.errors.push(error); } } } switch (type) { case PlainValue.Type.BLOCK_FOLDED: case PlainValue.Type.BLOCK_LITERAL: case PlainValue.Type.QUOTE_DOUBLE: case PlainValue.Type.QUOTE_SINGLE: return PlainValue.defaultTags.STR; case PlainValue.Type.FLOW_MAP: case PlainValue.Type.MAP: return PlainValue.defaultTags.MAP; case PlainValue.Type.FLOW_SEQ: case PlainValue.Type.SEQ: return PlainValue.defaultTags.SEQ; case PlainValue.Type.PLAIN: return nonSpecific ? PlainValue.defaultTags.STR : null; default: return null; } } function resolveByTagName(doc, node, tagName) { const { tags } = doc.schema; const matchWithTest = []; for (const tag of tags) { if (tag.tag === tagName) { if (tag.test) matchWithTest.push(tag);else { const res = tag.resolve(doc, node); return res instanceof Collection ? res : new Scalar(res); } } } const str = resolveString(doc, node); if (typeof str === 'string' && matchWithTest.length > 0) return resolveScalar(str, matchWithTest, tags.scalarFallback); return null; } function getFallbackTagName({ type }) { switch (type) { case PlainValue.Type.FLOW_MAP: case PlainValue.Type.MAP: return PlainValue.defaultTags.MAP; case PlainValue.Type.FLOW_SEQ: case PlainValue.Type.SEQ: return PlainValue.defaultTags.SEQ; default: return PlainValue.defaultTags.STR; } } function resolveTag(doc, node, tagName) { try { const res = resolveByTagName(doc, node, tagName); if (res) { if (tagName && node.tag) res.tag = tagName; return res; } } catch (error) { /* istanbul ignore if */ if (!error.source) error.source = node; doc.errors.push(error); return null; } try { const fallback = getFallbackTagName(node); if (!fallback) throw new Error(`The tag ${tagName} is unavailable`); const msg = `The tag ${tagName} is unavailable, falling back to ${fallback}`; doc.warnings.push(new PlainValue.YAMLWarning(node, msg)); const res = resolveByTagName(doc, node, fallback); res.tag = tagName; return res; } catch (error) { const refError = new PlainValue.YAMLReferenceError(node, error.message); refError.stack = error.stack; doc.errors.push(refError); return null; } } const isCollectionItem = node => { if (!node) return false; const { type } = node; return type === PlainValue.Type.MAP_KEY || type === PlainValue.Type.MAP_VALUE || type === PlainValue.Type.SEQ_ITEM; }; function resolveNodeProps(errors, node) { const comments = { before: [], after: [] }; let hasAnchor = false; let hasTag = false; const props = isCollectionItem(node.context.parent) ? node.context.parent.props.concat(node.props) : node.props; for (const { start, end } of props) { switch (node.context.src[start]) { case PlainValue.Char.COMMENT: { if (!node.commentHasRequiredWhitespace(start)) { const msg = 'Comments must be separated from other tokens by white space characters'; errors.push(new PlainValue.YAMLSemanticError(node, msg)); } const { header, valueRange } = node; const cc = valueRange && (start > valueRange.start || header && start > header.start) ? comments.after : comments.before; cc.push(node.context.src.slice(start + 1, end)); break; } // Actual anchor & tag resolution is handled by schema, here we just complain case PlainValue.Char.ANCHOR: if (hasAnchor) { const msg = 'A node can have at most one anchor'; errors.push(new PlainValue.YAMLSemanticError(node, msg)); } hasAnchor = true; break; case PlainValue.Char.TAG: if (hasTag) { const msg = 'A node can have at most one tag'; errors.push(new PlainValue.YAMLSemanticError(node, msg)); } hasTag = true; break; } } return { comments, hasAnchor, hasTag }; } function resolveNodeValue(doc, node) { const { anchors, errors, schema } = doc; if (node.type === PlainValue.Type.ALIAS) { const name = node.rawValue; const src = anchors.getNode(name); if (!src) { const msg = `Aliased anchor not found: ${name}`; errors.push(new PlainValue.YAMLReferenceError(node, msg)); return null; } // Lazy resolution for circular references const res = new Alias(src); anchors._cstAliases.push(res); return res; } const tagName = resolveTagName(doc, node); if (tagName) return resolveTag(doc, node, tagName); if (node.type !== PlainValue.Type.PLAIN) { const msg = `Failed to resolve ${node.type} node here`; errors.push(new PlainValue.YAMLSyntaxError(node, msg)); return null; } try { const str = resolveString(doc, node); return resolveScalar(str, schema.tags, schema.tags.scalarFallback); } catch (error) { if (!error.source) error.source = node; errors.push(error); return null; } } // sets node.resolved on success function resolveNode(doc, node) { if (!node) return null; if (node.error) doc.errors.push(node.error); const { comments, hasAnchor, hasTag } = resolveNodeProps(doc.errors, node); if (hasAnchor) { const { anchors } = doc; const name = node.anchor; const prev = anchors.getNode(name); // At this point, aliases for any preceding node with the same anchor // name have already been resolved, so it may safely be renamed. if (prev) anchors.map[anchors.newName(name)] = prev; // During parsing, we need to store the CST node in anchors.map as // anchors need to be available during resolution to allow for // circular references. anchors.map[name] = node; } if (node.type === PlainValue.Type.ALIAS && (hasAnchor || hasTag)) { const msg = 'An alias node must not specify any properties'; doc.errors.push(new PlainValue.YAMLSemanticError(node, msg)); } const res = resolveNodeValue(doc, node); if (res) { res.range = [node.range.start, node.range.end]; if (doc.options.keepCstNodes) res.cstNode = node; if (doc.options.keepNodeTypes) res.type = node.type; const cb = comments.before.join('\n'); if (cb) { res.commentBefore = res.commentBefore ? `${res.commentBefore}\n${cb}` : cb; } const ca = comments.after.join('\n'); if (ca) res.comment = res.comment ? `${res.comment}\n${ca}` : ca; } return node.resolved = res; } function resolveMap(doc, cst) { if (cst.type !== PlainValue.Type.MAP && cst.type !== PlainValue.Type.FLOW_MAP) { const msg = `A ${cst.type} node cannot be resolved as a mapping`; doc.errors.push(new PlainValue.YAMLSyntaxError(cst, msg)); return null; } const { comments, items } = cst.type === PlainValue.Type.FLOW_MAP ? resolveFlowMapItems(doc, cst) : resolveBlockMapItems(doc, cst); const map = new YAMLMap(); map.items = items; resolveComments(map, comments); let hasCollectionKey = false; for (let i = 0; i < items.length; ++i) { const { key: iKey } = items[i]; if (iKey instanceof Collection) hasCollectionKey = true; if (doc.schema.merge && iKey && iKey.value === MERGE_KEY) { items[i] = new Merge(items[i]); const sources = items[i].value.items; let error = null; sources.some(node => { if (node instanceof Alias) { // During parsing, alias sources are CST nodes; to account for // circular references their resolved values can't be used here. const { type } = node.source; if (type === PlainValue.Type.MAP || type === PlainValue.Type.FLOW_MAP) return false; return error = 'Merge nodes aliases can only point to maps'; } return error = 'Merge nodes can only have Alias nodes as values'; }); if (error) doc.errors.push(new PlainValue.YAMLSemanticError(cst, error)); } else { for (let j = i + 1; j < items.length; ++j) { const { key: jKey } = items[j]; if (iKey === jKey || iKey && jKey && Object.prototype.hasOwnProperty.call(iKey, 'value') && iKey.value === jKey.value) { const msg = `Map keys must be unique; "${iKey}" is repeated`; doc.errors.push(new PlainValue.YAMLSemanticError(cst, msg)); break; } } } } if (hasCollectionKey && !doc.options.mapAsMap) { const warn = 'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'; doc.warnings.push(new PlainValue.YAMLWarning(cst, warn)); } cst.resolved = map; return map; } const valueHasPairComment = ({ context: { lineStart, node, src }, props }) => { if (props.length === 0) return false; const { start } = props[0]; if (node && start > node.valueRange.start) return false; if (src[start] !== PlainValue.Char.COMMENT) return false; for (let i = lineStart; i < start; ++i) if (src[i] === '\n') return false; return true; }; function resolvePairComment(item, pair) { if (!valueHasPairComment(item)) return; const comment = item.getPropValue(0, PlainValue.Char.COMMENT, true); let found = false; const cb = pair.value.commentBefore; if (cb && cb.startsWith(comment)) { pair.value.commentBefore = cb.substr(comment.length + 1); found = true; } else { const cc = pair.value.comment; if (!item.node && cc && cc.startsWith(comment)) { pair.value.comment = cc.substr(comment.length + 1); found = true; } } if (found) pair.comment = comment; } function resolveBlockMapItems(doc, cst) { const comments = []; const items = []; let key = undefined; let keyStart = null; for (let i = 0; i < cst.items.length; ++i) { const item = cst.items[i]; switch (item.type) { case PlainValue.Type.BLANK_LINE: comments.push({ afterKey: !!key, before: items.length }); break; case PlainValue.Type.COMMENT: comments.push({ afterKey: !!key, before: items.length, comment: item.comment }); break; case PlainValue.Type.MAP_KEY: if (key !== undefined) items.push(new Pair(key)); if (item.error) doc.errors.push(item.error); key = resolveNode(doc, item.node); keyStart = null; break; case PlainValue.Type.MAP_VALUE: { if (key === undefined) key = null; if (item.error) doc.errors.push(item.error); if (!item.context.atLineStart && item.node && item.node.type === PlainValue.Type.MAP && !item.node.context.atLineStart) { const msg = 'Nested mappings are not allowed in compact mappings'; doc.errors.push(new PlainValue.YAMLSemanticError(item.node, msg)); } let valueNode = item.node; if (!valueNode && item.props.length > 0) { // Comments on an empty mapping value need to be preserved, so we // need to construct a minimal empty node here to use instead of the // missing `item.node`. -- eemeli/yaml#19 valueNode = new PlainValue.PlainValue(PlainValue.Type.PLAIN, []); valueNode.context = { parent: item, src: item.context.src }; const pos = item.range.start + 1; valueNode.range = { start: pos, end: pos }; valueNode.valueRange = { start: pos, end: pos }; if (typeof item.range.origStart === 'number') { const origPos = item.range.origStart + 1; valueNode.range.origStart = valueNode.range.origEnd = origPos; valueNode.valueRange.origStart = valueNode.valueRange.origEnd = origPos; } } const pair = new Pair(key, resolveNode(doc, valueNode)); resolvePairComment(item, pair); items.push(pair); if (key && typeof keyStart === 'number') { if (item.range.start > keyStart + 1024) doc.errors.push(getLongKeyError(cst, key)); } key = undefined; keyStart = null; } break; default: if (key !== undefined) items.push(new Pair(key)); key = resolveNode(doc, item); keyStart = item.range.start; if (item.error) doc.errors.push(item.error); next: for (let j = i + 1;; ++j) { const nextItem = cst.items[j]; switch (nextItem && nextItem.type) { case PlainValue.Type.BLANK_LINE: case PlainValue.Type.COMMENT: continue next; case PlainValue.Type.MAP_VALUE: break next; default: { const msg = 'Implicit map keys need to be followed by map values'; doc.errors.push(new PlainValue.YAMLSemanticError(item, msg)); break next; } } } if (item.valueRangeContainsNewline) { const msg = 'Implicit map keys need to be on a single line'; doc.errors.push(new PlainValue.YAMLSemanticError(item, msg)); } } } if (key !== undefined) items.push(new Pair(key)); return { comments, items }; } function resolveFlowMapItems(doc, cst) { const comments = []; const items = []; let key = undefined; let explicitKey = false; let next = '{'; for (let i = 0; i < cst.items.length; ++i) { const item = cst.items[i]; if (typeof item.char === 'string') { const { char, offset } = item; if (char === '?' && key === undefined && !explicitKey) { explicitKey = true; next = ':'; continue; } if (char === ':') { if (key === undefined) key = null; if (next === ':') { next = ','; continue; } } else { if (explicitKey) { if (key === undefined && char !== ',') key = null; explicitKey = false; } if (key !== undefined) { items.push(new Pair(key)); key = undefined; if (char === ',') { next = ':'; continue; } } } if (char === '}') { if (i === cst.items.length - 1) continue; } else if (char === next) { next = ':'; continue; } const msg = `Flow map contains an unexpected ${char}`; const err = new PlainValue.YAMLSyntaxError(cst, msg); err.offset = offset; doc.errors.push(err); } else if (item.type === PlainValue.Type.BLANK_LINE) { comments.push({ afterKey: !!key, before: items.length }); } else if (item.type === PlainValue.Type.COMMENT) { checkFlowCommentSpace(doc.errors, item); comments.push({ afterKey: !!key, before: items.length, comment: item.comment }); } else if (key === undefined) { if (next === ',') doc.errors.push(new PlainValue.YAMLSemanticError(item, 'Separator , missing in flow map')); key = resolveNode(doc, item); } else { if (next !== ',') doc.errors.push(new PlainValue.YAMLSemanticError(item, 'Indicator : missing in flow map entry')); items.push(new Pair(key, resolveNode(doc, item))); key = undefined; explicitKey = false; } } checkFlowCollectionEnd(doc.errors, cst); if (key !== undefined) items.push(new Pair(key)); return { comments, items }; } function resolveSeq(doc, cst) { if (cst.type !== PlainValue.Type.SEQ && cst.type !== PlainValue.Type.FLOW_SEQ) { const msg = `A ${cst.type} node cannot be resolved as a sequence`; doc.errors.push(new PlainValue.YAMLSyntaxError(cst, msg)); return null; } const { comments, items } = cst.type === PlainValue.Type.FLOW_SEQ ? resolveFlowSeqItems(doc, cst) : resolveBlockSeqItems(doc, cst); const seq = new YAMLSeq(); seq.items = items; resolveComments(seq, comments); if (!doc.options.mapAsMap && items.some(it => it instanceof Pair && it.key instanceof Collection)) { const warn = 'Keys with collection values will be stringified as YAML due to JS Object restrictions. Use mapAsMap: true to avoid this.'; doc.warnings.push(new PlainValue.YAMLWarning(cst, warn)); } cst.resolved = seq; return seq; } function resolveBlockSeqItems(doc, cst) { const comments = []; const items = []; for (let i = 0; i < cst.items.length; ++i) { const item = cst.items[i]; switch (item.type) { case PlainValue.Type.BLANK_LINE: comments.push({ before: items.length }); break; case PlainValue.Type.COMMENT: comments.push({ comment: item.comment, before: items.length }); break; case PlainValue.Type.SEQ_ITEM: if (item.error) doc.errors.push(item.error); items.push(resolveNode(doc, item.node)); if (item.hasProps) { const msg = 'Sequence items cannot have tags or anchors before the - indicator'; doc.errors.push(new PlainValue.YAMLSemanticError(item, msg)); } break; default: if (item.error) doc.errors.push(item.error); doc.errors.push(new PlainValue.YAMLSyntaxError(item, `Unexpected ${item.type} node in sequence`)); } } return { comments, items }; } function resolveFlowSeqItems(doc, cst) { const comments = []; const items = []; let explicitKey = false; let key = undefined; let keyStart = null; let next = '['; let prevItem = null; for (let i = 0; i < cst.items.length; ++i) { const item = cst.items[i]; if (typeof item.char === 'string') { const { char, offset } = item; if (char !== ':' && (explicitKey || key !== undefined)) { if (explicitKey && key === undefined) key = next ? items.pop() : null; items.push(new Pair(key)); explicitKey = false; key = undefined; keyStart = null; } if (char === next) { next = null; } else if (!next && char === '?') { explicitKey = true; } else if (next !== '[' && char === ':' && key === undefined) { if (next === ',') { key = items.pop(); if (key instanceof Pair) { const msg = 'Chaining flow sequence pairs is invalid'; const err = new PlainValue.YAMLSemanticError(cst, msg); err.offset = offset; doc.errors.push(err); } if (!explicitKey && typeof keyStart === 'number') { const keyEnd = item.range ? item.range.start : item.offset; if (keyEnd > keyStart + 1024) doc.errors.push(getLongKeyError(cst, key)); const { src } = prevItem.context; for (let i = keyStart; i < keyEnd; ++i) if (src[i] === '\n') { const msg = 'Implicit keys of flow sequence pairs need to be on a single line'; doc.errors.push(new PlainValue.YAMLSemanticError(prevItem, msg)); break; } } } else { key = null; } keyStart = null; explicitKey = false; next = null; } else if (next === '[' || char !== ']' || i < cst.items.length - 1) { const msg = `Flow sequence contains an unexpected ${char}`; const err = new PlainValue.YAMLSyntaxError(cst, msg); err.offset = offset; doc.errors.push(err); } } else if (item.type === PlainValue.Type.BLANK_LINE) { comments.push({ before: items.length }); } else if (item.type === PlainValue.Type.COMMENT) { checkFlowCommentSpace(doc.errors, item); comments.push({ comment: item.comment, before: items.length }); } else { if (next) { const msg = `Expected a ${next} in flow sequence`; doc.errors.push(new PlainValue.YAMLSemanticError(item, msg)); } const value = resolveNode(doc, item); if (key === undefined) { items.push(value); prevItem = item; } else { items.push(new Pair(key, value)); key = undefined; } keyStart = item.range.start; next = ','; } } checkFlowCollectionEnd(doc.errors, cst); if (key !== undefined) items.push(new Pair(key)); return { comments, items }; } exports.Alias = Alias; exports.Collection = Collection; exports.Merge = Merge; exports.Node = Node; exports.Pair = Pair; exports.Scalar = Scalar; exports.YAMLMap = YAMLMap; exports.YAMLSeq = YAMLSeq; exports.addComment = addComment; exports.binaryOptions = binaryOptions; exports.boolOptions = boolOptions; exports.findPair = findPair; exports.intOptions = intOptions; exports.isEmptyPath = isEmptyPath; exports.nullOptions = nullOptions; exports.resolveMap = resolveMap; exports.resolveNode = resolveNode; exports.resolveSeq = resolveSeq; exports.resolveString = resolveString; exports.strOptions = strOptions; exports.stringifyNumber = stringifyNumber; exports.stringifyString = stringifyString; exports.toJSON = toJSON;