@script-it/json-views-core 0.1.0-beta.1

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/THIRD_PARTY_NOTICES.txt +4917 -0
  4. package/dist/annotation-capabilities.d.ts +17 -0
  5. package/dist/annotation-capabilities.js +21 -0
  6. package/dist/atomic-value.d.ts +3 -0
  7. package/dist/atomic-value.js +6 -0
  8. package/dist/date-type.d.ts +48 -0
  9. package/dist/date-type.js +244 -0
  10. package/dist/diagnostics.d.ts +8 -0
  11. package/dist/diagnostics.js +252 -0
  12. package/dist/document-session.d.ts +52 -0
  13. package/dist/document-session.js +164 -0
  14. package/dist/formats/conversion.d.ts +17 -0
  15. package/dist/formats/conversion.js +75 -0
  16. package/dist/formats/csv-format.d.ts +31 -0
  17. package/dist/formats/csv-format.js +269 -0
  18. package/dist/formats/document-format.d.ts +35 -0
  19. package/dist/formats/document-format.js +1 -0
  20. package/dist/formats/json-format.d.ts +12 -0
  21. package/dist/formats/json-format.js +109 -0
  22. package/dist/html-view-bindings.d.ts +4 -0
  23. package/dist/html-view-bindings.js +32 -0
  24. package/dist/html-view-limits.d.ts +1 -0
  25. package/dist/html-view-limits.js +1 -0
  26. package/dist/index.d.ts +19 -0
  27. package/dist/index.js +19 -0
  28. package/dist/inference-fields.d.ts +5 -0
  29. package/dist/inference-fields.js +23 -0
  30. package/dist/inference.d.ts +26 -0
  31. package/dist/inference.js +304 -0
  32. package/dist/json-limits.d.ts +8 -0
  33. package/dist/json-limits.js +25 -0
  34. package/dist/json-path.d.ts +14 -0
  35. package/dist/json-path.js +84 -0
  36. package/dist/json-source-patcher.d.ts +97 -0
  37. package/dist/json-source-patcher.js +778 -0
  38. package/dist/metadata.d.ts +137 -0
  39. package/dist/metadata.js +952 -0
  40. package/dist/safe-pattern.d.ts +5 -0
  41. package/dist/safe-pattern.js +20 -0
  42. package/dist/schema-validation.d.ts +3 -0
  43. package/dist/schema-validation.js +4 -0
  44. package/dist/source-default.d.ts +2 -0
  45. package/dist/source-default.js +39 -0
  46. package/dist/table-suitability.d.ts +5 -0
  47. package/dist/table-suitability.js +55 -0
  48. package/dist/type-registry.d.ts +57 -0
  49. package/dist/type-registry.js +238 -0
  50. package/dist/value-comparison.d.ts +18 -0
  51. package/dist/value-comparison.js +133 -0
  52. package/dist/view-model.d.ts +18 -0
  53. package/dist/view-model.js +134 -0
  54. package/package.json +52 -0
  55. package/schema/jsonviews.schema.json +102 -0
