You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2161 lines
60 KiB
2161 lines
60 KiB
'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;
|
|
|