149 lines
5.0 KiB
JavaScript
149 lines
5.0 KiB
JavaScript
|
import { createNode } from '../doc/createNode.js';
|
||
|
import { isNode, isPair, isCollection, isScalar } from './identity.js';
|
||
|
import { NodeBase } from './Node.js';
|
||
|
|
||
|
function collectionFromPath(schema, path, value) {
|
||
|
let v = value;
|
||
|
for (let i = path.length - 1; i >= 0; --i) {
|
||
|
const k = path[i];
|
||
|
if (typeof k === 'number' && Number.isInteger(k) && k >= 0) {
|
||
|
const a = [];
|
||
|
a[k] = v;
|
||
|
v = a;
|
||
|
}
|
||
|
else {
|
||
|
v = new Map([[k, v]]);
|
||
|
}
|
||
|
}
|
||
|
return createNode(v, undefined, {
|
||
|
aliasDuplicateObjects: false,
|
||
|
keepUndefined: false,
|
||
|
onAnchor: () => {
|
||
|
throw new Error('This should not happen, please report a bug.');
|
||
|
},
|
||
|
schema,
|
||
|
sourceObjects: new Map()
|
||
|
});
|
||
|
}
|
||
|
// Type guard is intentionally a little wrong so as to be more useful,
|
||
|
// as it does not cover untypable empty non-string iterables (e.g. []).
|
||
|
const isEmptyPath = (path) => path == null ||
|
||
|
(typeof path === 'object' && !!path[Symbol.iterator]().next().done);
|
||
|
class Collection extends NodeBase {
|
||
|
constructor(type, schema) {
|
||
|
super(type);
|
||
|
Object.defineProperty(this, 'schema', {
|
||
|
value: schema,
|
||
|
configurable: true,
|
||
|
enumerable: false,
|
||
|
writable: true
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Create a copy of this collection.
|
||
|
*
|
||
|
* @param schema - If defined, overwrites the original's schema
|
||
|
*/
|
||
|
clone(schema) {
|
||
|
const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
|
||
|
if (schema)
|
||
|
copy.schema = schema;
|
||
|
copy.items = copy.items.map(it => isNode(it) || isPair(it) ? it.clone(schema) : it);
|
||
|
if (this.range)
|
||
|
copy.range = this.range.slice();
|
||
|
return copy;
|
||
|
}
|
||
|
/**
|
||
|
* Adds a value to the collection. For `!!map` and `!!omap` the value must
|
||
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
||
|
* that already exists in the map.
|
||
|
*/
|
||
|
addIn(path, value) {
|
||
|
if (isEmptyPath(path))
|
||
|
this.add(value);
|
||
|
else {
|
||
|
const [key, ...rest] = path;
|
||
|
const node = this.get(key, true);
|
||
|
if (isCollection(node))
|
||
|
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}`);
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Removes a value from the collection.
|
||
|
* @returns `true` if the item was found and removed.
|
||
|
*/
|
||
|
deleteIn(path) {
|
||
|
const [key, ...rest] = path;
|
||
|
if (rest.length === 0)
|
||
|
return this.delete(key);
|
||
|
const node = this.get(key, true);
|
||
|
if (isCollection(node))
|
||
|
return node.deleteIn(rest);
|
||
|
else
|
||
|
throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
|
||
|
}
|
||
|
/**
|
||
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
||
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
||
|
* `true` (collections are always returned intact).
|
||
|
*/
|
||
|
getIn(path, keepScalar) {
|
||
|
const [key, ...rest] = path;
|
||
|
const node = this.get(key, true);
|
||
|
if (rest.length === 0)
|
||
|
return !keepScalar && isScalar(node) ? node.value : node;
|
||
|
else
|
||
|
return isCollection(node) ? node.getIn(rest, keepScalar) : undefined;
|
||
|
}
|
||
|
hasAllNullValues(allowScalar) {
|
||
|
return this.items.every(node => {
|
||
|
if (!isPair(node))
|
||
|
return false;
|
||
|
const n = node.value;
|
||
|
return (n == null ||
|
||
|
(allowScalar &&
|
||
|
isScalar(n) &&
|
||
|
n.value == null &&
|
||
|
!n.commentBefore &&
|
||
|
!n.comment &&
|
||
|
!n.tag));
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Checks if the collection includes a value with the key `key`.
|
||
|
*/
|
||
|
hasIn(path) {
|
||
|
const [key, ...rest] = path;
|
||
|
if (rest.length === 0)
|
||
|
return this.has(key);
|
||
|
const node = this.get(key, true);
|
||
|
return isCollection(node) ? node.hasIn(rest) : false;
|
||
|
}
|
||
|
/**
|
||
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
||
|
* boolean to add/remove the item from the set.
|
||
|
*/
|
||
|
setIn(path, value) {
|
||
|
const [key, ...rest] = path;
|
||
|
if (rest.length === 0) {
|
||
|
this.set(key, value);
|
||
|
}
|
||
|
else {
|
||
|
const node = this.get(key, true);
|
||
|
if (isCollection(node))
|
||
|
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}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
Collection.maxFlowStringSingleLineLength = 60;
|
||
|
|
||
|
export { Collection, collectionFromPath, isEmptyPath };
|