@cobbled-creators/cobblemon-schemas 1.7.0

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.
Files changed (85) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +68 -0
  3. package/bin/cobblemon-schemas.mjs +51 -0
  4. package/dist/bundle.json +1 -0
  5. package/dist/validators.mjs +3 -0
  6. package/package.json +66 -0
  7. package/schemas/_shared/common.schema.json +89 -0
  8. package/schemas/_shared/defs/collections.schema.json +32 -0
  9. package/schemas/_shared/defs/drops.schema.json +89 -0
  10. package/schemas/_shared/defs/expressions.schema.json +82 -0
  11. package/schemas/_shared/defs/identifiers.schema.json +24 -0
  12. package/schemas/_shared/defs/npc.schema.json +76 -0
  13. package/schemas/_shared/defs/pokemon.schema.json +41 -0
  14. package/schemas/_shared/defs/ranges.schema.json +70 -0
  15. package/schemas/_shared/defs/ride.schema.json +77 -0
  16. package/schemas/_shared/defs/spawning.schema.json +183 -0
  17. package/schemas/_shared/defs/vectors.schema.json +44 -0
  18. package/schemas/action_effects/schema.json +123 -0
  19. package/schemas/bedrock_pokemon_resolvers/schema.json +184 -0
  20. package/schemas/bedrock_posers/schema.json +367 -0
  21. package/schemas/behaviours/schema.json +145 -0
  22. package/schemas/berries/schema.json +178 -0
  23. package/schemas/cosmetic_items/schema.json +55 -0
  24. package/schemas/dex_additions/schema.json +22 -0
  25. package/schemas/dex_entries/schema.json +83 -0
  26. package/schemas/dex_entry_additions/schema.json +63 -0
  27. package/schemas/dexes/schema.json +89 -0
  28. package/schemas/dialogues/schema.json +260 -0
  29. package/schemas/fossils/schema.json +22 -0
  30. package/schemas/global_species_features/schema.json +6 -0
  31. package/schemas/index.json +244 -0
  32. package/schemas/marks/schema.json +49 -0
  33. package/schemas/mechanics/aprijuices.schema.json +25 -0
  34. package/schemas/mechanics/berries.schema.json +35 -0
  35. package/schemas/mechanics/potions.schema.json +23 -0
  36. package/schemas/mechanics/remedies.schema.json +31 -0
  37. package/schemas/mechanics/slowpoke_tails.schema.json +17 -0
  38. package/schemas/natural_materials/schema.json +40 -0
  39. package/schemas/npc_presets/schema.json +6 -0
  40. package/schemas/npcs/schema.json +443 -0
  41. package/schemas/pokemon_interactions/schema.json +169 -0
  42. package/schemas/pokerods/schema.json +19 -0
  43. package/schemas/ride_settings/bird.schema.json +48 -0
  44. package/schemas/ride_settings/boat.schema.json +45 -0
  45. package/schemas/ride_settings/burst.schema.json +11 -0
  46. package/schemas/ride_settings/dolphin.schema.json +45 -0
  47. package/schemas/ride_settings/glider.schema.json +18 -0
  48. package/schemas/ride_settings/helicopter.schema.json +21 -0
  49. package/schemas/ride_settings/horse.schema.json +36 -0
  50. package/schemas/ride_settings/hover.schema.json +33 -0
  51. package/schemas/ride_settings/jet.schema.json +42 -0
  52. package/schemas/ride_settings/minekart.schema.json +33 -0
  53. package/schemas/ride_settings/rocket.schema.json +36 -0
  54. package/schemas/ride_settings/submarine.schema.json +33 -0
  55. package/schemas/ride_settings/vehicle.schema.json +45 -0
  56. package/schemas/seasonings/schema.json +124 -0
  57. package/schemas/spawn_bait_effects/schema.json +42 -0
  58. package/schemas/spawn_detail_presets/schema.json +79 -0
  59. package/schemas/spawn_pool_world/schema.json +455 -0
  60. package/schemas/spawn_rules/schema.json +217 -0
  61. package/schemas/species/schema.json +1043 -0
  62. package/schemas/species_additions/schema.json +22 -0
  63. package/schemas/species_feature_assignments/schema.json +25 -0
  64. package/schemas/species_features/schema.json +139 -0
  65. package/schemas/starters/schema.json +28 -0
  66. package/schemas/unlockable_pc_box_wallpapers/schema.json +29 -0
  67. package/src/errors/format.mjs +46 -0
  68. package/src/errors/pointer.mjs +30 -0
  69. package/src/errors/simplify.mjs +58 -0
  70. package/src/errors/type-branches.mjs +65 -0
  71. package/src/errors/values.mjs +7 -0
  72. package/src/index.d.ts +74 -0
  73. package/src/index.mjs +6 -0
  74. package/src/json/line-index.mjs +21 -0
  75. package/src/json/parse-json.mjs +28 -0
  76. package/src/json/syntax-errors.mjs +56 -0
  77. package/src/node/load-engine.mjs +30 -0
  78. package/src/node/load-schemas.mjs +35 -0
  79. package/src/node/package-info.mjs +10 -0
  80. package/src/node/read-pack.mjs +18 -0
  81. package/src/node.d.ts +13 -0
  82. package/src/node.mjs +5 -0
  83. package/src/schema-engine.mjs +43 -0
  84. package/src/schema-index.mjs +54 -0
  85. package/src/validate-pack.mjs +28 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cobblemon-schema.local/species_additions/schema.json",
