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;
|
---|