123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- import { Alias } from '../nodes/Alias.js';
- import { isEmptyPath, collectionFromPath } from '../nodes/Collection.js';
- import { NODE_TYPE, DOC, isNode, isCollection, isScalar } from '../nodes/identity.js';
- import { Pair } from '../nodes/Pair.js';
- import { toJS } from '../nodes/toJS.js';
- import { Schema } from '../schema/Schema.js';
- import { stringifyDocument } from '../stringify/stringifyDocument.js';
- import { anchorNames, findNewAnchor, createNodeAnchors } from './anchors.js';
- import { applyReviver } from './applyReviver.js';
- import { createNode } from './createNode.js';
- import { Directives } from './directives.js';
- class Document {
- constructor(value, replacer, options) {
- /** A comment before this Document */
- this.commentBefore = null;
- /** A comment immediately after this Document */
- this.comment = null;
- /** Errors encountered during parsing. */
- this.errors = [];
- /** Warnings encountered during parsing. */
- this.warnings = [];
- Object.defineProperty(this, NODE_TYPE, { value: DOC });
- let _replacer = null;
- if (typeof replacer === 'function' || Array.isArray(replacer)) {
- _replacer = replacer;
- }
- else if (options === undefined && replacer) {
- options = replacer;
- replacer = undefined;
- }
- const opt = Object.assign({
- intAsBigInt: false,
- keepSourceTokens: false,
- logLevel: 'warn',
- prettyErrors: true,
- strict: true,
- stringKeys: false,
- uniqueKeys: true,
- version: '1.2'
- }, options);
- this.options = opt;
- let { version } = opt;
- if (options?._directives) {
- this.directives = options._directives.atDocument();
- if (this.directives.yaml.explicit)
- version = this.directives.yaml.version;
- }
- else
- this.directives = new Directives({ version });
- this.setSchema(version, options);
- // @ts-expect-error We can't really know that this matches Contents.
- this.contents =
- value === undefined ? null : this.createNode(value, _replacer, options);
- }
- /**
- * Create a deep copy of this Document and its contents.
- *
- * Custom Node values that inherit from `Object` still refer to their original instances.
- */
- clone() {
- const copy = Object.create(Document.prototype, {
- [NODE_TYPE]: { value: DOC }
- });
- copy.commentBefore = this.commentBefore;
- copy.comment = this.comment;
- copy.errors = this.errors.slice();
- copy.warnings = this.warnings.slice();
- copy.options = Object.assign({}, this.options);
- if (this.directives)
- copy.directives = this.directives.clone();
- copy.schema = this.schema.clone();
- // @ts-expect-error We can't really know that this matches Contents.
- copy.contents = isNode(this.contents)
- ? this.contents.clone(copy.schema)
- : this.contents;
- if (this.range)
- copy.range = this.range.slice();
- return copy;
- }
- /** Adds a value to the document. */
- add(value) {
- if (assertCollection(this.contents))
- this.contents.add(value);
- }
- /** Adds a value to the document. */
- addIn(path, value) {
- if (assertCollection(this.contents))
- this.contents.addIn(path, value);
- }
- /**
- * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
- *
- * If `node` already has an anchor, `name` is ignored.
- * Otherwise, the `node.anchor` value will be set to `name`,
- * or if an anchor with that name is already present in the document,
- * `name` will be used as a prefix for a new unique anchor.
- * If `name` is undefined, the generated anchor will use 'a' as a prefix.
- */
- createAlias(node, name) {
- if (!node.anchor) {
- const prev = anchorNames(this);
- node.anchor =
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- !name || prev.has(name) ? findNewAnchor(name || 'a', prev) : name;
- }
- return new Alias(node.anchor);
- }
- createNode(value, replacer, options) {
- let _replacer = undefined;
- if (typeof replacer === 'function') {
- value = replacer.call({ '': value }, '', value);
- _replacer = replacer;
- }
- else if (Array.isArray(replacer)) {
- const keyToStr = (v) => typeof v === 'number' || v instanceof String || v instanceof Number;
- const asStr = replacer.filter(keyToStr).map(String);
- if (asStr.length > 0)
- replacer = replacer.concat(asStr);
- _replacer = replacer;
- }
- else if (options === undefined && replacer) {
- options = replacer;
- replacer = undefined;
- }
- const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {};
- const { onAnchor, setAnchors, sourceObjects } = createNodeAnchors(this,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- anchorPrefix || 'a');
- const ctx = {
- aliasDuplicateObjects: aliasDuplicateObjects ?? true,
- keepUndefined: keepUndefined ?? false,
- onAnchor,
- onTagObj,
- replacer: _replacer,
- schema: this.schema,
- sourceObjects
- };
- const node = createNode(value, tag, ctx);
- if (flow && isCollection(node))
- node.flow = true;
- setAnchors();
- return node;
- }
- /**
- * Convert a key and a value into a `Pair` using the current schema,
- * recursively wrapping all values as `Scalar` or `Collection` nodes.
- */
- createPair(key, value, options = {}) {
- const k = this.createNode(key, null, options);
- const v = this.createNode(value, null, options);
- return new Pair(k, v);
- }
- /**
- * Removes a value from the document.
- * @returns `true` if the item was found and removed.
- */
- delete(key) {
- return assertCollection(this.contents) ? this.contents.delete(key) : false;
- }
- /**
- * Removes a value from the document.
- * @returns `true` if the item was found and removed.
- */
- deleteIn(path) {
- if (isEmptyPath(path)) {
- if (this.contents == null)
- return false;
- // @ts-expect-error Presumed impossible if Strict extends false
- this.contents = null;
- return true;
- }
- return assertCollection(this.contents)
- ? this.contents.deleteIn(path)
- : false;
- }
- /**
- * 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).
- */
- get(key, keepScalar) {
- return isCollection(this.contents)
- ? this.contents.get(key, keepScalar)
- : undefined;
- }
- /**
- * Returns item at `path`, 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) {
- if (isEmptyPath(path))
- return !keepScalar && isScalar(this.contents)
- ? this.contents.value
- : this.contents;
- return isCollection(this.contents)
- ? this.contents.getIn(path, keepScalar)
- : undefined;
- }
- /**
- * Checks if the document includes a value with the key `key`.
- */
- has(key) {
- return isCollection(this.contents) ? this.contents.has(key) : false;
- }
- /**
- * Checks if the document includes a value at `path`.
- */
- hasIn(path) {
- if (isEmptyPath(path))
- return this.contents !== undefined;
- return isCollection(this.contents) ? this.contents.hasIn(path) : false;
- }
- /**
- * Sets a value in this document. For `!!set`, `value` needs to be a
- * boolean to add/remove the item from the set.
- */
- set(key, value) {
- if (this.contents == null) {
- // @ts-expect-error We can't really know that this matches Contents.
- this.contents = collectionFromPath(this.schema, [key], value);
- }
- else if (assertCollection(this.contents)) {
- this.contents.set(key, value);
- }
- }
- /**
- * Sets a value in this document. For `!!set`, `value` needs to be a
- * boolean to add/remove the item from the set.
- */
- setIn(path, value) {
- if (isEmptyPath(path)) {
- // @ts-expect-error We can't really know that this matches Contents.
- this.contents = value;
- }
- else if (this.contents == null) {
- // @ts-expect-error We can't really know that this matches Contents.
- this.contents = collectionFromPath(this.schema, Array.from(path), value);
- }
- else if (assertCollection(this.contents)) {
- this.contents.setIn(path, value);
- }
- }
- /**
- * Change the YAML version and schema used by the document.
- * A `null` version disables support for directives, explicit tags, anchors, and aliases.
- * It also requires the `schema` option to be given as a `Schema` instance value.
- *
- * Overrides all previously set schema options.
- */
- setSchema(version, options = {}) {
- if (typeof version === 'number')
- version = String(version);
- let opt;
- switch (version) {
- case '1.1':
- if (this.directives)
- this.directives.yaml.version = '1.1';
- else
- this.directives = new Directives({ version: '1.1' });
- opt = { resolveKnownTags: false, schema: 'yaml-1.1' };
- break;
- case '1.2':
- case 'next':
- if (this.directives)
- this.directives.yaml.version = version;
- else
- this.directives = new Directives({ version });
- opt = { resolveKnownTags: true, schema: 'core' };
- break;
- case null:
- if (this.directives)
- delete this.directives;
- opt = null;
- break;
- default: {
- const sv = JSON.stringify(version);
- throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`);
- }
- }
- // Not using `instanceof Schema` to allow for duck typing
- if (options.schema instanceof Object)
- this.schema = options.schema;
- else if (opt)
- this.schema = new Schema(Object.assign(opt, options));
- else
- throw new Error(`With a null YAML version, the { schema: Schema } option is required`);
- }
- // json & jsonArg are only used from toJSON()
- toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) {
- const ctx = {
- anchors: new Map(),
- doc: this,
- keep: !json,
- mapAsMap: mapAsMap === true,
- mapKeyWarned: false,
- maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100
- };
- const res = toJS(this.contents, jsonArg ?? '', ctx);
- if (typeof onAnchor === 'function')
- for (const { count, res } of ctx.anchors.values())
- onAnchor(res, count);
- return typeof reviver === 'function'
- ? applyReviver(reviver, { '': res }, '', res)
- : res;
- }
- /**
- * A JSON representation of the document `contents`.
- *
- * @param jsonArg Used by `JSON.stringify` to indicate the array index or
- * property name.
- */
- toJSON(jsonArg, onAnchor) {
- return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor });
- }
- /** A YAML representation of the document. */
- toString(options = {}) {
- if (this.errors.length > 0)
- throw new Error('Document with errors cannot be stringified');
- if ('indent' in options &&
- (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) {
- const s = JSON.stringify(options.indent);
- throw new Error(`"indent" option must be a positive integer, not ${s}`);
- }
- return stringifyDocument(this, options);
- }
- }
- function assertCollection(contents) {
- if (isCollection(contents))
- return true;
- throw new Error('Expected a YAML collection as document contents');
- }
- export { Document };
|