4
+ "title": "Cobblemon Species Addition",
5
+ "type": "object",
6
+ "properties": {
7
+ "target": {
8
+ "$ref": "../_shared/defs/identifiers.schema.json#/$defs/identifier"
9
+ }
10
+ },
11
+ "required": [
12
+ "target"
13
+ ],
14
+ "additionalProperties": true,
15
+ "allOf": [
16
+ {
17
+ "not": {
18
+ "maxProperties": 1
19
+ }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cobblemon-schema.local/species_feature_assignments/schema.json",
4
+ "title": "Cobblemon Species Feature Assignment",
5
+ "type": "object",
6
+ "properties": {
7
+ "pokemon": {
8
+ "type": "array",
9
+ "items": {
10
+ "$ref": "../_shared/defs/pokemon.schema.json#/$defs/pokemonProperties"
11
+ }
12
+ },
13
+ "features": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "string"
17
+ }
18
+ }
19
+ },
20
+ "required": [
21
+ "pokemon",
22
+ "features"
23
+ ],
24
+ "additionalProperties": false
25
+ }
@@ -0,0 +1,139 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cobblemon-schema.local/species_features/schema.json",
4
+ "title": "Cobblemon Species Feature Provider",
5
+ "type": "object",
6
+ "properties": {
7
+ "type": {
8
+ "type": "string",
9
+ "enum": [
10
+ "choice",
11
+ "flag",
12
+ "integer"
13
+ ]
14
+ },
15
+ "keys": {
16
+ "type": "array",
17
+ "minItems": 1,
18
+ "items": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "default": {
23
+ "oneOf": [
24
+ {
25
+ "type": "string"
26
+ },
27
+ {
28
+ "type": "integer"
29
+ },
30
+ {
31
+ "type": "boolean"
32
+ }
33
+ ]
34
+ },
35
+ "visible": {
36
+ "type": "boolean"
37
+ },
38
+ "needsKey": {
39
+ "type": "boolean"
40
+ },
41
+ "choices": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string"
45
+ }
46
+ },
47
+ "isAspect": {
48
+ "type": "boolean"
49
+ },
50
+ "aspectFormat": {
51
+ "type": "string"
52
+ },
53
+ "min": {
54
+ "type": "integer"
55
+ },
56
+ "max": {
57
+ "type": "integer"
58
+ },
59
+ "display": {
60
+ "type": "object",
61
+ "properties": {
62
+ "name": {
63
+ "type": "string"
64
+ },
65
+ "colour": {
66
+ "type": "array",
67
+ "minItems": 3,
68
+ "maxItems": 3,
69
+ "items": {
70
+ "type": "number"
71
+ }
72
+ },
73
+ "color": {
74
+ "type": "array",
75
+ "minItems": 3,
76
+ "maxItems": 3,
77
+ "items": {
78
+ "type": "number"
79
+ }
80
+ },
81
+ "underlay": {
82
+ "$ref": "../_shared/defs/identifiers.schema.json#/$defs/identifier"
83
+ },
84
+ "overlay": {
85
+ "$ref": "../_shared/defs/identifiers.schema.json#/$defs/identifier"
86
+ }
87
+ },
88
+ "additionalProperties": false
89
+ },
90
+ "itemPoints": {
91
+ "type": "object",
92
+ "additionalProperties": {
93
+ "type": "integer"
94
+ }
95
+ }
96
+ },
97
+ "required": [
98
+ "type",
99
+ "keys"
100
+ ],
101
+ "allOf": [
102
+ {
103
+ "if": {
104
+ "properties": {
105
+ "type": {
106
+ "const": "choice"
107
+ }
108
+ },
109
+ "required": [
110
+ "type"
111
+ ]
112
+ },
113
+ "then": {
114
+ "required": [
115
+ "choices"
116
+ ]
117
+ }
118
+ },
119
+ {
120
+ "if": {
121
+ "properties": {
122
+ "type": {
123
+ "const": "integer"
124
+ }
125
+ },
126
+ "required": [
127
+ "type"
128
+ ]
129
+ },
130
+ "then": {
131
+ "required": [
132
+ "min",
133
+ "max"
134
+ ]
135
+ }
136
+ }
137
+ ],
138
+ "additionalProperties": false
139
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cobblemon-schema.local/starters/schema.json",
4
+ "title": "Cobblemon Starter Category",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "minLength": 1
10
+ },
11
+ "displayName": {
12
+ "type": "string",
13
+ "minLength": 1
14
+ },
15
+ "pokemon": {
16
+ "type": "array",
17
+ "minItems": 1,
18
+ "items": {
19
+ "$ref": "../_shared/defs/pokemon.schema.json#/$defs/pokemonProperties"
20
+ }
21
+ }
22
+ },
23
+ "required": [
24
+ "name",
25
+ "pokemon"
26
+ ],
27
+ "additionalProperties": false
28
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cobblemon-schema.local/unlockable_pc_box_wallpapers/schema.json",
4
+ "title": "Cobblemon Unlockable PC Box Wallpaper",
5
+ "type": "object",
6
+ "properties": {
7
+ "enabled": {
8
+ "type": "boolean"
9
+ },
10
+ "displayName": {
11
+ "oneOf": [
12
+ {
13
+ "type": "string"
14
+ },
15
+ {
16
+ "type": "null"
17
+ }
18
+ ]
19
+ },
20
+ "texture": {
21
+ "$ref": "../_shared/defs/identifiers.schema.json#/$defs/identifier"
22
+ }
23
+ },
24
+ "required": [
25
+ "enabled",
26
+ "texture"
27
+ ],
28
+ "additionalProperties": false
29
+ }
@@ -0,0 +1,46 @@
1
+ import { asRecord, stringArray } from './values.mjs';
2
+
3
+ const MAX_LISTED_VALUES = 8;
4
+
5
+ export function formatAjvError(error) {
6
+ const params = error.params ?? {};
7
+ switch (error.keyword) {
8
+ case 'required':
9
+ return `Missing required property '${params.missingProperty}'.`;
10
+ case 'additionalProperties':
11
+ return `Unknown property '${params.additionalProperty}'.`;
12
+ case 'enum':
13
+ return `Value must be one of: ${listValues(params.allowedValues ?? [])}.`;
14
+ case 'const':
15
+ return `Value must be ${JSON.stringify(params.allowedValue)}.`;
16
+ case 'type':
17
+ return `Expected type '${params.type}'.`;
18
+ case 'minProperties':
19
+ return `Object must define at least ${countProperties(params.limit)}.`;
20
+ case 'not':
21
+ return formatNot(asRecord(asRecord(error.parentSchema)?.not));
22
+ case 'oneOf':
23
+ return 'Value must match exactly one supported schema shape.';
24
+ case 'anyOf':
25
+ return 'Value does not match any supported schema shape.';
26
+ default:
27
+ return error.message ?? 'Validation failed';
28
+ }
29
+ }
30
+
31
+ function formatNot(notSchema) {
32
+ if (typeof notSchema?.maxProperties === 'number') return `Object must define more than ${countProperties(notSchema.maxProperties)}.`;
33
+ const disallowed = stringArray(notSchema?.required);
34
+ if (disallowed.length === 1) return `Property '${disallowed[0]}' is not allowed in this context.`;
35
+ if (disallowed.length > 1) return `These properties are not allowed in this context: ${disallowed.join(', ')}.`;
36
+ return 'Value matches a disallowed schema shape.';
37
+ }
38
+
39
+ function listValues(values) {
40
+ const shown = values.slice(0, MAX_LISTED_VALUES).map((value) => JSON.stringify(value)).join(', ');
41
+ return values.length > MAX_LISTED_VALUES ? `${shown}, ...` : shown;
42
+ }
43
+
44
+ function countProperties(count) {
45
+ return `${count} ${count === 1 ? 'property' : 'properties'}`;
46
+ }
@@ -0,0 +1,30 @@
1
+ export function decodePointer(pointer) {
2
+ if (!pointer || pointer === '/') return [];
3
+ return pointer
4
+ .split('/')
5
+ .slice(1)
6
+ .map(unescapeSegment)
7
+ .map((segment) => (/^[0-9]+$/.test(segment) ? Number.parseInt(segment, 10) : segment));
8
+ }
9
+
10
+ export function parentPointer(pointer) {
11
+ const lastSlash = pointer.lastIndexOf('/');
12
+ return lastSlash <= 0 ? '' : pointer.slice(0, lastSlash);
13
+ }
14
+
15
+ export function isBelow(pointer, ancestor) {
16
+ return pointer.startsWith(ancestor ? `${ancestor}/` : '/');
17
+ }
18
+
19
+ export function valueAt(root, pointer) {
20
+ let current = root;
21
+ for (const segment of decodePointer(pointer)) {
22
+ if (current === null || typeof current !== 'object') return undefined;
23
+ current = current[segment];
24
+ }
25
+ return current;
26
+ }
27
+
28
+ function unescapeSegment(segment) {
29
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
30
+ }
@@ -0,0 +1,58 @@
1
+ import { isBelow, parentPointer } from './pointer.mjs';
2
+ import { findTypeBranches } from './type-branches.mjs';
3
+
4
+ const COMBINATOR_KEYWORDS = new Set(['anyOf', 'oneOf', 'if']);
5
+
6
+ export function simplifyAjvErrors(errors, rootData) {
7
+ const branches = findTypeBranches(errors, rootData);
8
+ const meaningful = errors.filter((error) =>
9
+ !isOtherBranchError(error, branches)
10
+ && !isParentTypeError(error, errors)
11
+ && !isRedundantCombinatorError(error, errors));
12
+ return withoutDuplicates(meaningful);
13
+ }
14
+
15
+ function isOtherBranchError(error, branches) {
16
+ if (error.keyword === 'required') return isRequiredByOtherBranch(error, branches.get(error.instancePath));
17
+ if (error.keyword === 'const' && error.instancePath.endsWith('/type')) {
18
+ return isOtherBranchTypeConst(error, branches.get(parentPointer(error.instancePath)));
19
+ }
20
+ return false;
21
+ }
22
+
23
+ function isRequiredByOtherBranch(error, branch) {
24
+ const missing = error.params?.missingProperty;
25
+ if (!missing || !branch?.actualType) return false;
26
+ if (branch.selectedRequired.size > 0) return !branch.selectedRequired.has(missing);
27
+ return branch.typeIsInvalid || branch.hasOtherErrorsInside;
28
+ }
29
+
30
+ function isOtherBranchTypeConst(error, branch) {
31
+ if (!branch?.actualType) return false;
32
+ if (branch.typeIsInvalid || branch.hasOtherErrorsInside) return true;
33
+ if (branch.selectedRequired.size === 0) return false;
34
+ return error.params?.allowedValue !== branch.actualType;
35
+ }
36
+
37
+ function isParentTypeError(error, errors) {
38
+ if (error.keyword !== 'type' || !error.params?.type) return false;
39
+ return errors.some((other) => other !== error && !COMBINATOR_KEYWORDS.has(other.keyword) && isBelow(other.instancePath, error.instancePath));
40
+ }
41
+
42
+ function isRedundantCombinatorError(error, errors) {
43
+ if (!COMBINATOR_KEYWORDS.has(error.keyword)) return false;
44
+ return errors.some((other) =>
45
+ other !== error
46
+ && !COMBINATOR_KEYWORDS.has(other.keyword)
47
+ && (other.instancePath === error.instancePath || isBelow(other.instancePath, error.instancePath)));
48
+ }
49
+
50
+ function withoutDuplicates(errors) {
51
+ const seen = new Set();
52
+ return errors.filter((error) => {
53
+ const key = `${error.keyword}|${error.instancePath}|${error.schemaPath}|${JSON.stringify(error.params)}`;
54
+ if (seen.has(key)) return false;
55
+ seen.add(key);
56
+ return true;
57
+ });
58
+ }
@@ -0,0 +1,65 @@
1
+ import { isBelow, parentPointer, valueAt } from './pointer.mjs';
2
+ import { asRecord, stringArray } from './values.mjs';
3
+
4
+ const BRANCH_KEYWORDS = new Set(['anyOf', 'oneOf', 'if', 'const', 'required']);
5
+
6
+ export function findTypeBranches(errors, rootData) {
7
+ const branches = new Map();
8
+ for (const [objectPointer, branch] of collectBranches(errors, rootData)) {
9
+ branches.set(objectPointer, summarizeBranch(objectPointer, branch, errors));
10
+ }
11
+ return branches;
12
+ }
13
+
14
+ function collectBranches(errors, rootData) {
15
+ const branches = new Map();
16
+ for (const error of errors) {
17
+ const parentSchema = asRecord(error.parentSchema);
18
+ const branchType = asRecord(asRecord(parentSchema?.properties)?.type)?.const;
19
+ if (typeof branchType !== 'string') continue;
20
+
21
+ const objectPointer = error.instancePath.endsWith('/type') ? parentPointer(error.instancePath) : error.instancePath;
22
+ if (!objectPointer) continue;
23
+
24
+ if (!branches.has(objectPointer)) {
25
+ branches.set(objectPointer, { actualType: typeAt(rootData, objectPointer), requiredByType: new Map() });
26
+ }
27
+ const { requiredByType } = branches.get(objectPointer);
28
+ const required = requiredByType.get(branchType) ?? new Set();
29
+ for (const field of requiredFields(parentSchema)) required.add(field);
30
+ requiredByType.set(branchType, required);
31
+ }
32
+ return branches;
33
+ }
34
+
35
+ function summarizeBranch(objectPointer, { actualType, requiredByType }, errors) {
36
+ const typePointer = `${objectPointer}/type`;
37
+ const typeIsInvalid = errors.some((error) => error.instancePath === typePointer && (error.keyword === 'enum' || error.keyword === 'type'));
38
+ const hasOtherErrorsInside = errors.some((error) =>
39
+ error.instancePath !== typePointer
40
+ && isBelow(error.instancePath, objectPointer)
41
+ && !BRANCH_KEYWORDS.has(error.keyword));
42
+
43
+ return {
44
+ actualType,
45
+ selectedRequired: (actualType && requiredByType.get(actualType)) || new Set(),
46
+ typeIsInvalid,
47
+ hasOtherErrorsInside,
48
+ };
49
+ }
50
+
51
+ function requiredFields(schema) {
52
+ const fields = new Set(stringArray(schema.required));
53
+ for (const combinator of [schema.anyOf, schema.oneOf]) {
54
+ if (!Array.isArray(combinator)) continue;
55
+ for (const option of combinator) {
56
+ for (const field of stringArray(asRecord(option)?.required)) fields.add(field);
57
+ }
58
+ }
59
+ return fields;
60
+ }
61
+
62
+ function typeAt(rootData, pointer) {
63
+ const type = asRecord(valueAt(rootData, pointer))?.type;
64
+ return typeof type === 'string' ? type : undefined;
65
+ }
@@ -0,0 +1,7 @@
1
+ export function asRecord(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
3
+ }
4
+
5
+ export function stringArray(value) {
6
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
7
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ export type Pointer = Array<string | number>;
2
+
3
+ export interface CobblemonTarget {
4
+ version: string;
5
+ minecraft: string;
6
+ baseline: string;
7
+ }
8
+
9
+ export interface SchemaEntry {
10
+ path: string;
11
+ id?: string;
12
+ dataPath?: string;
13
+ validator?: string;
14
+ }
15
+
16
+ export interface Bundle {
17
+ package: string;
18
+ cobblemon: CobblemonTarget;
19
+ entries: SchemaEntry[];
20
+ }
21
+
22
+ export interface SchemaError {
23
+ pointer: Pointer;
24
+ keyword: string;
25
+ params: Record<string, unknown>;
26
+ message: string;
27
+ }
28
+
29
+ export type ValidateFunction = ((value: unknown) => boolean) & { errors?: unknown[] | null };
30
+
31
+ export class SchemaEngine {
32
+ constructor(options: { entries: SchemaEntry[]; validators: Record<string, ValidateFunction>; cobblemon?: CobblemonTarget | null });
33
+ static fromBundle(bundle: Bundle, compiled: Record<string, ValidateFunction>): SchemaEngine;
34
+ readonly cobblemon: CobblemonTarget | null;
35
+ resolve(relPath: string): string | undefined;
36
+ validate(schemaPath: string, value: unknown): SchemaError[];
37
+ check(relPath: string, value: unknown): { schema: string | null; errors: SchemaError[] };
38
+ }
39
+
40
+ export function simplifyAjvErrors<T>(errors: T[], rootData: unknown): T[];
41
+ export function formatAjvError(error: unknown): string;
42
+ export function decodePointer(pointer: string): Pointer;
43
+
44
+ export interface Position {
45
+ line: number;
46
+ column: number;
47
+ }
48
+
49
+ export interface ParsedJson {
50
+ value: unknown;
51
+ ok: boolean;
52
+ syntaxErrors: Array<Position & { message: string }>;
53
+ locate(pointer: Pointer): Position | Record<string, never>;
54
+ }
55
+
56
+ export function parseJson(bytes: Uint8Array | string): ParsedJson;
57
+
58
+ export interface PackError {
59
+ kind: 'syntax' | 'schema';
60
+ pointer: Pointer;
61
+ message: string;
62
+ line?: number;
63
+ column?: number;
64
+ keyword?: string;
65
+ params?: Record<string, unknown>;
66
+ }
67
+
68
+ export interface PackResult {
69
+ file: string;
70
+ schema: string;
71
+ errors: PackError[];
72
+ }
73
+
74
+ export function validatePack(files: Map<string, Uint8Array | string>, engine: SchemaEngine): PackResult[];
package/src/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ export { SchemaEngine } from './schema-engine.mjs';
2
+ export { simplifyAjvErrors } from './errors/simplify.mjs';
3
+ export { formatAjvError } from './errors/format.mjs';
4
+ export { decodePointer } from './errors/pointer.mjs';
5
+ export { parseJson } from './json/parse-json.mjs';
6
+ export { validatePack } from './validate-pack.mjs';
@@ -0,0 +1,21 @@
1
+ export function createLineIndex(text) {
2
+ const lineStarts = [0];
3
+ for (let offset = 0; offset < text.length; offset += 1) {
4
+ if (text.charCodeAt(offset) === 10) lineStarts.push(offset + 1);
5
+ }
6
+ return (offset) => {
7
+ const line = lineContaining(lineStarts, offset);
8
+ return { line: line + 1, column: offset - lineStarts[line] + 1 };
9
+ };
10
+ }
11
+
12
+ function lineContaining(lineStarts, offset) {
13
+ let low = 0;
14
+ let high = lineStarts.length - 1;
15
+ while (low < high) {
16
+ const middle = (low + high + 1) >> 1;
17
+ if (lineStarts[middle] <= offset) low = middle;
18
+ else high = middle - 1;
19
+ }
20
+ return low;
21
+ }
@@ -0,0 +1,28 @@
1
+ import { findNodeAtLocation, parse, parseTree } from 'jsonc-parser';
2
+ import { createLineIndex } from './line-index.mjs';
3
+ import { describeSyntaxErrors } from './syntax-errors.mjs';
4
+
5
+ const STRICT_COMMAS = { allowTrailingComma: false, disallowComments: false };
6
+ const LENIENT_COMMAS = { allowTrailingComma: true, disallowComments: false };
7
+
8
+ export function parseJson(content) {
9
+ const text = withoutBom(typeof content === 'string' ? content : new TextDecoder('utf-8', { ignoreBOM: true }).decode(content));
10
+ const positionOf = createLineIndex(text);
11
+ const parseErrors = [];
12
+ const value = parse(text, parseErrors, STRICT_COMMAS);
13
+ const tree = parseTree(text, [], LENIENT_COMMAS);
14
+
15
+ return {
16
+ value,
17
+ ok: parseErrors.length === 0,
18
+ syntaxErrors: describeSyntaxErrors(text, parseErrors, positionOf),
19
+ locate(pointer) {
20
+ const node = tree && findNodeAtLocation(tree, pointer);
21
+ return node ? positionOf(node.offset) : {};
22
+ },
23
+ };
24
+ }
25
+
26
+ function withoutBom(text) {
27
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
28
+ }
@@ -0,0 +1,56 @@
1
+ import { createScanner, printParseErrorCode, SyntaxKind } from 'jsonc-parser';
2
+
3
+ const MESSAGES = {
4
+ InvalidSymbol: 'Unexpected character.',
5
+ InvalidNumberFormat: 'Invalid number.',
6
+ PropertyNameExpected: 'Expected a property name.',
7
+ ValueExpected: 'Expected a value.',
8
+ ColonExpected: 'Expected ":".',
9
+ CommaExpected: 'Expected ",".',
10
+ CloseBraceExpected: 'Expected "}".',
11
+ CloseBracketExpected: 'Expected "]".',
12
+ EndOfFileExpected: 'Unexpected content after the end of the JSON.',
13
+ UnexpectedEndOfComment: 'Unclosed comment.',
14
+ UnexpectedEndOfString: 'Unclosed string.',
15
+ UnexpectedEndOfNumber: 'Incomplete number.',
16
+ InvalidUnicode: 'Invalid unicode escape.',
17
+ InvalidEscapeCharacter: 'Invalid escape character.',
18
+ InvalidCharacter: 'Invalid character in string.',
19
+ };
20
+
21
+ const CLOSING_BRACKETS = '}]';
22
+ const ERRORS_AFTER_TRAILING_COMMA = new Set(['PropertyNameExpected', 'ValueExpected']);
23
+
24
+ export function describeSyntaxErrors(text, parseErrors, positionOf) {
25
+ const described = [];
26
+ const reportedOffsets = new Set();
27
+ for (const parseError of parseErrors) {
28
+ const { message, offset } = describe(text, parseError);
29
+ if (reportedOffsets.has(offset)) continue;
30
+ reportedOffsets.add(offset);
31
+ described.push({ message, ...positionOf(offset) });
32
+ }
33
+ return described;
34
+ }
35
+
36
+ function describe(text, { error, offset }) {
37
+ const code = printParseErrorCode(error);
38
+ const trailingComma = findTrailingComma(text, code, offset);
39
+ if (trailingComma !== null) return { message: 'Trailing comma.', offset: trailingComma };
40
+ return { message: MESSAGES[code] ?? code, offset };
41
+ }
42
+
43
+ function findTrailingComma(text, code, offset) {
44
+ if (!CLOSING_BRACKETS.includes(text[offset]) || !ERRORS_AFTER_TRAILING_COMMA.has(code)) return null;
45
+ const previous = tokenBefore(text, offset);
46
+ return previous?.kind === SyntaxKind.CommaToken ? previous.offset : null;
47
+ }
48
+
49
+ function tokenBefore(text, offset) {
50
+ const scanner = createScanner(text, true);
51
+ let previous = null;
52
+ for (let kind = scanner.scan(); kind !== SyntaxKind.EOF && scanner.getTokenOffset() < offset; kind = scanner.scan()) {
53
+ previous = { kind, offset: scanner.getTokenOffset() };
54
+ }
55
+ return previous;
56
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { SchemaEngine } from '../schema-engine.mjs';
5
+ import { loadSchemas } from './load-schemas.mjs';
6
+ import { cobblemon, distRoot } from './package-info.mjs';
7
+
8
+ const BUNDLE_FILE = path.join(distRoot, 'bundle.json');
9
+ const VALIDATORS_FILE = path.join(distRoot, 'validators.mjs');
10
+
11
+ export async function loadEngine({ live = false } = {}) {
12
+ if (!live && fs.existsSync(BUNDLE_FILE)) return loadBundledEngine();
13
+ return compileEngine();
14
+ }
15
+
16
+ async function loadBundledEngine() {
17
+ const bundle = JSON.parse(fs.readFileSync(BUNDLE_FILE, 'utf8'));
18
+ const compiled = await import(pathToFileURL(VALIDATORS_FILE).href);
19
+ return SchemaEngine.fromBundle(bundle, compiled);
20
+ }
21
+
22
+ function compileEngine() {
23
+ const { ajv, entries } = loadSchemas();
24
+ const validators = {};
25
+ for (const entry of entries) {
26
+ const validator = entry.id && ajv.getSchema(entry.id);
27
+ if (validator) validators[entry.path] = validator;
28
+ }
29
+ return new SchemaEngine({ entries, validators, cobblemon });
30
+ }