[d24f17c] | 1 | 'use strict';
|
---|
| 2 |
|
---|
| 3 | var Alias = require('../nodes/Alias.js');
|
---|
| 4 | var Collection = require('../nodes/Collection.js');
|
---|
| 5 | var identity = require('../nodes/identity.js');
|
---|
| 6 | var Pair = require('../nodes/Pair.js');
|
---|
| 7 | var toJS = require('../nodes/toJS.js');
|
---|
| 8 | var Schema = require('../schema/Schema.js');
|
---|
| 9 | var stringifyDocument = require('../stringify/stringifyDocument.js');
|
---|
| 10 | var anchors = require('./anchors.js');
|
---|
| 11 | var applyReviver = require('./applyReviver.js');
|
---|
| 12 | var createNode = require('./createNode.js');
|
---|
| 13 | var directives = require('./directives.js');
|
---|
| 14 |
|
---|
| 15 | class Document {
|
---|
| 16 | constructor(value, replacer, options) {
|
---|
| 17 | /** A comment before this Document */
|
---|
| 18 | this.commentBefore = null;
|
---|
| 19 | /** A comment immediately after this Document */
|
---|
| 20 | this.comment = null;
|
---|
| 21 | /** Errors encountered during parsing. */
|
---|
| 22 | this.errors = [];
|
---|
| 23 | /** Warnings encountered during parsing. */
|
---|
| 24 | this.warnings = [];
|
---|
| 25 | Object.defineProperty(this, identity.NODE_TYPE, { value: identity.DOC });
|
---|
| 26 | let _replacer = null;
|
---|
| 27 | if (typeof replacer === 'function' || Array.isArray(replacer)) {
|
---|
| 28 | _replacer = replacer;
|
---|
| 29 | }
|
---|
| 30 | else if (options === undefined && replacer) {
|
---|
| 31 | options = replacer;
|
---|
| 32 | replacer = undefined;
|
---|
| 33 | }
|
---|
| 34 | const opt = Object.assign({
|
---|
| 35 | intAsBigInt: false,
|
---|
| 36 | keepSourceTokens: false,
|
---|
| 37 | logLevel: 'warn',
|
---|
| 38 | prettyErrors: true,
|
---|
| 39 | strict: true,
|
---|
| 40 | uniqueKeys: true,
|
---|
| 41 | version: '1.2'
|
---|
| 42 | }, options);
|
---|
| 43 | this.options = opt;
|
---|
| 44 | let { version } = opt;
|
---|
| 45 | if (options?._directives) {
|
---|
| 46 | this.directives = options._directives.atDocument();
|
---|
| 47 | if (this.directives.yaml.explicit)
|
---|
| 48 | version = this.directives.yaml.version;
|
---|
| 49 | }
|
---|
| 50 | else
|
---|
| 51 | this.directives = new directives.Directives({ version });
|
---|
| 52 | this.setSchema(version, options);
|
---|
| 53 | // @ts-expect-error We can't really know that this matches Contents.
|
---|
| 54 | this.contents =
|
---|
| 55 | value === undefined ? null : this.createNode(value, _replacer, options);
|
---|
| 56 | }
|
---|
| 57 | /**
|
---|
| 58 | * Create a deep copy of this Document and its contents.
|
---|
| 59 | *
|
---|
| 60 | * Custom Node values that inherit from `Object` still refer to their original instances.
|
---|
| 61 | */
|
---|
| 62 | clone() {
|
---|
| 63 | const copy = Object.create(Document.prototype, {
|
---|
| 64 | [identity.NODE_TYPE]: { value: identity.DOC }
|
---|
| 65 | });
|
---|
| 66 | copy.commentBefore = this.commentBefore;
|
---|
| 67 | copy.comment = this.comment;
|
---|
| 68 | copy.errors = this.errors.slice();
|
---|
| 69 | copy.warnings = this.warnings.slice();
|
---|
| 70 | copy.options = Object.assign({}, this.options);
|
---|
| 71 | if (this.directives)
|
---|
| 72 | copy.directives = this.directives.clone();
|
---|
| 73 | copy.schema = this.schema.clone();
|
---|
| 74 | // @ts-expect-error We can't really know that this matches Contents.
|
---|
| 75 | copy.contents = identity.isNode(this.contents)
|
---|
| 76 | ? this.contents.clone(copy.schema)
|
---|
| 77 | : this.contents;
|
---|
| 78 | if (this.range)
|
---|
| 79 | copy.range = this.range.slice();
|
---|
| 80 | return copy;
|
---|
| 81 | }
|
---|
| 82 | /** Adds a value to the document. */
|
---|
| 83 | add(value) {
|
---|
| 84 | if (assertCollection(this.contents))
|
---|
| 85 | this.contents.add(value);
|
---|
| 86 | }
|
---|
| 87 | /** Adds a value to the document. */
|
---|
| 88 | addIn(path, value) {
|
---|
| 89 | if (assertCollection(this.contents))
|
---|
| 90 | this.contents.addIn(path, value);
|
---|
| 91 | }
|
---|
| 92 | /**
|
---|
| 93 | * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
---|
| 94 | *
|
---|
| 95 | * If `node` already has an anchor, `name` is ignored.
|
---|
| 96 | * Otherwise, the `node.anchor` value will be set to `name`,
|
---|
| 97 | * or if an anchor with that name is already present in the document,
|
---|
| 98 | * `name` will be used as a prefix for a new unique anchor.
|
---|
| 99 | * If `name` is undefined, the generated anchor will use 'a' as a prefix.
|
---|
| 100 | */
|
---|
| 101 | createAlias(node, name) {
|
---|
| 102 | if (!node.anchor) {
|
---|
| 103 | const prev = anchors.anchorNames(this);
|
---|
| 104 | node.anchor =
|
---|
| 105 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
---|
| 106 | !name || prev.has(name) ? anchors.findNewAnchor(name || 'a', prev) : name;
|
---|
| 107 | }
|
---|
| 108 | return new Alias.Alias(node.anchor);
|
---|
| 109 | }
|
---|
| 110 | createNode(value, replacer, options) {
|
---|
| 111 | let _replacer = undefined;
|
---|
| 112 | if (typeof replacer === 'function') {
|
---|
| 113 | value = replacer.call({ '': value }, '', value);
|
---|
| 114 | _replacer = replacer;
|
---|
| 115 | }
|
---|
| 116 | else if (Array.isArray(replacer)) {
|
---|
| 117 | const keyToStr = (v) => typeof v === 'number' || v instanceof String || v instanceof Number;
|
---|
| 118 | const asStr = replacer.filter(keyToStr).map(String);
|
---|
| 119 | if (asStr.length > 0)
|
---|
| 120 | replacer = replacer.concat(asStr);
|
---|
| 121 | _replacer = replacer;
|
---|
| 122 | }
|
---|
| 123 | else if (options === undefined && replacer) {
|
---|
| 124 | options = replacer;
|
---|
| 125 | replacer = undefined;
|
---|
| 126 | }
|
---|
| 127 | const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {};
|
---|
| 128 | const { onAnchor, setAnchors, sourceObjects } = anchors.createNodeAnchors(this,
|
---|
| 129 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
---|
| 130 | anchorPrefix || 'a');
|
---|
| 131 | const ctx = {
|
---|
| 132 | aliasDuplicateObjects: aliasDuplicateObjects ?? true,
|
---|
| 133 | keepUndefined: keepUndefined ?? false,
|
---|
| 134 | onAnchor,
|
---|
| 135 | onTagObj,
|
---|
| 136 | replacer: _replacer,
|
---|
| 137 | schema: this.schema,
|
---|
| 138 | sourceObjects
|
---|
| 139 | };
|
---|
| 140 | const node = createNode.createNode(value, tag, ctx);
|
---|
| 141 | if (flow && identity.isCollection(node))
|
---|
| 142 | node.flow = true;
|
---|
| 143 | setAnchors();
|
---|
| 144 | return node;
|
---|
| 145 | }
|
---|
| 146 | /**
|
---|
| 147 | * Convert a key and a value into a `Pair` using the current schema,
|
---|
| 148 | * recursively wrapping all values as `Scalar` or `Collection` nodes.
|
---|
| 149 | */
|
---|
| 150 | createPair(key, value, options = {}) {
|
---|
| 151 | const k = this.createNode(key, null, options);
|
---|
| 152 | const v = this.createNode(value, null, options);
|
---|
| 153 | return new Pair.Pair(k, v);
|
---|
| 154 | }
|
---|
| 155 | /**
|
---|
| 156 | * Removes a value from the document.
|
---|
| 157 | * @returns `true` if the item was found and removed.
|
---|
| 158 | */
|
---|
| 159 | delete(key) {
|
---|
| 160 | return assertCollection(this.contents) ? this.contents.delete(key) : false;
|
---|
| 161 | }
|
---|
| 162 | /**
|
---|
| 163 | * Removes a value from the document.
|
---|
| 164 | * @returns `true` if the item was found and removed.
|
---|
| 165 | */
|
---|
| 166 | deleteIn(path) {
|
---|
| 167 | if (Collection.isEmptyPath(path)) {
|
---|
| 168 | if (this.contents == null)
|
---|
| 169 | return false;
|
---|
| 170 | // @ts-expect-error Presumed impossible if Strict extends false
|
---|
| 171 | this.contents = null;
|
---|
| 172 | return true;
|
---|
| 173 | }
|
---|
| 174 | return assertCollection(this.contents)
|
---|
| 175 | ? this.contents.deleteIn(path)
|
---|
| 176 | : false;
|
---|
| 177 | }
|
---|
| 178 | /**
|
---|
| 179 | * Returns item at `key`, or `undefined` if not found. By default unwraps
|
---|
| 180 | * scalar values from their surrounding node; to disable set `keepScalar` to
|
---|
| 181 | * `true` (collections are always returned intact).
|
---|
| 182 | */
|
---|
| 183 | get(key, keepScalar) {
|
---|
| 184 | return identity.isCollection(this.contents)
|
---|
| 185 | ? this.contents.get(key, keepScalar)
|
---|
| 186 | : undefined;
|
---|
| 187 | }
|
---|
| 188 | /**
|
---|
| 189 | * Returns item at `path`, or `undefined` if not found. By default unwraps
|
---|
| 190 | * scalar values from their surrounding node; to disable set `keepScalar` to
|
---|
| 191 | * `true` (collections are always returned intact).
|
---|
| 192 | */
|
---|
| 193 | getIn(path, keepScalar) {
|
---|
| 194 | if (Collection.isEmptyPath(path))
|
---|
| 195 | return !keepScalar && identity.isScalar(this.contents)
|
---|
| 196 | ? this.contents.value
|
---|
| 197 | : this.contents;
|
---|
| 198 | return identity.isCollection(this.contents)
|
---|
| 199 | ? this.contents.getIn(path, keepScalar)
|
---|
| 200 | : undefined;
|
---|
| 201 | }
|
---|
| 202 | /**
|
---|
| 203 | * Checks if the document includes a value with the key `key`.
|
---|
| 204 | */
|
---|
| 205 | has(key) {
|
---|
| 206 | return identity.isCollection(this.contents) ? this.contents.has(key) : false;
|
---|
| 207 | }
|
---|
| 208 | /**
|
---|
| 209 | * Checks if the document includes a value at `path`.
|
---|
| 210 | */
|
---|
| 211 | hasIn(path) {
|
---|
| 212 | if (Collection.isEmptyPath(path))
|
---|
| 213 | return this.contents !== undefined;
|
---|
| 214 | return identity.isCollection(this.contents) ? this.contents.hasIn(path) : false;
|
---|
| 215 | }
|
---|
| 216 | /**
|
---|
| 217 | * Sets a value in this document. For `!!set`, `value` needs to be a
|
---|
| 218 | * boolean to add/remove the item from the set.
|
---|
| 219 | */
|
---|
| 220 | set(key, value) {
|
---|
| 221 | if (this.contents == null) {
|
---|
| 222 | // @ts-expect-error We can't really know that this matches Contents.
|
---|
| 223 | this.contents = Collection.collectionFromPath(this.schema, [key], value);
|
---|
| 224 | }
|
---|
| 225 | else if (assertCollection(this.contents)) {
|
---|
| 226 | this.contents.set(key, value);
|
---|
| 227 | }
|
---|
| 228 | }
|
---|
| 229 | /**
|
---|
| 230 | * Sets a value in this document. For `!!set`, `value` needs to be a
|
---|
| 231 | * boolean to add/remove the item from the set.
|
---|
| 232 | */
|
---|
| 233 | setIn(path, value) {
|
---|
| 234 | if (Collection.isEmptyPath(path)) {
|
---|
| 235 | // @ts-expect-error We can't really know that this matches Contents.
|
---|
| 236 | this.contents = value;
|
---|
| 237 | }
|
---|
| 238 | else if (this.contents == null) {
|
---|
| 239 | // @ts-expect-error We can't really know that this matches Contents.
|
---|
| 240 | this.contents = Collection.collectionFromPath(this.schema, Array.from(path), value);
|
---|
| 241 | }
|
---|
| 242 | else if (assertCollection(this.contents)) {
|
---|
| 243 | this.contents.setIn(path, value);
|
---|
| 244 | }
|
---|
| 245 | }
|
---|
| 246 | /**
|
---|
| 247 | * Change the YAML version and schema used by the document.
|
---|
| 248 | * A `null` version disables support for directives, explicit tags, anchors, and aliases.
|
---|
| 249 | * It also requires the `schema` option to be given as a `Schema` instance value.
|
---|
| 250 | *
|
---|
| 251 | * Overrides all previously set schema options.
|
---|
| 252 | */
|
---|
| 253 | setSchema(version, options = {}) {
|
---|
| 254 | if (typeof version === 'number')
|
---|
| 255 | version = String(version);
|
---|
| 256 | let opt;
|
---|
| 257 | switch (version) {
|
---|
| 258 | case '1.1':
|
---|
| 259 | if (this.directives)
|
---|
| 260 | this.directives.yaml.version = '1.1';
|
---|
| 261 | else
|
---|
| 262 | this.directives = new directives.Directives({ version: '1.1' });
|
---|
| 263 | opt = { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' };
|
---|
| 264 | break;
|
---|
| 265 | case '1.2':
|
---|
| 266 | case 'next':
|
---|
| 267 | if (this.directives)
|
---|
| 268 | this.directives.yaml.version = version;
|
---|
| 269 | else
|
---|
| 270 | this.directives = new directives.Directives({ version });
|
---|
| 271 | opt = { merge: false, resolveKnownTags: true, schema: 'core' };
|
---|
| 272 | break;
|
---|
| 273 | case null:
|
---|
| 274 | if (this.directives)
|
---|
| 275 | delete this.directives;
|
---|
| 276 | opt = null;
|
---|
| 277 | break;
|
---|
| 278 | default: {
|
---|
| 279 | const sv = JSON.stringify(version);
|
---|
| 280 | throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`);
|
---|
| 281 | }
|
---|
| 282 | }
|
---|
| 283 | // Not using `instanceof Schema` to allow for duck typing
|
---|
| 284 | if (options.schema instanceof Object)
|
---|
| 285 | this.schema = options.schema;
|
---|
| 286 | else if (opt)
|
---|
| 287 | this.schema = new Schema.Schema(Object.assign(opt, options));
|
---|
| 288 | else
|
---|
| 289 | throw new Error(`With a null YAML version, the { schema: Schema } option is required`);
|
---|
| 290 | }
|
---|
| 291 | // json & jsonArg are only used from toJSON()
|
---|
| 292 | toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) {
|
---|
| 293 | const ctx = {
|
---|
| 294 | anchors: new Map(),
|
---|
| 295 | doc: this,
|
---|
| 296 | keep: !json,
|
---|
| 297 | mapAsMap: mapAsMap === true,
|
---|
| 298 | mapKeyWarned: false,
|
---|
| 299 | maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100
|
---|
| 300 | };
|
---|
| 301 | const res = toJS.toJS(this.contents, jsonArg ?? '', ctx);
|
---|
| 302 | if (typeof onAnchor === 'function')
|
---|
| 303 | for (const { count, res } of ctx.anchors.values())
|
---|
| 304 | onAnchor(res, count);
|
---|
| 305 | return typeof reviver === 'function'
|
---|
| 306 | ? applyReviver.applyReviver(reviver, { '': res }, '', res)
|
---|
| 307 | : res;
|
---|
| 308 | }
|
---|
| 309 | /**
|
---|
| 310 | * A JSON representation of the document `contents`.
|
---|
| 311 | *
|
---|
| 312 | * @param jsonArg Used by `JSON.stringify` to indicate the array index or
|
---|
| 313 | * property name.
|
---|
| 314 | */
|
---|
| 315 | toJSON(jsonArg, onAnchor) {
|
---|
| 316 | return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor });
|
---|
| 317 | }
|
---|
| 318 | /** A YAML representation of the document. */
|
---|
| 319 | toString(options = {}) {
|
---|
| 320 | if (this.errors.length > 0)
|
---|
| 321 | throw new Error('Document with errors cannot be stringified');
|
---|
| 322 | if ('indent' in options &&
|
---|
| 323 | (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) {
|
---|
| 324 | const s = JSON.stringify(options.indent);
|
---|
| 325 | throw new Error(`"indent" option must be a positive integer, not ${s}`);
|
---|
| 326 | }
|
---|
| 327 | return stringifyDocument.stringifyDocument(this, options);
|
---|
| 328 | }
|
---|
| 329 | }
|
---|
| 330 | function assertCollection(contents) {
|
---|
| 331 | if (identity.isCollection(contents))
|
---|
| 332 | return true;
|
---|
| 333 | throw new Error('Expected a YAML collection as document contents');
|
---|
| 334 | }
|
---|
| 335 |
|
---|
| 336 | exports.Document = Document;
|
---|