@@ -0,0 +1,5 @@
1
+ import { RE2JS } from 're2js';
2
+ /** RE2 syntax provides linear-time matching; unsupported constructs fail compilation.
3
+ * Pattern size and cache count also bound compilation input and retained programs.
4
+ */
5
+ export declare function compileSafePattern(source: string): RE2JS;
@@ -0,0 +1,20 @@
1
+ import { RE2JS } from 're2js';
2
+ const MAX_PATTERN_LENGTH = 4096;
3
+ const MAX_CACHED_PATTERNS = 128;
4
+ const patterns = new Map();
5
+ /** RE2 syntax provides linear-time matching; unsupported constructs fail compilation.
6
+ * Pattern size and cache count also bound compilation input and retained programs.
7
+ */
8
+ export function compileSafePattern(source) {
9
+ if (typeof source !== 'string' || source.length > MAX_PATTERN_LENGTH) {
10
+ throw new TypeError(`pattern must be a string of at most ${MAX_PATTERN_LENGTH} characters`);
11
+ }
12
+ const existing = patterns.get(source);
13
+ if (existing)
14
+ return existing;
15
+ const compiled = RE2JS.compile(source);
16
+ if (patterns.size >= MAX_CACHED_PATTERNS)
17
+ patterns.delete(patterns.keys().next().value);
18
+ patterns.set(source, compiled);
19
+ return compiled;
20
+ }
@@ -0,0 +1,3 @@
1
+ import type { JsonViewSchemaDescriptor } from './metadata.js';
2
+ import { type JsonViewTypeRegistry } from './type-registry.js';
3
+ export declare function validateJsonViewSchemaValue(value: unknown, descriptor: JsonViewSchemaDescriptor, typeRegistry?: JsonViewTypeRegistry): string | undefined;
@@ -0,0 +1,4 @@
1
+ import { defaultTypeRegistry } from './type-registry.js';
2
+ export function validateJsonViewSchemaValue(value, descriptor, typeRegistry = defaultTypeRegistry) {
3
+ return typeRegistry.validate(value, descriptor);
4
+ }
@@ -0,0 +1,2 @@
1
+ /** Initial presentation only. Never changes a user's selection during edits. */
2
+ export declare function prefersSource(content: string, metadata?: unknown): boolean;
@@ -0,0 +1,39 @@
1
+ import { compileJsonViewMetadata } from './metadata.js';
2
+ import { isJsonViewTableCandidate } from './table-suitability.js';
3
+ /** Initial presentation only. Never changes a user's selection during edits. */
4
+ export function prefersSource(content, metadata) {
5
+ try {
6
+ const root = JSON.parse(content);
7
+ const compiled = compileJsonViewMetadata(root, undefined, metadata === undefined ? {} : { metadata });
8
+ if (compiled.metadataSource !== 'inferred' || compiled.views.length || isJsonViewTableCandidate(root))
9
+ return false;
10
+ const formatted = JSON.stringify(root, null, 2);
11
+ const lines = formatted.split('\n').length;
12
+ if (formatted.length > 8000 || lines > 100)
13
+ return false;
14
+ const entries = root !== null && typeof root === 'object' ? Object.values(root) : [];
15
+ // A compact root with little visible information is easier to read whole
16
+ // than by opening each nested value, even when nesting is only one level.
17
+ if (entries.length <= 5 && formatted.length <= 3000 && lines <= 40
18
+ && entries.some((value) => value !== null && typeof value === 'object' && Object.keys(value).length > 0
19
+ // Flat lists are simple field values, not structural nesting.
20
+ && (!Array.isArray(value) || value.some((item) => item !== null && typeof item === 'object'))))
21
+ return true;
22
+ let leaves = 0;
23
+ let hidden = 0;
24
+ const visit = (value, depth) => {
25
+ if (value !== null && typeof value === 'object')
26
+ Object.values(value).forEach((child) => visit(child, depth + 1));
27
+ else {
28
+ leaves++;
29
+ if (depth >= 3)
30
+ hidden++;
31
+ }
32
+ };
33
+ visit(root, 0);
34
+ return leaves > 0 && hidden / leaves >= 0.6;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
@@ -0,0 +1,5 @@
1
+ /** Conservative eligibility shared by predicted views and automatic JSON tables. */
2
+ /** Two or more records with fields can render as rows and columns. */
3
+ export declare function isJsonViewTableCandidate(value: unknown): boolean;
4
+ /** Shared scalar paths only; never explodes arrays or rewrites source records. */
5
+ export declare function inferFlatColumnPaths(value: unknown): string[][] | undefined;
@@ -0,0 +1,55 @@
1
+ /** Conservative eligibility shared by predicted views and automatic JSON tables. */
2
+ const MIN_RECORDS = 2;
3
+ const MAX_SAMPLE_ROWS = 10;
4
+ function isRecord(value) {
5
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
6
+ }
7
+ function isReadable(value) {
8
+ return typeof value === 'boolean'
9
+ || (typeof value === 'number' && Number.isFinite(value))
10
+ || (typeof value === 'string' && value.trim().length > 0);
11
+ }
12
+ /** Two or more records with fields can render as rows and columns. */
13
+ export function isJsonViewTableCandidate(value) {
14
+ const rows = Array.isArray(value) ? value : isRecord(value) ? Object.values(value) : [];
15
+ return rows.length >= MIN_RECORDS && rows.every(isRecord)
16
+ && rows.some((row) => Object.keys(row).some((key) => key !== '$jsonviews'));
17
+ }
18
+ /** Shared scalar paths only; never explodes arrays or rewrites source records. */
19
+ export function inferFlatColumnPaths(value) {
20
+ if (!Array.isArray(value) || value.length < MIN_RECORDS || !value.every(isRecord))
21
+ return undefined;
22
+ const sample = value.slice(0, MAX_SAMPLE_ROWS);
23
+ const shapes = sample.map((row) => {
24
+ const paths = [];
25
+ let invalid = false;
26
+ const visit = (record, prefix) => {
27
+ for (const [key, item] of Object.entries(record)) {
28
+ if (key === '$jsonviews')
29
+ continue;
30
+ const path = [...prefix, key];
31
+ if (isRecord(item) && prefix.length < 2)
32
+ visit(item, path);
33
+ else if (item === null || isReadable(item))
34
+ paths.push(path);
35
+ else
36
+ invalid = true;
37
+ if (paths.length > 12) {
38
+ invalid = true;
39
+ return;
40
+ }
41
+ }
42
+ };
43
+ visit(row, []);
44
+ return invalid ? undefined : paths;
45
+ });
46
+ if (shapes.some((shape) => !shape))
47
+ return undefined;
48
+ const paths = shapes[0];
49
+ if (paths.length < 2 || !paths.some((path) => path.length > 1))
50
+ return undefined;
51
+ const shared = paths.filter((path) => shapes.every((shape) => shape.some((other) => JSON.stringify(path) === JSON.stringify(other))));
52
+ if (shared.length / Math.max(...shapes.map((shape) => shape.length)) < 0.8)
53
+ return undefined;
54
+ return shared;
55
+ }
@@ -0,0 +1,57 @@
1
+ export declare const BUILT_IN_TYPE_NAMES: readonly ["text", "markdown", "html", "number", "checkbox", "select", "multi-select", "date", "url", "email", "body"];
2
+ export type BuiltInTypeName = (typeof BUILT_IN_TYPE_NAMES)[number];
3
+ /** Stable palette names understood by the built-in choice widgets. */
4
+ export declare const JSON_VIEW_OPTION_COLORS: readonly ["gray", "blue", "green", "yellow", "orange", "red", "purple", "pink"];
5
+ export type JsonViewOptionColor = (typeof JSON_VIEW_OPTION_COLORS)[number];
6
+ export interface JsonViewTypeDescriptorLike {
7
+ type: string;
8
+ required?: boolean;
9
+ minimum?: number | string | null;
10
+ maximum?: number | string | null;
11
+ pattern?: string;
12
+ options?: string[];
13
+ optionColors?: Record<string, JsonViewOptionColor>;
14
+ [key: string]: unknown;
15
+ }
16
+ export interface JsonViewTypeDefinition<Descriptor extends JsonViewTypeDescriptorLike = JsonViewTypeDescriptorLike> {
17
+ name: string;
18
+ validate: (value: unknown, descriptor: Descriptor) => string | undefined;
19
+ warnings?: (value: unknown, descriptor: Descriptor) => string[];
20
+ /** Checks type-specific annotation fields before the descriptor can be used. */
21
+ validateDescriptor?: (descriptor: Descriptor) => string | undefined;
22
+ /** Converts persisted JSON to the editor model. Omitted hooks are identity. */
23
+ parse?: (value: unknown, descriptor: Descriptor) => unknown;
24
+ /** Converts an editor model to JSON, before canonical value validation. */
25
+ serialize?: (value: unknown, descriptor: Descriptor) => unknown;
26
+ compare?: (left: unknown, right: unknown, direction: 'asc' | 'desc') => number;
27
+ matchesFilter?: (value: unknown, operator: string, expected: unknown) => boolean | undefined;
28
+ filterOperators?: readonly string[];
29
+ /** Supported annotation keys for diagnostic help. Omit for an open extension vocabulary. */
30
+ descriptorProperties?: readonly string[];
31
+ }
32
+ export declare class JsonViewTypeRegistry {
33
+ private readonly definitions;
34
+ private readonly listeners;
35
+ private revision;
36
+ get version(): number;
37
+ subscribe: (listener: () => void) => (() => void);
38
+ private changed;
39
+ constructor(definitions?: readonly JsonViewTypeDefinition[]);
40
+ register(definition: JsonViewTypeDefinition): this;
41
+ unregister(name: string): this;
42
+ get(name: string): JsonViewTypeDefinition | undefined;
43
+ has(name: string): boolean;
44
+ names(): string[];
45
+ clone(): JsonViewTypeRegistry;
46
+ validateDescriptor(descriptor: JsonViewTypeDescriptorLike): string | undefined;
47
+ parse(value: unknown, descriptor: JsonViewTypeDescriptorLike): unknown;
48
+ serialize(value: unknown, descriptor: JsonViewTypeDescriptorLike): unknown;
49
+ validate(value: unknown, descriptor: JsonViewTypeDescriptorLike): string | undefined;
50
+ warnings(value: unknown, descriptor: JsonViewTypeDescriptorLike): string[];
51
+ compare(left: unknown, right: unknown, descriptor: JsonViewTypeDescriptorLike, direction: 'asc' | 'desc'): number | undefined;
52
+ matchesFilter(value: unknown, operator: string, expected: unknown, descriptor: JsonViewTypeDescriptorLike): boolean | undefined;
53
+ filterOperators(descriptor: JsonViewTypeDescriptorLike): readonly string[] | undefined;
54
+ }
55
+ export declare const dateTypeDefinition: JsonViewTypeDefinition;
56
+ export declare function createDefaultTypeRegistry(): JsonViewTypeRegistry;
57
+ export declare const defaultTypeRegistry: JsonViewTypeRegistry;
@@ -0,0 +1,238 @@
1
+ import { DATE_FILTER_OPERATORS, compareDateValues, dateValueWarnings, matchesDateFilter, validateDateValue, } from './date-type.js';
2
+ import { compileSafePattern } from './safe-pattern.js';
3
+ import { BUILT_IN_DESCRIPTOR_PROPERTIES, COMMON_DESCRIPTOR_PROPERTIES } from './annotation-capabilities.js';
4
+ export const BUILT_IN_TYPE_NAMES = [
5
+ 'text',
6
+ 'markdown',
7
+ 'html',
8
+ 'number',
9
+ 'checkbox',
10
+ 'select',
11
+ 'multi-select',
12
+ 'date',
13
+ 'url',
14
+ 'email',
15
+ 'body',
16
+ ];
17
+ /** Stable palette names understood by the built-in choice widgets. */
18
+ export const JSON_VIEW_OPTION_COLORS = [
19
+ 'gray', 'blue', 'green', 'yellow', 'orange', 'red', 'purple', 'pink',
20
+ ];
21
+ function requireString(value) {
22
+ return typeof value === 'string' ? undefined : 'Value must be text';
23
+ }
24
+ export class JsonViewTypeRegistry {
25
+ definitions = new Map();
26
+ listeners = new Set();
27
+ revision = 0;
28
+ get version() { return this.revision; }
29
+ subscribe = (listener) => {
30
+ this.listeners.add(listener);
31
+ return () => { this.listeners.delete(listener); };
32
+ };
33
+ changed() {
34
+ this.revision += 1;
35
+ this.listeners.forEach((listener) => listener());
36
+ }
37
+ constructor(definitions = []) {
38
+ definitions.forEach((definition) => this.register(definition));
39
+ }
40
+ register(definition) {
41
+ if (typeof definition.name !== 'string' || !definition.name.trim())
42
+ throw new TypeError('A JSON Views type must have a name');
43
+ if (typeof definition.validate !== 'function')
44
+ throw new TypeError('A JSON Views type must provide validate');
45
+ if (definition.filterOperators?.some((operator) => typeof operator !== 'string' || !operator.trim())) {
46
+ throw new TypeError('Filter operators must be non-empty strings');
47
+ }
48
+ this.definitions.set(definition.name, Object.freeze({
49
+ ...definition,
50
+ ...(definition.filterOperators ? { filterOperators: Object.freeze([...definition.filterOperators]) } : {}),
51
+ ...(definition.descriptorProperties ? { descriptorProperties: Object.freeze([...definition.descriptorProperties]) } : {}),
52
+ }));
53
+ this.changed();
54
+ return this;
55
+ }
56
+ unregister(name) {
57
+ if (this.definitions.delete(name))
58
+ this.changed();
59
+ return this;
60
+ }
61
+ get(name) {
62
+ return this.definitions.get(name);
63
+ }
64
+ has(name) {
65
+ return this.definitions.has(name);
66
+ }
67
+ names() {
68
+ return [...this.definitions.keys()];
69
+ }
70
+ clone() {
71
+ return new JsonViewTypeRegistry([...this.definitions.values()]);
72
+ }
73
+ validateDescriptor(descriptor) {
74
+ try {
75
+ const issue = this.get(descriptor.type)?.validateDescriptor?.(descriptor);
76
+ return issue === undefined || typeof issue === 'string' ? issue : 'Descriptor validator must return a string or undefined';
77
+ }
78
+ catch (error) {
79
+ return `Descriptor validator failed: ${error instanceof Error ? error.message : String(error)}`;
80
+ }
81
+ }
82
+ parse(value, descriptor) {
83
+ const parse = this.get(descriptor.type)?.parse;
84
+ return parse ? parse(value, descriptor) : value;
85
+ }
86
+ serialize(value, descriptor) {
87
+ const serialize = this.get(descriptor.type)?.serialize;
88
+ return serialize ? serialize(value, descriptor) : value;
89
+ }
90
+ validate(value, descriptor) {
91
+ if (value === undefined || value === null || (value === '' && descriptor.type !== 'date')) {
92
+ return descriptor.required === true ? 'A value is required' : undefined;
93
+ }
94
+ const definition = this.get(descriptor.type);
95
+ if (!definition)
96
+ return `Unknown type: ${descriptor.type}`;
97
+ try {
98
+ const issue = definition.validate(value, descriptor);
99
+ if (issue !== undefined && typeof issue !== 'string')
100
+ return 'Validator must return a string or undefined';
101
+ if (issue)
102
+ return issue;
103
+ if (typeof value === 'string' && descriptor.pattern !== undefined) {
104
+ if (!compileSafePattern(descriptor.pattern).matcher(value).find())
105
+ return 'Value does not match the required pattern';
106
+ }
107
+ }
108
+ catch (error) {
109
+ return `Validation failed: ${error instanceof Error ? error.message : String(error)}`;
110
+ }
111
+ return undefined;
112
+ }
113
+ warnings(value, descriptor) {
114
+ try {
115
+ const warnings = this.get(descriptor.type)?.warnings?.(value, descriptor) ?? [];
116
+ return Array.isArray(warnings) && warnings.every((warning) => typeof warning === 'string')
117
+ ? warnings : ['Warning provider must return an array of strings'];
118
+ }
119
+ catch (error) {
120
+ return [`Warning provider failed: ${error instanceof Error ? error.message : String(error)}`];
121
+ }
122
+ }
123
+ compare(left, right, descriptor, direction) {
124
+ try {
125
+ const result = this.get(descriptor.type)?.compare?.(left, right, direction);
126
+ return typeof result === 'number' && Number.isFinite(result) ? result : undefined;
127
+ }
128
+ catch {
129
+ return undefined;
130
+ }
131
+ }
132
+ matchesFilter(value, operator, expected, descriptor) {
133
+ try {
134
+ const matches = this.get(descriptor.type)?.matchesFilter?.(value, operator, expected);
135
+ return matches === undefined || typeof matches === 'boolean' ? matches : false;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ filterOperators(descriptor) {
142
+ return this.get(descriptor.type)?.filterOperators;
143
+ }
144
+ }
145
+ const textType = { name: 'text', validate: requireString, serialize: (value) => value == null ? value : String(value) };
146
+ const markdownType = { ...textType, name: 'markdown' };
147
+ const htmlType = { ...textType, name: 'html' };
148
+ const bodyType = { ...textType, name: 'body' };
149
+ const numberType = {
150
+ name: 'number',
151
+ serialize: (value) => typeof value === 'string' ? value.trim() === '' ? null : Number(value) : value,
152
+ validate(value, descriptor) {
153
+ if (typeof value !== 'number' || !Number.isFinite(value))
154
+ return 'Value must be a finite number';
155
+ if (typeof descriptor.minimum === 'number' && value < descriptor.minimum)
156
+ return `Value must be at least ${descriptor.minimum}`;
157
+ if (typeof descriptor.maximum === 'number' && value > descriptor.maximum)
158
+ return `Value must be at most ${descriptor.maximum}`;
159
+ return undefined;
160
+ },
161
+ };
162
+ const checkboxType = {
163
+ name: 'checkbox',
164
+ serialize(value) {
165
+ if (value === true || value === 'true')
166
+ return true;
167
+ if (value === false || value === 'false')
168
+ return false;
169
+ return value;
170
+ },
171
+ validate: (value) => typeof value === 'boolean' ? undefined : 'Value must be true or false',
172
+ };
173
+ const selectType = {
174
+ name: 'select',
175
+ serialize: (value) => value === '' ? null : value,
176
+ validate(value) {
177
+ return typeof value === 'string' ? undefined : 'Value must be text';
178
+ },
179
+ };
180
+ const multiSelectType = {
181
+ name: 'multi-select',
182
+ serialize: (value) => typeof value === 'string' ? JSON.parse(value) : value,
183
+ validate(value) {
184
+ if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
185
+ return 'Value must be an array of text options';
186
+ }
187
+ return undefined;
188
+ },
189
+ };
190
+ export const dateTypeDefinition = {
191
+ name: 'date',
192
+ validate: (value, descriptor) => validateDateValue(value, descriptor),
193
+ warnings: dateValueWarnings,
194
+ compare: compareDateValues,
195
+ matchesFilter: matchesDateFilter,
196
+ filterOperators: DATE_FILTER_OPERATORS,
197
+ };
198
+ const urlType = {
199
+ name: 'url',
200
+ serialize: textType.serialize,
201
+ validate(value) {
202
+ if (typeof value !== 'string')
203
+ return 'Value must be a URL';
204
+ try {
205
+ const url = new URL(value);
206
+ return url.protocol === 'http:' || url.protocol === 'https:' ? undefined : 'Enter a valid web URL';
207
+ }
208
+ catch {
209
+ return 'Enter a valid web URL';
210
+ }
211
+ },
212
+ };
213
+ const emailType = {
214
+ name: 'email',
215
+ serialize: textType.serialize,
216
+ validate: (value) => typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
217
+ ? undefined
218
+ : 'Enter a valid email address',
219
+ };
220
+ export function createDefaultTypeRegistry() {
221
+ return new JsonViewTypeRegistry([
222
+ textType,
223
+ numberType,
224
+ checkboxType,
225
+ selectType,
226
+ multiSelectType,
227
+ dateTypeDefinition,
228
+ urlType,
229
+ emailType,
230
+ markdownType,
231
+ htmlType,
232
+ bodyType,
233
+ ].map((definition) => ({
234
+ ...definition,
235
+ descriptorProperties: [...COMMON_DESCRIPTOR_PROPERTIES, ...BUILT_IN_DESCRIPTOR_PROPERTIES[definition.name]],
236
+ })));
237
+ }
238
+ export const defaultTypeRegistry = createDefaultTypeRegistry();
@@ -0,0 +1,18 @@
1
+ import type { ValuePath } from './json-path.js';
2
+ /** Optional source tokens keep numeric comparisons independent of JS rounding. */
3
+ export type JsonNumberLookup = (path?: ValuePath | null) => string | undefined;
4
+ export interface JsonComparisonContext {
5
+ numberAtPath?: JsonNumberLookup;
6
+ leftPath?: ValuePath;
7
+ rightPath?: ValuePath;
8
+ }
9
+ /** Exact decimal ordering, including overflow/underflow and arbitrarily large exponents.
10
+ * Work is proportional to token length; exponents never cause zero-filled expansion.
11
+ */
12
+ export declare function compareJsonNumberTokens(left: string, right: string): number;
13
+ /** Total type ordering, then numeric/text/lexicographic container ordering. */
14
+ export declare function compareJsonValues(left: unknown, right: unknown, context?: JsonComparisonContext): number;
15
+ export declare function jsonValuesEqual(left: unknown, right: unknown, context?: JsonComparisonContext): boolean;
16
+ /** Text conversion that never invokes a JSON object's own toString/valueOf. */
17
+ export declare function jsonValueText(value: unknown): string;
18
+ export declare function matchesJsonValueFilter(value: unknown, operator: string, expected?: unknown, context?: JsonComparisonContext): boolean;
@@ -0,0 +1,133 @@
1
+ import { assertJsonDepth } from './json-limits.js';
2
+ const NUMBER = /^(-?)(0|[1-9][0-9]*)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/;
3
+ function decimal(token) {
4
+ const match = NUMBER.exec(token);
5
+ if (!match)
6
+ throw new TypeError('Expected a JSON number token');
7
+ const fraction = match[3] ?? '';
8
+ const digits = (match[2] + fraction).replace(/^0+/, '').replace(/0+$/, '');
9
+ const raw = (match[2] + fraction).replace(/^0+/, '');
10
+ return {
11
+ sign: digits ? (match[1] ? -1 : 1) : 0,
12
+ digits,
13
+ magnitude: BigInt(match[4] ?? '0') - BigInt(fraction.length) + BigInt(raw.length),
14
+ };
15
+ }
16
+ /** Exact decimal ordering, including overflow/underflow and arbitrarily large exponents.
17
+ * Work is proportional to token length; exponents never cause zero-filled expansion.
18
+ */
19
+ export function compareJsonNumberTokens(left, right) {
20
+ const a = decimal(left), b = decimal(right);
21
+ if (a.sign !== b.sign)
22
+ return a.sign < b.sign ? -1 : 1;
23
+ if (!a.sign)
24
+ return 0;
25
+ if (a.magnitude !== b.magnitude)
26
+ return (a.magnitude < b.magnitude ? -1 : 1) * a.sign;
27
+ for (let index = 0; index < Math.max(a.digits.length, b.digits.length); index++) {
28
+ const l = a.digits[index] ?? '0', r = b.digits[index] ?? '0';
29
+ if (l !== r)
30
+ return (l < r ? -1 : 1) * a.sign;
31
+ }
32
+ return 0;
33
+ }
34
+ function rank(value) {
35
+ if (value === null)
36
+ return 5;
37
+ if (typeof value === 'boolean')
38
+ return 0;
39
+ if (typeof value === 'number')
40
+ return 1;
41
+ if (typeof value === 'string')
42
+ return 2;
43
+ if (Array.isArray(value))
44
+ return 3;
45
+ if (typeof value === 'object')
46
+ return 4;
47
+ return 6; // Missing values are last.
48
+ }
49
+ const child = (path, key) => path && [...path, key];
50
+ function compare(left, right, context, strict) {
51
+ assertJsonDepth(left);
52
+ assertJsonDepth(right);
53
+ const pending = [{ left, right, context }];
54
+ while (pending.length) {
55
+ const { left: a, right: b, context: ctx } = pending.pop();
56
+ const ar = rank(a), br = rank(b);
57
+ if (ar !== br)
58
+ return ar < br ? -1 : 1;
59
+ if (typeof a === 'number' && typeof b === 'number') {
60
+ const at = ctx.numberAtPath?.(ctx.leftPath) ?? (Number.isFinite(a) ? String(a) : undefined);
61
+ const bt = ctx.numberAtPath?.(ctx.rightPath) ?? (Number.isFinite(b) ? String(b) : undefined);
62
+ const result = at !== undefined && bt !== undefined ? compareJsonNumberTokens(at, bt)
63
+ : Object.is(a, b) ? 0 : a < b ? -1 : 1;
64
+ if (result)
65
+ return result;
66
+ }
67
+ else if (typeof a === 'string' && typeof b === 'string') {
68
+ const result = strict ? (a === b ? 0 : a < b ? -1 : 1) : a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
69
+ if (result)
70
+ return result;
71
+ }
72
+ else if (typeof a === 'boolean' && typeof b === 'boolean') {
73
+ if (a !== b)
74
+ return a ? 1 : -1;
75
+ }
76
+ else if (a !== null && b !== null && typeof a === 'object' && typeof b === 'object') {
77
+ const ak = Array.isArray(a) ? a.map((_, index) => index) : Object.keys(a).sort();
78
+ const bk = Array.isArray(b) ? b.map((_, index) => index) : Object.keys(b).sort();
79
+ // Length is a final tie-break, after lexicographic key/value comparison.
80
+ pending.push({ left: ak.length, right: bk.length, context: {} });
81
+ for (let index = Math.min(ak.length, bk.length) - 1; index >= 0; index--) {
82
+ const key = ak[index], other = bk[index];
83
+ pending.push({ left: a[key], right: b[other],
84
+ context: { ...ctx, leftPath: child(ctx.leftPath, key), rightPath: child(ctx.rightPath, other) } });
85
+ // Keys are JSON data too; never call methods supplied by an object.
86
+ pending.push({ left: key, right: other, context: {} });
87
+ }
88
+ }
89
+ }
90
+ return 0;
91
+ }
92
+ /** Total type ordering, then numeric/text/lexicographic container ordering. */
93
+ export function compareJsonValues(left, right, context = {}) {
94
+ return compare(left, right, context, false);
95
+ }
96
+ export function jsonValuesEqual(left, right, context = {}) {
97
+ return compare(left, right, context, true) === 0;
98
+ }
99
+ /** Text conversion that never invokes a JSON object's own toString/valueOf. */
100
+ export function jsonValueText(value) {
101
+ return value !== null && typeof value === 'object' ? JSON.stringify(value) : String(value);
102
+ }
103
+ export function matchesJsonValueFilter(value, operator, expected, context = {}) {
104
+ const empty = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0);
105
+ if (operator === 'isEmpty')
106
+ return empty;
107
+ if (operator === 'isNotEmpty')
108
+ return !empty;
109
+ const equal = (a, b, ctx = context) => jsonValuesEqual(a, b, ctx);
110
+ if (operator === 'eq')
111
+ return equal(value, expected);
112
+ if (operator === 'neq')
113
+ return !equal(value, expected);
114
+ if (operator === 'in' || operator === 'notIn') {
115
+ const found = Array.isArray(expected) && expected.some((candidate, index) => equal(value, candidate, { ...context, rightPath: child(context.rightPath, index) }));
116
+ return operator === 'in' ? found : !found;
117
+ }
118
+ if (operator === 'contains' || operator === 'notContains') {
119
+ const found = typeof value === 'string' ? value.toLocaleLowerCase().includes(jsonValueText(expected ?? '').toLocaleLowerCase())
120
+ : Array.isArray(value) && value.some((candidate, index) => equal(candidate, expected, { ...context, leftPath: child(context.leftPath, index) }));
121
+ return operator === 'contains' ? found : !found;
122
+ }
123
+ const ordered = compareJsonValues(value, expected, context);
124
+ if (operator === 'gt')
125
+ return ordered > 0;
126
+ if (operator === 'gte')
127
+ return ordered >= 0;
128
+ if (operator === 'lt')
129
+ return ordered < 0;
130
+ if (operator === 'lte')
131
+ return ordered <= 0;
132
+ return false;
133
+ }
@@ -0,0 +1,18 @@
1
+ import { type JsonComparisonContext, type JsonNumberLookup } from './value-comparison.js';
2
+ import type { ValuePath } from './json-path.js';
3
+ import { type CompiledJsonViewFilterRule, type CompiledJsonViewSchema, type CompiledJsonViewView, type JsonViewPath, type JsonViewViewRow } from './metadata.js';
4
+ import { type JsonViewTypeRegistry } from './type-registry.js';
5
+ export interface JsonViewDisplayColumn {
6
+ id: string;
7
+ label: string;
8
+ path: JsonViewPath;
9
+ }
10
+ export interface JsonViewProjectedCollection {
11
+ rows: JsonViewViewRow[];
12
+ columns: JsonViewDisplayColumn[];
13
+ records: Record<string, unknown>[];
14
+ }
15
+ export declare function matchesJsonViewFilterRule(value: unknown, rule: CompiledJsonViewFilterRule, descriptor?: CompiledJsonViewSchema['descriptor'], typeRegistry?: JsonViewTypeRegistry, context?: JsonComparisonContext): boolean;
16
+ export declare function applyJsonViewViewRows(root: unknown, view: CompiledJsonViewView, schema?: readonly CompiledJsonViewSchema[], typeRegistry?: JsonViewTypeRegistry, numberAtPath?: JsonNumberLookup): JsonViewViewRow[];
17
+ export declare function projectJsonViewCollection(root: unknown, view: CompiledJsonViewView, schema?: readonly CompiledJsonViewSchema[], typeRegistry?: JsonViewTypeRegistry, numberAtPath?: JsonNumberLookup): JsonViewProjectedCollection;
18
+ export declare function schemaRelativePathForRow(schema: CompiledJsonViewSchema, rowPath: ValuePath): ValuePath | undefined;