UNPKG

13.1 kBJavaScriptView Raw
1'use strict';
2
3var Alias = require('../nodes/Alias.js');
4var Collection = require('../nodes/Collection.js');
5var identity = require('../nodes/identity.js');
6var Pair = require('../nodes/Pair.js');
7var toJS = require('../nodes/toJS.js');
8var Schema = require('../schema/Schema.js');
9var stringifyDocument = require('../stringify/stringifyDocument.js');
10var anchors = require('./anchors.js');
11var applyReviver = require('./applyReviver.js');
12var createNode = require('./createNode.js');
13var directives = require('./directives.js');
14
15class 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}
330function assertCollection(contents) {
331 if (identity.isCollection(contents))
332 return true;
333 throw new Error('Expected a YAML collection as document contents');
334}
335
336exports.Document = Document;