5etools-utils 0.15.7 → 0.15.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,166 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import {BrewTesterBase} from "./BrewTesterBase.js";
3
- import {DataTester, DataTesterBase, BraceCheck, EscapeCharacterCheck} from "../TestData.js";
3
+ import {DataTester, BraceCheck, EscapeCharacterCheck, ImageUrlCheck, CopySourceCheck} from "../TestData.js";
4
4
  import * as Uf from "../UtilFs.js";
5
- import {ObjectWalker} from "../ObjectWalker.js";
6
- import {UtilSource} from "../UtilSource.js";
7
5
  import Um from "../UtilMisc.js";
8
6
 
9
- class _CopySourceCheck extends DataTesterBase {
10
- static _FileState = class {
11
- sources;
12
- dependencies;
13
- internalCopies;
14
-
15
- constructor ({contents}) {
16
- this.sources = new Set(
17
- (contents._meta?.sources?.map(src => src?.json) || [])
18
- .filter(Boolean),
19
- );
20
- this.dependencies = Object.fromEntries(
21
- Object.entries(contents._meta?.dependencies || {})
22
- .map(([prop, arr]) => [prop, new Set(arr)]),
23
- );
24
- this.internalCopies = new Set(contents._meta?.internalCopies || []);
25
- }
26
- };
27
-
28
- registerParsedFileCheckers (parsedJsonChecker) {
29
- parsedJsonChecker.registerFileHandler(this);
30
- }
31
-
32
- handleFile (file, contents) {
33
- if (!file.includes("Kobold Press; Scarlet Citadel.json")) return;
34
-
35
- const fileState = new this.constructor._FileState({contents});
36
-
37
- Object.entries(contents)
38
- .forEach(([prop, arr]) => {
39
- if (prop.startsWith("_")) return;
40
- if (!(arr instanceof Array)) return;
41
-
42
- arr.forEach(ent => {
43
- const propStack = [prop];
44
- const inlineDependencies = new Set();
45
-
46
- ObjectWalker.walk({
47
- obj: ent,
48
- filePath: file,
49
- primitiveHandlers: {
50
- preObject: this._onPreObject.bind(this, {propStack, inlineDependencies}),
51
- object: this._checkObject.bind(this, {fileState, propStack, inlineDependencies}),
52
- postObject: this._onPostObject.bind(this, {propStack, inlineDependencies}),
53
- },
54
- });
55
- });
56
- });
57
- }
58
-
59
- _onPreObject ({propStack, inlineDependencies}, obj) {
60
- if (obj.type !== "statblockInline") return;
61
-
62
- propStack.push(obj.dataType);
63
- (obj.dependencies || []).forEach(dep => inlineDependencies.add(dep));
64
- }
65
-
66
- _onPostObject ({propStack, inlineDependencies}, obj) {
67
- if (obj.type !== "statblockInline") return;
68
-
69
- propStack.pop();
70
- inlineDependencies.clear();
71
- }
72
-
73
- _checkObject ({fileState, propStack, inlineDependencies}, obj, {filePath}) {
74
- if (!obj._copy?.source) return;
75
-
76
- const prop = propStack.at(-1);
77
- const sourceCopy = obj._copy.source;
78
-
79
- // Classes/subclasses have an alternate structure.
80
- if (["class", "subclass"].includes(prop) && UtilSource.isSiteSource(sourceCopy)) {
81
- const classNameLower = obj._copy.className?.toLowerCase();
82
- if (
83
- fileState.dependencies[prop]?.has(classNameLower)
84
- || inlineDependencies.has(classNameLower)
85
- ) return;
86
- }
87
-
88
- // If a root entity, i.e. not in a `statblockInline`, allow internal copies.
89
- if (
90
- propStack.length === 1
91
- && fileState.internalCopies.has(prop)
92
- && fileState.sources.has(sourceCopy)
93
- ) return;
94
-
95
- if (
96
- fileState.dependencies[prop]?.has(sourceCopy)
97
- || inlineDependencies.has(sourceCopy)
98
- ) return;
99
-
100
- this._addMessage(`Entity "${propStack.join(" -> ")}" "${obj.name}" "_copy" source "${sourceCopy}" did not match sources found in dependencies in file "${filePath}"\n`);
101
- }
102
- }
103
-
104
- class _ImageUrlCheck extends DataTesterBase {
105
- static _RE_IMG_PATH = /^(?<type>img|pdf)\/(?<source>[^/]+)\//;
106
-
107
- static _FileState = class {
108
- sources;
109
-
110
- constructor ({contents}) {
111
- this.sources = new Set(
112
- [
113
- ...(contents._meta?.sources?.map(src => src?.json) || [])
114
- .filter(Boolean)
115
- .map(srcJson => srcJson.replace(/:/g, "")),
116
- ...(contents._test?.additionalImageSources || [])
117
- .map(srcJson => srcJson.replace(/:/g, "")),
118
- ],
119
- );
120
- }
121
- };
122
-
123
- constructor ({imgRepoName, urlPrefixExpected}) {
124
- super();
125
- this._imgRepoName = imgRepoName;
126
- this._urlPrefixExpected = urlPrefixExpected;
127
- }
128
-
129
- registerParsedFileCheckers (parsedJsonChecker) {
130
- parsedJsonChecker.registerFileHandler(this);
131
- }
132
-
133
- handleFile (file, contents) {
134
- const fileState = new this.constructor._FileState({contents});
135
-
136
- ObjectWalker.walk({
137
- obj: contents,
138
- filePath: file,
139
- primitiveHandlers: {
140
- object: this._checkObject.bind(this, {fileState}),
141
- },
142
- });
143
- }
144
-
145
- _checkObject ({fileState}, obj, {filePath}) {
146
- if (obj.type !== "image" || obj.href?.type !== "external" || !obj.href?.url) return;
147
-
148
- const {url} = obj.href;
149
- if (!url.toLowerCase().startsWith(this._urlPrefixExpected.toLowerCase())) return;
150
-
151
- const mPath = this.constructor._RE_IMG_PATH.exec(url.slice(this._urlPrefixExpected.length));
152
- if (!mPath) {
153
- this._addMessage(`Unknown "${this._imgRepoName}" URL pattern in file "${filePath}": "${url}"\n`);
154
- return;
155
- }
156
-
157
- const {source, type} = mPath.groups;
158
- if (fileState.sources.has(source)) return;
159
-
160
- this._addMessage(`Image source part "${source}" in "${this._imgRepoName}" ${type} URL did not match sources found in file "_meta" or "_test" in file "${filePath}": "${url}"\n`);
161
- }
162
- }
163
-
164
7
  export class BrewTesterFileContents extends BrewTesterBase {
165
8
  _LOG_TAG = "FILE_CONTENTS";
166
9
 
@@ -180,8 +23,8 @@ export class BrewTesterFileContents extends BrewTesterBase {
180
23
  const dataTesters = [
181
24
  new BraceCheck(),
182
25
  new EscapeCharacterCheck(),
183
- new _ImageUrlCheck({imgRepoName: this._imgRepoName, urlPrefixExpected: this._urlPrefixExpected}),
184
- new _CopySourceCheck(),
26
+ new ImageUrlCheck({imgRepoName: this._imgRepoName, urlPrefixExpected: this._urlPrefixExpected}),
27
+ new CopySourceCheck(),
185
28
  ];
186
29
  DataTester.register({dataTesters});
187
30
 
package/lib/TestData.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import {readJsonSync} from "./UtilFs.js";
3
3
  import {ObjectWalker} from "./ObjectWalker.js";
4
+ import {UtilSource} from "./UtilSource.js";
4
5
 
5
6
  /** Runs multiple handlers on each file, to avoid re-reading each file for each handler */
6
7
  class _ParsedJsonChecker {
@@ -194,9 +195,166 @@ class DataTester {
194
195
  }
195
196
  }
196
197
 
198
+ class CopySourceCheck extends DataTesterBase {
199
+ static _FileState = class {
200
+ sources;
201
+ dependencies;
202
+ internalCopies;
203
+
204
+ constructor ({contents}) {
205
+ this.sources = new Set(
206
+ (contents._meta?.sources?.map(src => src?.json) || [])
207
+ .filter(Boolean),
208
+ );
209
+ this.dependencies = Object.fromEntries(
210
+ Object.entries(contents._meta?.dependencies || {})
211
+ .map(([prop, arr]) => [prop, new Set(arr)]),
212
+ );
213
+ this.internalCopies = new Set(contents._meta?.internalCopies || []);
214
+ }
215
+ };
216
+
217
+ registerParsedFileCheckers (parsedJsonChecker) {
218
+ parsedJsonChecker.registerFileHandler(this);
219
+ }
220
+
221
+ handleFile (file, contents) {
222
+ if (!file.includes("Kobold Press; Scarlet Citadel.json")) return;
223
+
224
+ const fileState = new this.constructor._FileState({contents});
225
+
226
+ Object.entries(contents)
227
+ .forEach(([prop, arr]) => {
228
+ if (prop.startsWith("_")) return;
229
+ if (!(arr instanceof Array)) return;
230
+
231
+ arr.forEach(ent => {
232
+ const propStack = [prop];
233
+ const inlineDependencies = new Set();
234
+
235
+ ObjectWalker.walk({
236
+ obj: ent,
237
+ filePath: file,
238
+ primitiveHandlers: {
239
+ preObject: this._onPreObject.bind(this, {propStack, inlineDependencies}),
240
+ object: this._checkObject.bind(this, {fileState, propStack, inlineDependencies}),
241
+ postObject: this._onPostObject.bind(this, {propStack, inlineDependencies}),
242
+ },
243
+ });
244
+ });
245
+ });
246
+ }
247
+
248
+ _onPreObject ({propStack, inlineDependencies}, obj) {
249
+ if (obj.type !== "statblockInline") return;
250
+
251
+ propStack.push(obj.dataType);
252
+ (obj.dependencies || []).forEach(dep => inlineDependencies.add(dep));
253
+ }
254
+
255
+ _onPostObject ({propStack, inlineDependencies}, obj) {
256
+ if (obj.type !== "statblockInline") return;
257
+
258
+ propStack.pop();
259
+ inlineDependencies.clear();
260
+ }
261
+
262
+ _checkObject ({fileState, propStack, inlineDependencies}, obj, {filePath}) {
263
+ if (!obj._copy?.source) return;
264
+
265
+ const prop = propStack.at(-1);
266
+ const sourceCopy = obj._copy.source;
267
+
268
+ // Classes/subclasses have an alternate structure.
269
+ if (["class", "subclass"].includes(prop) && UtilSource.isSiteSource(sourceCopy)) {
270
+ const classNameLower = obj._copy.className?.toLowerCase();
271
+ if (
272
+ fileState.dependencies[prop]?.has(classNameLower)
273
+ || inlineDependencies.has(classNameLower)
274
+ ) return;
275
+ }
276
+
277
+ // If a root entity, i.e. not in a `statblockInline`, allow internal copies.
278
+ if (
279
+ propStack.length === 1
280
+ && fileState.internalCopies.has(prop)
281
+ && fileState.sources.has(sourceCopy)
282
+ ) return;
283
+
284
+ if (
285
+ fileState.dependencies[prop]?.has(sourceCopy)
286
+ || inlineDependencies.has(sourceCopy)
287
+ ) return;
288
+
289
+ this._addMessage(`Entity "${propStack.join(" -> ")}" "${obj.name}" "_copy" source "${sourceCopy}" did not match sources found in dependencies in file "${filePath}"\n`);
290
+ }
291
+ }
292
+
293
+ class ImageUrlCheck extends DataTesterBase {
294
+ static _RE_IMG_PATH = /^(?<type>img|pdf)\/(?<source>[^/]+)\//;
295
+
296
+ static _FileState = class {
297
+ sources;
298
+
299
+ constructor ({contents}) {
300
+ this.sources = new Set(
301
+ [
302
+ ...(contents._meta?.sources?.map(src => src?.json) || [])
303
+ .filter(Boolean)
304
+ .map(srcJson => srcJson.replace(/:/g, "")),
305
+ ...(contents._test?.additionalImageSources || [])
306
+ .map(srcJson => srcJson.replace(/:/g, "")),
307
+ ],
308
+ );
309
+ }
310
+ };
311
+
312
+ constructor ({imgRepoName, urlPrefixExpected}) {
313
+ super();
314
+ this._imgRepoName = imgRepoName;
315
+ this._urlPrefixExpected = urlPrefixExpected;
316
+ }
317
+
318
+ registerParsedFileCheckers (parsedJsonChecker) {
319
+ parsedJsonChecker.registerFileHandler(this);
320
+ }
321
+
322
+ handleFile (file, contents) {
323
+ const fileState = new this.constructor._FileState({contents});
324
+
325
+ ObjectWalker.walk({
326
+ obj: contents,
327
+ filePath: file,
328
+ primitiveHandlers: {
329
+ object: this._checkObject.bind(this, {fileState}),
330
+ },
331
+ });
332
+ }
333
+
334
+ _checkObject ({fileState}, obj, {filePath}) {
335
+ if (obj.type !== "image" || obj.href?.type !== "external" || !obj.href?.url) return;
336
+
337
+ const {url} = obj.href;
338
+ if (!url.toLowerCase().startsWith(this._urlPrefixExpected.toLowerCase())) return;
339
+
340
+ const mPath = this.constructor._RE_IMG_PATH.exec(url.slice(this._urlPrefixExpected.length));
341
+ if (!mPath) {
342
+ this._addMessage(`Unknown "${this._imgRepoName}" URL pattern in file "${filePath}": "${url}"\n`);
343
+ return;
344
+ }
345
+
346
+ const {source, type} = mPath.groups;
347
+ if (fileState.sources.has(source)) return;
348
+
349
+ this._addMessage(`Image source part "${source}" in "${this._imgRepoName}" ${type} URL did not match sources found in file "_meta" or "_test" in file "${filePath}": "${url}"\n`);
350
+ }
351
+ }
352
+
197
353
  export {
198
354
  DataTester,
199
355
  DataTesterBase,
200
356
  BraceCheck,
201
357
  EscapeCharacterCheck,
358
+ CopySourceCheck,
359
+ ImageUrlCheck,
202
360
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "5etools-utils",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "description": "Shared utilities for the 5etools ecosystem.",
5
5
  "type": "module",
6
6
  "main": "lib/Api.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "1.11.7",
3
+ "version": "1.12.0",
4
4
  "type": "object",
5
5
  "description": "Homebrew for 5etools. Should include arrays titled similarly to the main site data, e.g. `spell` or `class`",
6
6
  "$defs": {
@@ -315,16 +315,6 @@
315
315
  },
316
316
  "markdownDescription": "Structure as per &quot;dependencies&quot;. Additional sources to be included when loading the file."
317
317
  },
318
- "references": {
319
- "description": "A \"soft\" alternative to `\"dependencies\"`, used only for data validation (and may therefore be omitted).\n\nAn array of `[\"<NonSiteJsonSource1>\", ..., \"<NonSiteJsonSourceN>\"]`. Entities from these sources will be made available when running e.g. 5etools' `test-tags.js`.",
320
- "type": "array",
321
- "items": {
322
- "type": "string"
323
- },
324
- "minItems": 1,
325
- "uniqueItems": true,
326
- "markdownDescription": "A &quot;soft&quot; alternative to &quot;dependencies&quot;, used only for data validation (and may therefore be omitted).\n\nAn array of [&quot;&lt;NonSiteJsonSource1&gt;&quot;, ..., &quot;&lt;NonSiteJsonSourceN&gt;&quot;]. Entities from these sources will be made available when running e.g. 5etools&apos; test-tags.js."
327
- },
328
318
  "internalCopies": {
329
319
  "description": "An array of keys that are copied from within the current document. e.g. \"item\", \"monsterFluff\", \"background\" etc.",
330
320
  "type": "array",
@@ -381,6 +371,16 @@
381
371
  "description": "Supplementary information used when testing this homebrew.",
382
372
  "type": "object",
383
373
  "properties": {
374
+ "references": {
375
+ "description": "A \"soft\" alternative to `\"dependencies\"`, used only for data validation (and may therefore be omitted).\n\nAn array of `[\"<NonSiteJsonSource1>\", ..., \"<NonSiteJsonSourceN>\"]`. Entities from these sources will be made available when running e.g. 5etools' `test-tags.js`.",
376
+ "type": "array",
377
+ "items": {
378
+ "type": "string"
379
+ },
380
+ "minItems": 1,
381
+ "uniqueItems": true,
382
+ "markdownDescription": "A &quot;soft&quot; alternative to &quot;dependencies&quot;, used only for data validation (and may therefore be omitted).\n\nAn array of [&quot;&lt;NonSiteJsonSource1&gt;&quot;, ..., &quot;&lt;NonSiteJsonSourceN&gt;&quot;]. Entities from these sources will be made available when running e.g. 5etools&apos; test-tags.js."
383
+ },
384
384
  "additionalImageSources": {
385
385
  "description": "Other sources from which this homebrew should be allowed to use images.",
386
386
  "type": "array",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "util-copy.json",
4
4
  "title": "Util: Copy",
5
- "version": "1.0.10",
5
+ "version": "1.0.11",
6
6
  "$defs": {
7
7
  "_mod_renameArr_rename": {
8
8
  "type": "object",
@@ -1112,7 +1112,9 @@
1112
1112
  "required": [
1113
1113
  "_variables"
1114
1114
  ]
1115
- }
1115
+ },
1116
+ "minItems": 1,
1117
+ "uniqueItems": true
1116
1118
  }
1117
1119
  },
1118
1120
  "required": [
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "util-foundry.json",
4
4
  "title": "Util: Foundry",
5
- "version": "1.1.7",
5
+ "version": "1.1.8",
6
6
  "$defs": {
7
7
  "entryDataObject": {
8
8
  "description": "Additional \"5etools-type\" data to be stored on the entry.",
@@ -1517,6 +1517,110 @@
1517
1517
  "type"
1518
1518
  ]
1519
1519
  },
1520
+ "_foundryActivityObject_forward": {
1521
+ "type": "object",
1522
+ "properties": {
1523
+ "foundryId": {
1524
+ "description": "Use only when required. For example, when linking a \"rider\".",
1525
+ "$ref": "#/$defs/foundryIdShort",
1526
+ "markdownDescription": "Use only when required. For example, when linking a &quot;rider&quot;."
1527
+ },
1528
+ "img": {
1529
+ "type": "string"
1530
+ },
1531
+ "description": {
1532
+ "type": "object"
1533
+ },
1534
+ "descriptionEntries": {
1535
+ "description": "If supplied, will be used to populate \"description.chatFlavor\".",
1536
+ "type": "array",
1537
+ "items": {
1538
+ "$ref": "entry.json"
1539
+ },
1540
+ "markdownDescription": "If supplied, will be used to populate &quot;description.chatFlavor&quot;."
1541
+ },
1542
+ "consumption": {
1543
+ "type": "object",
1544
+ "properties": {
1545
+ "targets": {
1546
+ "type": "array",
1547
+ "items": {
1548
+ "type": "object",
1549
+ "properties": {
1550
+ "target": {
1551
+ "oneOf": [
1552
+ {
1553
+ "type": "string"
1554
+ },
1555
+ {
1556
+ "description": "A link to an importable entity",
1557
+ "type": "object",
1558
+ "properties": {
1559
+ "prop": {
1560
+ "type": "string"
1561
+ },
1562
+ "uid": {
1563
+ "type": "string"
1564
+ }
1565
+ },
1566
+ "required": [
1567
+ "prop",
1568
+ "uid"
1569
+ ],
1570
+ "additionalProperties": false,
1571
+ "markdownDescription": "A link to an importable entity"
1572
+ },
1573
+ {
1574
+ "description": "A link to a 5etools-style resource",
1575
+ "type": "object",
1576
+ "properties": {
1577
+ "consumes": {
1578
+ "type": "object",
1579
+ "properties": {
1580
+ "name": {
1581
+ "$ref": "util.json#/$defs/consumesName"
1582
+ }
1583
+ },
1584
+ "required": [
1585
+ "name"
1586
+ ],
1587
+ "additionalProperties": false
1588
+ }
1589
+ },
1590
+ "required": [
1591
+ "consumes"
1592
+ ],
1593
+ "additionalProperties": false,
1594
+ "markdownDescription": "A link to a 5etools-style resource"
1595
+ }
1596
+ ]
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ },
1603
+ "effects": {
1604
+ "type": "array",
1605
+ "items": {
1606
+ "$ref": "#/$defs/_foundryIdRef"
1607
+ }
1608
+ },
1609
+ "_id": false,
1610
+ "sort": false,
1611
+ "template": false,
1612
+ "affects": false,
1613
+ "type": {
1614
+ "const": "forward"
1615
+ },
1616
+ "activity": {
1617
+ "$ref": "#/$defs/_foundryIdRef"
1618
+ }
1619
+ },
1620
+ "required": [
1621
+ "type"
1622
+ ]
1623
+ },
1520
1624
  "_foundryActivityObject_utility": {
1521
1625
  "type": "object",
1522
1626
  "properties": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "version": "1.11.7",
3
+ "version": "1.12.0",
4
4
  "type": "object",
5
5
  "description": "Homebrew for 5etools. Should include arrays titled similarly to the main site data, e.g. `spell` or `class`",
6
6
  "$defs": {
@@ -313,16 +313,6 @@
313
313
  },
314
314
  "markdownDescription": "Structure as per &quot;dependencies&quot;. Additional sources to be included when loading the file."
315
315
  },
316
- "references": {
317
- "description": "A \"soft\" alternative to `\"dependencies\"`, used only for data validation (and may therefore be omitted).\n\nAn array of `[\"<NonSiteJsonSource1>\", ..., \"<NonSiteJsonSourceN>\"]`. Entities from these sources will be made available when running e.g. 5etools' `test-tags.js`.",
318
- "type": "array",
319
- "items": {
320
- "type": "string"
321
- },
322
- "minItems": 1,
323
- "uniqueItems": true,
324
- "markdownDescription": "A &quot;soft&quot; alternative to &quot;dependencies&quot;, used only for data validation (and may therefore be omitted).\n\nAn array of [&quot;&lt;NonSiteJsonSource1&gt;&quot;, ..., &quot;&lt;NonSiteJsonSourceN&gt;&quot;]. Entities from these sources will be made available when running e.g. 5etools&apos; test-tags.js."
325
- },
326
316
  "internalCopies": {
327
317
  "description": "An array of keys that are copied from within the current document. e.g. \"item\", \"monsterFluff\", \"background\" etc.",
328
318
  "type": "array",
@@ -379,6 +369,16 @@
379
369
  "description": "Supplementary information used when testing this homebrew.",
380
370
  "type": "object",
381
371
  "properties": {
372
+ "references": {
373
+ "description": "A \"soft\" alternative to `\"dependencies\"`, used only for data validation (and may therefore be omitted).\n\nAn array of `[\"<NonSiteJsonSource1>\", ..., \"<NonSiteJsonSourceN>\"]`. Entities from these sources will be made available when running e.g. 5etools' `test-tags.js`.",
374
+ "type": "array",
375
+ "items": {
376
+ "type": "string"
377
+ },
378
+ "minItems": 1,
379
+ "uniqueItems": true,
380
+ "markdownDescription": "A &quot;soft&quot; alternative to &quot;dependencies&quot;, used only for data validation (and may therefore be omitted).\n\nAn array of [&quot;&lt;NonSiteJsonSource1&gt;&quot;, ..., &quot;&lt;NonSiteJsonSourceN&gt;&quot;]. Entities from these sources will be made available when running e.g. 5etools&apos; test-tags.js."
381
+ },
382
382
  "additionalImageSources": {
383
383
  "description": "Other sources from which this homebrew should be allowed to use images.",
384
384
  "type": "array",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "util-copy.json",
4
4
  "title": "Util: Copy",
5
- "version": "1.0.10",
5
+ "version": "1.0.11",
6
6
  "$defs": {
7
7
  "_mod_renameArr_rename": {
8
8
  "type": "object",
@@ -1112,7 +1112,9 @@
1112
1112
  "required": [
1113
1113
  "_variables"
1114
1114
  ]
1115
- }
1115
+ },
1116
+ "minItems": 1,
1117
+ "uniqueItems": true
1116
1118
  }
1117
1119
  },
1118
1120
  "required": [