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