@oh-my-pi/omptype 17.2.7 → 17.2.8

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.
package/dist/js/errors.js CHANGED
@@ -26,37 +26,65 @@ export class OmpError {
26
26
  this.#rawExpected = expected;
27
27
  this.#config = config;
28
28
  }
29
+ /** Prefix this failure when a nested schema delegates validation. */
30
+ prefix(key) {
31
+ this.path.unshift(key);
32
+ return this;
33
+ }
34
+ /** Apply schema-local formatting to this failure. */
35
+ configure(config) {
36
+ this.#config = { ...this.#config, ...config };
37
+ return this;
38
+ }
29
39
  /** Stable category for programmatic error handling. */
30
40
  get code() {
31
41
  return errorCode(this.#rawExpected, this.data);
32
42
  }
33
43
  #context(expected, actual, problem = "") {
34
- return { code: this.code, path: this.path, data: this.data, expected, actual, problem };
44
+ const { description, rule } = describeExpectation(this.#rawExpected);
45
+ return {
46
+ code: this.code,
47
+ path: this.path,
48
+ propString: formatPath(this.path),
49
+ data: this.data,
50
+ expected,
51
+ actual,
52
+ problem,
53
+ description,
54
+ ...(rule === undefined ? {} : { rule }),
55
+ };
35
56
  }
36
57
  /** Human-readable expectation, including a configured override. */
37
58
  get expected() {
38
- const actual = describeValue(this.data);
39
- return format(this.#config?.expected, this.#context(this.#rawExpected, actual), this.#rawExpected);
59
+ const actual = describeValue(this.data, this.#config?.preserveActual ? "predicate" : this.code);
60
+ const parts = this.#rawExpected.split(" or ");
61
+ const fallback = parts.length < 3 ? this.#rawExpected : `${parts.slice(0, -1).join(", ")} or ${parts[parts.length - 1]}`;
62
+ return format(this.#config?.expected, this.#context(fallback, actual), fallback);
40
63
  }
41
64
  /** Short description of the received value, e.g. `"a number"` or `"missing"`. */
42
65
  get actual() {
43
- const actual = describeValue(this.data);
44
- return format(this.#config?.actual, this.#context(this.expected, actual), actual);
66
+ const actual = describeValue(this.data, this.#config?.preserveActual ? "predicate" : this.code);
67
+ const override = this.#config?.actual;
68
+ return typeof override === "function" ? override(this.data) : (override ?? actual);
45
69
  }
46
70
  /** Path-less problem statement: `must be <expected> (was <actual>)`. */
47
71
  get problem() {
48
72
  const expected = this.expected;
49
73
  const actual = this.actual;
50
- const fallback = this.data === MISSING ? `must be ${expected} (was missing)` : `must be ${expected} (was ${actual})`;
74
+ const fallback = this.data === MISSING
75
+ ? `must be ${expected} (was missing)`
76
+ : actual === ""
77
+ ? `must be ${expected}`
78
+ : `must be ${expected} (was ${actual})`;
51
79
  return format(this.#config?.problem, this.#context(expected, actual, fallback), fallback);
52
80
  }
53
- /** Full message including the path prefix. */
54
81
  get message() {
55
82
  const expected = this.expected;
56
83
  const actual = this.actual;
57
84
  const problem = this.problem;
58
- const at = this.path.length === 0 ? "" : `${this.path.map(String).join(".")} `;
59
- return format(this.#config?.message, this.#context(expected, actual, problem), `${at}${problem}`);
85
+ const at = this.path.length === 0 ? "" : `${formatPath(this.path)} `;
86
+ const messageActual = this.#config?.actual === undefined ? describeKind(this.data) : actual;
87
+ return format(this.#config?.message, this.#context(expected, messageActual, problem), `${at}${problem}`);
60
88
  }
61
89
  toString() {
62
90
  return this.message;
@@ -64,7 +92,26 @@ export class OmpError {
64
92
  }
65
93
  /** Sentinel for a required key that was absent (distinguishes from `undefined`). */
66
94
  export const MISSING = Symbol("omptype.missing");
67
- function describeValue(data) {
95
+ function stringifyValue(data) {
96
+ const seen = new WeakSet();
97
+ return (JSON.stringify(data, (_key, value) => {
98
+ if (typeof value !== "object" || value === null)
99
+ return value;
100
+ if (seen.has(value))
101
+ return "(cycle)";
102
+ seen.add(value);
103
+ return value;
104
+ }) ?? "an object");
105
+ }
106
+ function describeValue(data, code) {
107
+ if (data === MISSING)
108
+ return "missing";
109
+ if (code === "undeclared")
110
+ return "";
111
+ if (code === "referenceSame")
112
+ return "";
113
+ if (code === "domain")
114
+ return describeKind(data);
68
115
  if (data === null)
69
116
  return "null";
70
117
  if (Array.isArray(data))
@@ -81,47 +128,150 @@ function describeValue(data) {
81
128
  case "undefined":
82
129
  return "undefined";
83
130
  case "object":
84
- return "an object";
131
+ return stringifyValue(data);
85
132
  case "function":
86
133
  return "a function";
87
134
  default:
88
135
  return "a symbol";
89
136
  }
90
137
  }
138
+ function describeKind(data) {
139
+ if (data === MISSING)
140
+ return "missing";
141
+ if (typeof data === "number" && Number.isNaN(data))
142
+ return "NaN";
143
+ if (data === null)
144
+ return "null";
145
+ if (Array.isArray(data))
146
+ return "an object";
147
+ switch (typeof data) {
148
+ case "string":
149
+ return "a string";
150
+ case "number":
151
+ return "a number";
152
+ case "bigint":
153
+ return "a bigint";
154
+ case "boolean":
155
+ return "boolean";
156
+ case "undefined":
157
+ return "undefined";
158
+ case "object": {
159
+ const ctor = Object.getPrototypeOf(data)?.constructor;
160
+ return typeof ctor?.name === "string" && ctor.name !== "Object" ? ctor.name : "an object";
161
+ }
162
+ case "function":
163
+ return "a function";
164
+ default:
165
+ return "a symbol";
166
+ }
167
+ }
168
+ function describeExpectation(expected) {
169
+ const divisor = /divisible by (\d+(?:\.\d+)?)/.exec(expected);
170
+ if (divisor) {
171
+ const rule = Number(divisor[1]);
172
+ return { description: rule === 2 ? "even" : `divisible by ${rule}`, rule };
173
+ }
174
+ return { description: expected.replace(/^(?:an?|the) /, "") };
175
+ }
176
+ function formatPath(path) {
177
+ let out = "";
178
+ for (const key of path) {
179
+ if (typeof key === "number")
180
+ out += `[${key}]`;
181
+ else if (typeof key === "symbol")
182
+ out += `[${String(key)}]`;
183
+ else
184
+ out += out.length === 0 ? key : `.${key}`;
185
+ }
186
+ return out;
187
+ }
91
188
  function errorCode(expected, data) {
92
189
  if (data === MISSING)
93
190
  return "required";
94
- if (expected.includes("divisible by"))
191
+ if (expected === "removed")
192
+ return "undeclared";
193
+ if (expected.includes("serialized to the same value"))
194
+ return "referenceSame";
195
+ if (expected.includes("divisible by") || expected.includes("integer"))
95
196
  return "divisor";
96
- if (expected.includes("at least") || expected.includes("more than"))
197
+ if (expected === "true" ||
198
+ expected === "false" ||
199
+ expected === "null" ||
200
+ expected === "undefined" ||
201
+ expected === "NaN" ||
202
+ expected === "Infinity" ||
203
+ expected === "-Infinity" ||
204
+ /^-?\d+(?:\.\d+)?n?$/.test(expected) ||
205
+ (expected.includes(" or ") &&
206
+ expected.split(" or ").every(part => /^".*"$|^-?\d+(?:\.\d+)?n?$|^(?:true|false|null|undefined)$/.test(part)))) {
207
+ return "unit";
208
+ }
209
+ if (expected.includes("at least") ||
210
+ expected.includes("more than") ||
211
+ expected.includes("timestamp after") ||
212
+ expected === "non-negative" ||
213
+ expected === "positive") {
97
214
  return "min";
98
- if (expected.includes("at most") || expected.includes("less than"))
215
+ }
216
+ if (expected.includes("at most") ||
217
+ expected.includes("less than") ||
218
+ expected.includes("timestamp before") ||
219
+ expected === "non-positive" ||
220
+ expected === "negative") {
99
221
  return "max";
100
- if (expected.includes("matching") || expected.includes("format") || expected.includes("email"))
222
+ }
223
+ if (expected.includes("matching") ||
224
+ expected.includes("format") ||
225
+ expected.includes("email") ||
226
+ expected.includes("parsable") ||
227
+ expected.includes("only digits")) {
101
228
  return "pattern";
102
- if (expected.includes("predicate") || expected.includes("satisfying"))
229
+ }
230
+ if (expected.includes("predicate") || expected.includes("satisfying") || expected.includes("according to"))
103
231
  return "predicate";
104
232
  if (expected.startsWith('"') || expected.startsWith("the date "))
105
233
  return "unit";
106
- return "domain";
234
+ if (expected === "an Error" || expected === "a Date" || expected.startsWith("an instance of "))
235
+ return "domain";
236
+ if ((expected.includes(" or ") && !expected.includes("IPv")) ||
237
+ expected.endsWith(" instance") ||
238
+ expected.startsWith("a number representing") ||
239
+ [
240
+ "a string",
241
+ "a number",
242
+ "a bigint",
243
+ "a symbol",
244
+ "boolean",
245
+ "an object",
246
+ "an array",
247
+ "a tuple",
248
+ "undefined",
249
+ "null",
250
+ "unknown",
251
+ "never",
252
+ ].includes(expected))
253
+ return "domain";
254
+ return "predicate";
107
255
  }
108
256
  export class OmpErrors {
109
257
  #path;
110
258
  #expected;
111
259
  #data;
112
260
  #entry;
261
+ #entries;
113
262
  #config;
114
- /** Number of failures; omptype validators fast-fail on the first error. */
115
- length = 1;
263
+ #separator = "\n";
116
264
  constructor(path, expected, data, config) {
117
265
  this.#path = path;
118
266
  this.#expected = expected;
119
267
  this.#data = data;
120
268
  this.#config = config;
121
269
  }
122
- /** First and only validation failure, materialized on demand. */
270
+ get length() {
271
+ return this.#entries?.length ?? 1;
272
+ }
123
273
  get 0() {
124
- return this.#getEntry();
274
+ return this.#entries?.[0] ?? this.#getEntry();
125
275
  }
126
276
  static single(path, expected, data, config) {
127
277
  return new OmpErrors(path, expected, data, config);
@@ -134,45 +284,90 @@ export class OmpErrors {
134
284
  this.#entry = entry;
135
285
  return entry;
136
286
  }
137
- /** Prefix the failure path with `key` when nesting sub-schemas. */
287
+ /** Append all failures from `other`, preserving traversal order. */
288
+ append(other) {
289
+ this.#entries ??= [this.#getEntry()];
290
+ const entries = this.#entries;
291
+ for (const entry of other)
292
+ entries.push(entry);
293
+ return this;
294
+ }
295
+ /** Prefix every failure path with `key` when nesting sub-schemas. */
138
296
  prefix(key) {
139
- const path = this.#path;
140
- this.#path = path === undefined ? [key] : Array.isArray(path) ? [key, ...path] : [key, path];
141
- this.#entry = undefined;
297
+ if (this.#entries) {
298
+ for (const entry of this.#entries)
299
+ entry.prefix(key);
300
+ }
301
+ else {
302
+ const path = this.#path;
303
+ this.#path = path === undefined ? [key] : Array.isArray(path) ? [key, ...path] : [key, path];
304
+ this.#entry = undefined;
305
+ }
142
306
  return this;
143
307
  }
144
- /** Apply schema-local message formatting without rebuilding the failure. */
308
+ /** Apply schema-local message formatting without rebuilding failures. */
145
309
  configure(config) {
146
- this.#config = { ...this.#config, ...config };
147
- this.#entry = undefined;
310
+ if (this.#entries) {
311
+ for (const entry of this.#entries)
312
+ entry.configure(config);
313
+ }
314
+ else {
315
+ this.#config = { ...this.#config, ...config };
316
+ this.#entry = undefined;
317
+ }
148
318
  return this;
149
319
  }
150
- /** Index the failure by its dotted property path (`""` for the root). */
151
320
  get byPath() {
152
- const entry = this.#getEntry();
153
- return { [entry.path.map(String).join(".")]: entry };
321
+ const result = {};
322
+ for (const entry of this)
323
+ result[entry.path.map(String).join(".")] = entry;
324
+ return result;
154
325
  }
155
- /** Transform the failure entry into a plain array. */
156
326
  map(fn) {
157
- return [fn(this.#getEntry(), 0, this)];
327
+ const result = [];
328
+ let index = 0;
329
+ for (const entry of this)
330
+ result.push(fn(entry, index++, this));
331
+ return result;
158
332
  }
159
- /** Select the failure entry into a plain array. */
160
333
  filter(fn) {
161
- const entry = this.#getEntry();
162
- return fn(entry, 0, this) ? [entry] : [];
334
+ const result = [];
335
+ let index = 0;
336
+ for (const entry of this) {
337
+ if (fn(entry, index++, this))
338
+ result.push(entry);
339
+ }
340
+ return result;
163
341
  }
164
- /** Iterate over the single failure entry. */
165
342
  *[Symbol.iterator]() {
166
- yield this.#getEntry();
343
+ if (this.#entries) {
344
+ yield* this.#entries;
345
+ }
346
+ else {
347
+ yield this.#getEntry();
348
+ }
349
+ }
350
+ /** @internal Render multiple branch failures as alternatives rather than independent failures. */
351
+ asAlternatives() {
352
+ this.#separator = " or ";
353
+ return this;
167
354
  }
168
- /** Human-readable failure text, materialized only when requested. */
169
355
  get summary() {
170
- return this.#getEntry().message;
356
+ const entries = [...this];
357
+ if (this.#separator === "\n" &&
358
+ entries.length > 1 &&
359
+ entries.every(entry => Object.is(entry.data, entries[0].data) &&
360
+ entry.path.length === entries[0].path.length &&
361
+ entry.path.every((key, index) => key === entries[0].path[index]))) {
362
+ const at = formatPath(entries[0].path);
363
+ const actual = typeof entries[0].data === "string" ? JSON.stringify(entries[0].data) : String(entries[0].data);
364
+ return `${at}${at === "" ? "" : " "}(${actual}) must be...\n${entries.map(entry => ` ◦ ${entry.expected}`).join("\n")}`;
365
+ }
366
+ return entries.map(error => error.message).join(this.#separator);
171
367
  }
172
368
  toString() {
173
369
  return this.summary;
174
370
  }
175
- /** Throw a `TraversalError` carrying this result. */
176
371
  throw() {
177
372
  throw new TraversalError(this);
178
373
  }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * JSON Schema → omptype importer.
3
+ *
4
+ * Rebuilds a callable schema from JSON Schema documents — the inverse of
5
+ * `Type.toJsonSchema()` for the dialect omptype (and its Zod/TypeBox
6
+ * adapters) emits: draft-07 and draft-2020-12 structural keywords, string
7
+ * formats, `$defs`/`definitions` references (including recursion), enums,
8
+ * and composition via `anyOf`/`oneOf`/`allOf`.
9
+ *
10
+ * Unknown or non-structural keywords are ignored, matching lenient importer
11
+ * behavior: the result validates every constraint the schema can express in
12
+ * omptype IR and passes the rest through.
13
+ */
14
+ import { OmpTypeError } from "./errors.js";
15
+ import { IR_BRAND } from "./ir.js";
16
+ import { keywordIR, patternIR } from "./keywords.js";
17
+ import { type } from "./type.js";
18
+ /** `format` values lowered to built-in keyword validators. */
19
+ const FORMAT_KEYWORDS = {
20
+ email: "string.email",
21
+ uuid: "string.uuid",
22
+ "date-time": "string.date.iso",
23
+ date: "string.date.iso",
24
+ ipv4: "string.ip.v4",
25
+ ipv6: "string.ip.v6",
26
+ regex: "string.regex",
27
+ };
28
+ class Importer {
29
+ #root;
30
+ #aliases = new Map();
31
+ constructor(root) {
32
+ this.#root = root;
33
+ }
34
+ resolveRef(ref) {
35
+ const cached = this.#aliases.get(ref);
36
+ if (cached !== undefined)
37
+ return cached;
38
+ let target;
39
+ if (ref === "#") {
40
+ target = this.#root;
41
+ }
42
+ else {
43
+ const defsMatch = /^#\/(\$defs|definitions)\/(.+)$/.exec(ref);
44
+ if (defsMatch === null)
45
+ throw new OmpTypeError(`unsupported $ref: ${ref}`);
46
+ const defs = this.#root[defsMatch[1]];
47
+ target = typeof defs === "object" && defs !== null ? defs[defsMatch[2]] : undefined;
48
+ if (target === undefined)
49
+ throw new OmpTypeError(`unresolved $ref: ${ref}`);
50
+ }
51
+ // Register the alias before lowering so recursive references resolve
52
+ // to the same node instead of recursing forever.
53
+ let lowered;
54
+ const alias = {
55
+ k: "alias",
56
+ name: ref,
57
+ resolve: () => {
58
+ lowered ??= this.lower(target);
59
+ return lowered;
60
+ },
61
+ };
62
+ this.#aliases.set(ref, alias);
63
+ return alias;
64
+ }
65
+ lower(schema) {
66
+ if (schema === true)
67
+ return { k: "unknown" };
68
+ if (schema === false)
69
+ return { k: "never" };
70
+ if (typeof schema !== "object" || schema === null) {
71
+ throw new OmpTypeError("JSON Schema nodes must be booleans or objects");
72
+ }
73
+ const node = schema;
74
+ const desc = typeof node.description === "string" ? node.description : undefined;
75
+ const ir = this.#lowerNode(node);
76
+ return desc === undefined ? ir : { ...ir, desc };
77
+ }
78
+ #lowerNode(node) {
79
+ if (typeof node.$ref === "string")
80
+ return this.resolveRef(node.$ref);
81
+ if (Array.isArray(node.enum)) {
82
+ const members = node.enum.map((v) => ({ k: "lit", v }));
83
+ return members.length === 1 ? members[0] : { k: "union", members };
84
+ }
85
+ if ("const" in node)
86
+ return { k: "lit", v: node.const };
87
+ const branches = node.anyOf ?? node.oneOf;
88
+ if (Array.isArray(branches)) {
89
+ return { k: "union", members: branches.map(branch => this.lower(branch)) };
90
+ }
91
+ if (Array.isArray(node.allOf)) {
92
+ const members = node.allOf.map(branch => this.lower(branch));
93
+ return members.length === 1 ? members[0] : { k: "intersection", members };
94
+ }
95
+ if (typeof node.not === "object" && node.not !== null && Object.keys(node.not).length === 0) {
96
+ return { k: "never" };
97
+ }
98
+ if (Array.isArray(node.type)) {
99
+ const members = node.type.map(t => this.#lowerTyped(node, String(t)));
100
+ return members.length === 1 ? members[0] : { k: "union", members };
101
+ }
102
+ if (typeof node.type === "string")
103
+ return this.#lowerTyped(node, node.type);
104
+ // No explicit type: infer from structural keywords, else accept anything.
105
+ if (node.properties !== undefined || node.required !== undefined)
106
+ return this.#lowerObject(node);
107
+ if (node.items !== undefined || node.prefixItems !== undefined)
108
+ return this.#lowerArray(node);
109
+ return { k: "unknown" };
110
+ }
111
+ #lowerTyped(node, kind) {
112
+ switch (kind) {
113
+ case "null":
114
+ return { k: "null" };
115
+ case "boolean":
116
+ return { k: "boolean" };
117
+ case "string":
118
+ return this.#lowerString(node);
119
+ case "number":
120
+ case "integer": {
121
+ const ir = { k: "number" };
122
+ if (kind === "integer")
123
+ ir.int = true;
124
+ if (typeof node.minimum === "number")
125
+ ir.min = node.minimum;
126
+ if (typeof node.maximum === "number")
127
+ ir.max = node.maximum;
128
+ if (typeof node.exclusiveMinimum === "number") {
129
+ ir.min = node.exclusiveMinimum;
130
+ ir.xmin = true;
131
+ }
132
+ if (typeof node.exclusiveMaximum === "number") {
133
+ ir.max = node.exclusiveMaximum;
134
+ ir.xmax = true;
135
+ }
136
+ if (typeof node.multipleOf === "number")
137
+ ir.divisor = node.multipleOf;
138
+ return ir;
139
+ }
140
+ case "object":
141
+ return this.#lowerObject(node);
142
+ case "array":
143
+ return this.#lowerArray(node);
144
+ default:
145
+ throw new OmpTypeError(`unsupported JSON Schema type: ${kind}`);
146
+ }
147
+ }
148
+ #lowerString(node) {
149
+ const base = { k: "string" };
150
+ if (typeof node.minLength === "number")
151
+ base.min = node.minLength;
152
+ if (typeof node.maxLength === "number")
153
+ base.max = node.maxLength;
154
+ if (node.format === "uri" || node.format === "url")
155
+ base.url = true;
156
+ const members = [];
157
+ if (typeof node.format === "string") {
158
+ const keyword = FORMAT_KEYWORDS[node.format];
159
+ if (keyword !== undefined) {
160
+ const formatIR = keywordIR(keyword);
161
+ if (formatIR !== undefined)
162
+ members.push(formatIR);
163
+ }
164
+ }
165
+ if (typeof node.pattern === "string")
166
+ members.push(patternIR(new RegExp(node.pattern)));
167
+ if (members.length === 0)
168
+ return base;
169
+ if (base.min !== undefined || base.max !== undefined || base.url)
170
+ members.unshift(base);
171
+ return members.length === 1 ? members[0] : { k: "intersection", members };
172
+ }
173
+ #lowerObject(node) {
174
+ const required = new Set(Array.isArray(node.required) ? node.required.map(String) : []);
175
+ const props = [];
176
+ if (typeof node.properties === "object" && node.properties !== null) {
177
+ for (const [key, value] of Object.entries(node.properties)) {
178
+ const prop = { key, opt: !required.has(key), val: this.lower(value) };
179
+ if (typeof value === "object" && value !== null && "default" in value) {
180
+ prop.hasDefault = true;
181
+ prop.def = value.default;
182
+ prop.opt = true;
183
+ }
184
+ props.push(prop);
185
+ }
186
+ }
187
+ const extra = node.additionalProperties;
188
+ return {
189
+ k: "object",
190
+ props,
191
+ extras: extra === false ? "reject" : "keep",
192
+ ...(typeof extra === "object" && extra !== null ? { index: this.lower(extra) } : {}),
193
+ };
194
+ }
195
+ #lowerArray(node) {
196
+ if (Array.isArray(node.prefixItems)) {
197
+ const minItems = typeof node.minItems === "number" ? node.minItems : node.prefixItems.length;
198
+ const prefix = node.prefixItems.map((item, index) => ({
199
+ val: this.lower(item),
200
+ opt: index >= minItems,
201
+ }));
202
+ return {
203
+ k: "tuple",
204
+ prefix,
205
+ postfix: [],
206
+ ...(node.items !== undefined && node.items !== false ? { variadic: this.lower(node.items) } : {}),
207
+ };
208
+ }
209
+ const ir = { k: "array", el: node.items === undefined ? { k: "unknown" } : this.lower(node.items) };
210
+ if (typeof node.minItems === "number")
211
+ ir.min = node.minItems;
212
+ if (typeof node.maxItems === "number")
213
+ ir.max = node.maxItems;
214
+ return ir;
215
+ }
216
+ }
217
+ /**
218
+ * Build a callable omptype schema from a JSON Schema document.
219
+ *
220
+ * # Errors
221
+ * Throws {@link OmpTypeError} on malformed nodes, unresolvable `$ref`s, or
222
+ * types omptype cannot represent.
223
+ */
224
+ export function fromJsonSchema(schema) {
225
+ const importer = new Importer(typeof schema === "object" && schema !== null ? schema : {});
226
+ const embedded = {
227
+ [IR_BRAND]: true,
228
+ ir: importer.lower(schema),
229
+ hasSteps: false,
230
+ hasDefault: false,
231
+ run: value => value,
232
+ };
233
+ return type.raw(embedded);
234
+ }
package/dist/js/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * JSON Schema emission.
7
7
  */
8
8
  export * from "./errors.js";
9
+ export * from "./from-json-schema.js";
9
10
  export * from "./infer.js";
10
11
  export * from "./ir.js";
11
12
  export * from "./json-schema.js";