Collection.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import { createNode } from '../doc/createNode.js';
  2. import { isNode, isPair, isCollection, isScalar } from './identity.js';
  3. import { NodeBase } from './Node.js';
  4. function collectionFromPath(schema, path, value) {
  5. let v = value;
  6. for (let i = path.length - 1; i >= 0; --i) {
  7. const k = path[i];
  8. if (typeof k === 'number' && Number.isInteger(k) && k >= 0) {
  9. const a = [];
  10. a[k] = v;
  11. v = a;
  12. }
  13. else {
  14. v = new Map([[k, v]]);
  15. }
  16. }
  17. return createNode(v, undefined, {
  18. aliasDuplicateObjects: false,
  19. keepUndefined: false,
  20. onAnchor: () => {
  21. throw new Error('This should not happen, please report a bug.');
  22. },
  23. schema,
  24. sourceObjects: new Map()
  25. });
  26. }
  27. // Type guard is intentionally a little wrong so as to be more useful,
  28. // as it does not cover untypable empty non-string iterables (e.g. []).
  29. const isEmptyPath = (path) => path == null ||
  30. (typeof path === 'object' && !!path[Symbol.iterator]().next().done);
  31. class Collection extends NodeBase {
  32. constructor(type, schema) {
  33. super(type);
  34. Object.defineProperty(this, 'schema', {
  35. value: schema,
  36. configurable: true,
  37. enumerable: false,
  38. writable: true
  39. });
  40. }
  41. /**
  42. * Create a copy of this collection.
  43. *
  44. * @param schema - If defined, overwrites the original's schema
  45. */
  46. clone(schema) {
  47. const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
  48. if (schema)
  49. copy.schema = schema;
  50. copy.items = copy.items.map(it => isNode(it) || isPair(it) ? it.clone(schema) : it);
  51. if (this.range)
  52. copy.range = this.range.slice();
  53. return copy;
  54. }
  55. /**
  56. * Adds a value to the collection. For `!!map` and `!!omap` the value must
  57. * be a Pair instance or a `{ key, value }` object, which may not have a key
  58. * that already exists in the map.
  59. */
  60. addIn(path, value) {
  61. if (isEmptyPath(path))
  62. this.add(value);
  63. else {
  64. const [key, ...rest] = path;
  65. const node = this.get(key, true);
  66. if (isCollection(node))
  67. node.addIn(rest, value);
  68. else if (node === undefined && this.schema)
  69. this.set(key, collectionFromPath(this.schema, rest, value));
  70. else
  71. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  72. }
  73. }
  74. /**
  75. * Removes a value from the collection.
  76. * @returns `true` if the item was found and removed.
  77. */
  78. deleteIn(path) {
  79. const [key, ...rest] = path;
  80. if (rest.length === 0)
  81. return this.delete(key);
  82. const node = this.get(key, true);
  83. if (isCollection(node))
  84. return node.deleteIn(rest);
  85. else
  86. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  87. }
  88. /**
  89. * Returns item at `key`, or `undefined` if not found. By default unwraps
  90. * scalar values from their surrounding node; to disable set `keepScalar` to
  91. * `true` (collections are always returned intact).
  92. */
  93. getIn(path, keepScalar) {
  94. const [key, ...rest] = path;
  95. const node = this.get(key, true);
  96. if (rest.length === 0)
  97. return !keepScalar && isScalar(node) ? node.value : node;
  98. else
  99. return isCollection(node) ? node.getIn(rest, keepScalar) : undefined;
  100. }
  101. hasAllNullValues(allowScalar) {
  102. return this.items.every(node => {
  103. if (!isPair(node))
  104. return false;
  105. const n = node.value;
  106. return (n == null ||
  107. (allowScalar &&
  108. isScalar(n) &&
  109. n.value == null &&
  110. !n.commentBefore &&
  111. !n.comment &&
  112. !n.tag));
  113. });
  114. }
  115. /**
  116. * Checks if the collection includes a value with the key `key`.
  117. */
  118. hasIn(path) {
  119. const [key, ...rest] = path;
  120. if (rest.length === 0)
  121. return this.has(key);
  122. const node = this.get(key, true);
  123. return isCollection(node) ? node.hasIn(rest) : false;
  124. }
  125. /**
  126. * Sets a value in this collection. For `!!set`, `value` needs to be a
  127. * boolean to add/remove the item from the set.
  128. */
  129. setIn(path, value) {
  130. const [key, ...rest] = path;
  131. if (rest.length === 0) {
  132. this.set(key, value);
  133. }
  134. else {
  135. const node = this.get(key, true);
  136. if (isCollection(node))
  137. node.setIn(rest, value);
  138. else if (node === undefined && this.schema)
  139. this.set(key, collectionFromPath(this.schema, rest, value));
  140. else
  141. throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`);
  142. }
  143. }
  144. }
  145. export { Collection, collectionFromPath, isEmptyPath };