xml-model 2.0.0-beta.3 → 2.0.0-beta.5

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/index.js CHANGED
@@ -2,6 +2,6 @@ import { DATA, isModel, model } from "./model.js";
2
2
  import XML, { ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode } from "./xml/xml-js.js";
3
3
  import { xml } from "./xml/schema-meta.js";
4
4
  import { normalizeCodecOptions, registerDefault } from "./xml/codec.js";
5
- import { xmlModel } from "./xml/model.js";
5
+ import { XMLBase, XMLBaseWithSource, xmlModel } from "./xml/model.js";
6
6
  import "./xml/index.js";
7
- export { DATA, XML, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel };
7
+ export { DATA, XML, XMLBase, XMLBaseWithSource, ZXMLCommentNode, ZXMLElementNode, ZXMLNode, ZXMLRoot, ZXMLTextNode, isModel, model, normalizeCodecOptions, registerDefault, xml, xmlModel };
@@ -49,6 +49,7 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
49
49
  clearBuffers(parser);
50
50
  parser.q = parser.c = "";
51
51
  parser.bufferCheckPosition = sax.MAX_BUFFER_LENGTH;
52
+ parser.encoding = null;
52
53
  parser.opt = opt || {};
53
54
  parser.opt.lowercase = parser.opt.lowercase || parser.opt.lowercasetags;
54
55
  parser.looseCase = parser.opt.lowercase ? "toLowerCase" : "toUpperCase";
@@ -146,6 +147,19 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
146
147
  function createStream(strict, opt) {
147
148
  return new SAXStream(strict, opt);
148
149
  }
150
+ function determineBufferEncoding(data, isEnd) {
151
+ if (data.length >= 2) {
152
+ if (data[0] === 255 && data[1] === 254) return "utf-16le";
153
+ if (data[0] === 254 && data[1] === 255) return "utf-16be";
154
+ }
155
+ if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) return "utf8";
156
+ if (data.length >= 4) {
157
+ if (data[0] === 60 && data[1] === 0 && data[2] === 63 && data[3] === 0) return "utf-16le";
158
+ if (data[0] === 0 && data[1] === 60 && data[2] === 0 && data[3] === 63) return "utf-16be";
159
+ return "utf8";
160
+ }
161
+ return isEnd ? "utf8" : null;
162
+ }
149
163
  function SAXStream(strict, opt) {
150
164
  if (!(this instanceof SAXStream)) return new SAXStream(strict, opt);
151
165
  Stream.apply(this);
@@ -161,6 +175,7 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
161
175
  me._parser.error = null;
162
176
  };
163
177
  this._decoder = null;
178
+ this._decoderBuffer = null;
164
179
  streamWraps.forEach(function(ev) {
165
180
  Object.defineProperty(me, "on" + ev, {
166
181
  get: function() {
@@ -180,10 +195,30 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
180
195
  });
181
196
  }
182
197
  SAXStream.prototype = Object.create(Stream.prototype, { constructor: { value: SAXStream } });
198
+ SAXStream.prototype._decodeBuffer = function(data, isEnd) {
199
+ if (this._decoderBuffer) {
200
+ data = Buffer.concat([this._decoderBuffer, data]);
201
+ this._decoderBuffer = null;
202
+ }
203
+ if (!this._decoder) {
204
+ var encoding = determineBufferEncoding(data, isEnd);
205
+ if (!encoding) {
206
+ this._decoderBuffer = data;
207
+ return "";
208
+ }
209
+ this._parser.encoding = encoding;
210
+ this._decoder = new TextDecoder(encoding);
211
+ }
212
+ return this._decoder.decode(data, { stream: !isEnd });
213
+ };
183
214
  SAXStream.prototype.write = function(data) {
184
- if (typeof Buffer === "function" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(data)) {
185
- if (!this._decoder) this._decoder = new TextDecoder("utf8");
186
- data = this._decoder.decode(data, { stream: true });
215
+ if (typeof Buffer === "function" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(data)) data = this._decodeBuffer(data, false);
216
+ else if (this._decoderBuffer) {
217
+ var remaining = this._decodeBuffer(Buffer.alloc(0), true);
218
+ if (remaining) {
219
+ this._parser.write(remaining);
220
+ this.emit("data", remaining);
221
+ }
187
222
  }
188
223
  this._parser.write(data.toString());
189
224
  this.emit("data", data);
@@ -191,7 +226,13 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
191
226
  };
192
227
  SAXStream.prototype.end = function(chunk) {
193
228
  if (chunk && chunk.length) this.write(chunk);
194
- if (this._decoder) {
229
+ if (this._decoderBuffer) {
230
+ var finalChunk = this._decodeBuffer(Buffer.alloc(0), true);
231
+ if (finalChunk) {
232
+ this._parser.write(finalChunk);
233
+ this.emit("data", finalChunk);
234
+ }
235
+ } else if (this._decoder) {
195
236
  var remaining = this._decoder.decode();
196
237
  if (remaining) {
197
238
  this._parser.write(remaining);
@@ -548,6 +589,26 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
548
589
  function emit(parser, event, data) {
549
590
  parser[event] && parser[event](data);
550
591
  }
592
+ function getDeclaredEncoding(body) {
593
+ var match = body && body.match(/(?:^|\s)encoding\s*=\s*(['"])([^'"]+)\1/i);
594
+ return match ? match[2] : null;
595
+ }
596
+ function normalizeEncodingName(encoding) {
597
+ if (!encoding) return null;
598
+ return encoding.toLowerCase().replace(/[^a-z0-9]/g, "");
599
+ }
600
+ function encodingsMatch(detectedEncoding, declaredEncoding) {
601
+ const detected = normalizeEncodingName(detectedEncoding);
602
+ const declared = normalizeEncodingName(declaredEncoding);
603
+ if (!detected || !declared) return true;
604
+ if (declared === "utf16") return detected === "utf16le" || detected === "utf16be";
605
+ return detected === declared;
606
+ }
607
+ function validateXmlDeclarationEncoding(parser, data) {
608
+ if (!parser.strict || !parser.encoding || !data || data.name !== "xml") return;
609
+ var declaredEncoding = getDeclaredEncoding(data.body);
610
+ if (declaredEncoding && !encodingsMatch(parser.encoding, declaredEncoding)) strictFail(parser, "XML declaration encoding " + declaredEncoding + " does not match detected stream encoding " + parser.encoding.toUpperCase());
611
+ }
551
612
  function emitNode(parser, nodeType, data) {
552
613
  if (parser.textNode) closeText(parser);
553
614
  emit(parser, nodeType, data);
@@ -1014,10 +1075,12 @@ var require_sax = /* @__PURE__ */ __commonJSMin(((exports) => {
1014
1075
  continue;
1015
1076
  case S.PROC_INST_ENDING:
1016
1077
  if (c === ">") {
1017
- emitNode(parser, "onprocessinginstruction", {
1078
+ const procInstEndData = {
1018
1079
  name: parser.procInstName,
1019
1080
  body: parser.procInstBody
1020
- });
1081
+ };
1082
+ validateXmlDeclarationEncoding(parser, procInstEndData);
1083
+ emitNode(parser, "onprocessinginstruction", procInstEndData);
1021
1084
  parser.procInstName = parser.procInstBody = "";
1022
1085
  parser.state = S.TEXT;
1023
1086
  } else {
package/dist/util/zod.js CHANGED
@@ -9,6 +9,8 @@ function getParentSchema(schema) {
9
9
  if (isZodType(schema.def.in)) parent = schema.def.in;
10
10
  } else if (schema instanceof z.ZodOptional) {
11
11
  if (isZodType(schema.def.innerType)) parent = schema.def.innerType;
12
+ } else if (schema instanceof z.ZodDefault) {
13
+ if (isZodType(schema.def.innerType)) parent = schema.def.innerType;
12
14
  } else if (schema instanceof z.ZodLazy) {
13
15
  const value = schema.def.getter();
14
16
  if (isZodType(value)) parent = value;
@@ -32,8 +32,8 @@ export interface CodecOptions<S extends z.ZodType> {
32
32
  */
33
33
  export type UserCodecOptions<S extends z.ZodType = z.ZodType> = {
34
34
  tagname?: string | CodecOptions<S>["tagname"];
35
- decode?: CodecOptions<S>["decode"];
36
- encode?: CodecOptions<S>["encode"];
35
+ decode?: (ctx: RootDecodingContext<S>, next: () => z.input<S>) => z.input<S>;
36
+ encode?: (ctx: RootEncodingContext<S>, next: () => XMLElement) => XMLElement;
37
37
  propertyTagname?: string | CodecOptions<S>["propertyTagname"];
38
38
  inlineProperty?: boolean;
39
39
  propertyMatch?: RegExp | CodecOptions<S>["propertyMatch"];
@@ -71,13 +71,40 @@ export declare function normalizeCodecOptions<S extends z.ZodType>(schema: S, op
71
71
  type OrderEntry = string | XMLElement;
72
72
  export interface XMLState {
73
73
  /** Preserves element ordering and unknown elements across a decode → encode round-trip. */
74
- fieldOrder: OrderEntry[];
74
+ sequence: OrderEntry[];
75
+ /** Present when xmlStateSchema({ source: true }) is used: the original XMLElement. */
76
+ source?: XMLElement;
75
77
  }
76
78
  /**
77
- * Non-enumerable Symbol attached to decoded data objects (and forwarded to model instances).
78
- * Groups all XML codec round-trip state under a single key.
79
+ * String key used to store XML round-trip state on decoded data objects.
80
+ * Using a string (rather than a Symbol) allows Zod's schema.parse() to
81
+ * preserve it naturally when the key is included in the schema via xmlStateSchema().
79
82
  */
80
- export declare const XML_STATE: unique symbol;
83
+ export declare const XML_STATE_KEY: "__xml_state";
84
+ /**
85
+ * Schema for the XML round-trip state field.
86
+ *
87
+ * Include in your base model schema under `XML_STATE_KEY` to preserve element ordering
88
+ * and unknown elements through Zod's `schema.parse()` for nested model instances.
89
+ *
90
+ * Pass `{ source: true }` to also record the original `XMLElement` on each instance.
91
+ *
92
+ * @example
93
+ * class XMLBase extends xmlModel(z.object({
94
+ * [XML_STATE_KEY]: xmlStateSchema(),
95
+ * }), { tagname: "base" }) {}
96
+ *
97
+ * // With source recording:
98
+ * class XMLBase extends xmlModel(z.object({
99
+ * [XML_STATE_KEY]: xmlStateSchema({ source: true }),
100
+ * }), { tagname: "base" }) {}
101
+ */
102
+ export declare function xmlStateSchema(): z.ZodOptional<z.ZodCustom<XMLState>>;
103
+ export declare function xmlStateSchema(options: {
104
+ source: true;
105
+ }): z.ZodOptional<z.ZodCustom<XMLState & {
106
+ source: XMLElement;
107
+ }>>;
81
108
  export declare function decode<S extends z.ZodType>(schema: S, xml: XMLElement): z.input<S>;
82
109
  export declare function encode<S extends z.ZodType>(schema: S, data: z.output<S>): XMLElement;
83
110
  type DefaultResolver<S extends z.ZodType = z.ZodType> = (schema: S) => CodecOptions<S> | void;
package/dist/xml/codec.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import XML from "./xml-js.js";
2
2
  import { isZodType } from "../util/zod.js";
3
- import { getUserOptions } from "./schema-meta.js";
3
+ import { getUserOptions, prop } from "./schema-meta.js";
4
4
  import { kebabCase } from "../util/kebab-case.js";
5
5
  import { z } from "zod";
6
6
  //#region src/xml/codec.ts
@@ -28,8 +28,8 @@ function normalizeCodecOptions(schema, options = {}) {
28
28
  const result = {
29
29
  schema,
30
30
  tagname,
31
- decode: options.decode ?? defaultOptions().decode,
32
- encode: options.encode ?? defaultOptions().encode,
31
+ decode: options.decode ? (ctx) => options.decode(ctx, () => defaultOptions().decode(ctx)) : (ctx) => defaultOptions().decode(ctx),
32
+ encode: options.encode ? (ctx) => options.encode(ctx, () => defaultOptions().encode(ctx)) : (ctx) => defaultOptions().encode(ctx),
33
33
  propertyTagname,
34
34
  inlineProperty,
35
35
  propertyMatch,
@@ -51,7 +51,10 @@ function normalizeCodecOptions(schema, options = {}) {
51
51
  data: property.value
52
52
  });
53
53
  if (XML.isEmpty(res)) return;
54
- if (property.options.inlineProperty) ctx.result.elements.push(...res.elements);
54
+ if (property.options.inlineProperty) ctx.result.elements.push(...res.elements.map((el) => el.type === "element" ? {
55
+ ...el,
56
+ name: property.tagname
57
+ } : el));
55
58
  else ctx.result.elements.push(res);
56
59
  }
57
60
  };
@@ -66,10 +69,19 @@ function resolveCodecOptions(schema) {
66
69
  return options;
67
70
  }
68
71
  /**
69
- * Non-enumerable Symbol attached to decoded data objects (and forwarded to model instances).
70
- * Groups all XML codec round-trip state under a single key.
72
+ * String key used to store XML round-trip state on decoded data objects.
73
+ * Using a string (rather than a Symbol) allows Zod's schema.parse() to
74
+ * preserve it naturally when the key is included in the schema via xmlStateSchema().
71
75
  */
72
- var XML_STATE = Symbol("xml-model.state");
76
+ var XML_STATE_KEY = "__xml_state";
77
+ function xmlStateSchema(options) {
78
+ return prop(z.custom().optional(), {
79
+ decode: options?.source ? (ctx, _next) => {
80
+ (ctx.result[XML_STATE_KEY] ??= {}).source = ctx.xml;
81
+ } : () => {},
82
+ encode: () => {}
83
+ });
84
+ }
73
85
  function resolvePropertiesCodecOptions(schema) {
74
86
  const shape = schema.def.shape;
75
87
  const options = {};
@@ -103,7 +115,10 @@ function resolveDefault(schema) {
103
115
  }
104
116
  registerDefault((schema) => {
105
117
  if (schema instanceof z.ZodArray) {
106
- const elOptions = resolveCodecOptions(schema.def.element);
118
+ const elSchema = schema.def.element;
119
+ if (!isZodType(elSchema)) throw new Error(`Expected a ZodType, got ${elSchema}`);
120
+ const elOptions = resolveCodecOptions(elSchema);
121
+ const elHasOwnTagname = Boolean(getUserOptions(elSchema).tagname);
107
122
  return normalizeCodecOptions(schema, {
108
123
  decode(ctx) {
109
124
  const { xml } = ctx;
@@ -116,12 +131,16 @@ registerDefault((schema) => {
116
131
  encode(ctx) {
117
132
  const values = ctx.data;
118
133
  if (!Array.isArray(values)) throw new Error("expected array");
134
+ const elOptsForEncode = elHasOwnTagname ? elOptions : {
135
+ ...elOptions,
136
+ tagname: ctx.options.tagname
137
+ };
119
138
  return {
120
139
  type: "element",
121
140
  name: ctx.options.tagname(ctx),
122
141
  attributes: {},
123
- elements: values.map((v) => elOptions.encode({
124
- options: elOptions,
142
+ elements: values.map((v) => elOptsForEncode.encode({
143
+ options: elOptsForEncode,
125
144
  data: v
126
145
  }))
127
146
  };
@@ -134,12 +153,18 @@ registerDefault((schema) => {
134
153
  const innerOptions = resolveCodecOptions(inner);
135
154
  return normalizeCodecOptions(schema, {
136
155
  decode(ctx) {
137
- if (!ctx.xml) return void 0;
156
+ if (ctx.xml === null) return void 0;
138
157
  else return innerOptions.decode(ctx);
139
158
  },
140
159
  encode(ctx) {
141
160
  if (typeof ctx.data === "undefined") return {};
142
161
  else return innerOptions.encode(ctx);
162
+ },
163
+ decodeAsProperty(ctx) {
164
+ if (ctx.property.xml !== null) innerOptions.decodeAsProperty(ctx);
165
+ },
166
+ encodeAsProperty(ctx) {
167
+ if (typeof ctx.property.value !== "undefined") innerOptions.encodeAsProperty(ctx);
143
168
  }
144
169
  });
145
170
  }
@@ -238,6 +263,7 @@ registerDefault((schema) => {
238
263
  return normalizeCodecOptions(schema, {
239
264
  decode(ctx) {
240
265
  const sequence = [];
266
+ const result = { [XML_STATE_KEY]: { sequence } };
241
267
  const propContexts = Object.fromEntries(Object.entries(options).map(([name, propOpts]) => {
242
268
  return [name, {
243
269
  name,
@@ -274,7 +300,6 @@ registerDefault((schema) => {
274
300
  } else throw new Error(`Same element was matched by multiple properties: ${matches.join(", ")}`);
275
301
  }
276
302
  for (const propName in options) if (!seenProperties.has(propName)) sequence.push(propName);
277
- const result = {};
278
303
  for (const prop in options) {
279
304
  const o = options[prop];
280
305
  const propCtx = propContexts[prop];
@@ -293,12 +318,6 @@ registerDefault((schema) => {
293
318
  result
294
319
  });
295
320
  }
296
- Object.defineProperty(result, XML_STATE, {
297
- value: { fieldOrder: sequence },
298
- enumerable: false,
299
- writable: true,
300
- configurable: true
301
- });
302
321
  return result;
303
322
  },
304
323
  encode(ctx) {
@@ -309,7 +328,7 @@ registerDefault((schema) => {
309
328
  attributes: {},
310
329
  elements: []
311
330
  };
312
- const sequence = data[XML_STATE]?.fieldOrder ?? Object.keys(options);
331
+ const sequence = data["__xml_state"]?.sequence ?? Object.keys(options);
313
332
  for (const item of sequence) if (typeof item === "string") {
314
333
  const o = options[item];
315
334
  if (!o) throw new Error(`Failed to resolve property options for sequence item ${item}`);
@@ -334,6 +353,6 @@ registerDefault((schema) => {
334
353
  }
335
354
  });
336
355
  //#endregion
337
- export { XML_STATE, decode, encode, normalizeCodecOptions, registerDefault };
356
+ export { XML_STATE_KEY, decode, encode, normalizeCodecOptions, registerDefault, xmlStateSchema };
338
357
 
339
358
  //# sourceMappingURL=codec.js.map
@@ -1,6 +1,6 @@
1
1
  export * from './xml-js';
2
2
  export { xml } from './schema-meta';
3
- export type { UserCodecOptions } from './codec';
3
+ export type { UserCodecOptions, XMLState } from './codec';
4
4
  export { registerDefault, normalizeCodecOptions } from './codec';
5
- export { xmlModel, type XmlModelConstructor } from './model';
5
+ export { xmlModel, XMLBase, XMLBaseWithSource, type XmlModelConstructor } from './model';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -14,5 +14,37 @@ export type XmlModelConstructor<S extends z.ZodObject<any> = z.ZodObject<any>, I
14
14
  /** Converts an instance to an XML string. */
15
15
  toXMLString(instance: z.infer<S>, options?: StringifyOptions): string;
16
16
  };
17
+ /**
18
+ * Base class for xmlModel classes. Preserves element ordering and unknown elements
19
+ * through `schema.parse()` for nested model instances.
20
+ *
21
+ * @example
22
+ * class Device extends XMLBase.extend(
23
+ * { name: z.string() },
24
+ * xml.root({ tagname: "device" }),
25
+ * ) {}
26
+ */
27
+ export declare const XMLBase: XmlModelConstructor<z.ZodObject<{
28
+ __xml_state: z.ZodOptional<z.ZodCustom<import('./codec').XMLState, unknown>>;
29
+ }, z.core.$strip>, {
30
+ __xml_state?: import('./codec').XMLState;
31
+ }>;
32
+ /**
33
+ * Like {@link XMLBase}, but also records the original `XMLElement` as `.source`
34
+ * on each instance's XML state.
35
+ *
36
+ * @example
37
+ * const device = Device.fromXML(`<device>…</device>`);
38
+ * device[XML_STATE_KEY]?.source; // XMLElement
39
+ */
40
+ export declare const XMLBaseWithSource: XmlModelConstructor<z.ZodObject<{
41
+ __xml_state: z.ZodOptional<z.ZodCustom<import('./codec').XMLState & {
42
+ source: XMLElement;
43
+ }, unknown>>;
44
+ }, z.core.$strip>, {
45
+ __xml_state?: import('./codec').XMLState & {
46
+ source: XMLElement;
47
+ };
48
+ }>;
17
49
  export declare function xmlModel<S extends z.ZodObject<any>>(schema: S, options?: UserCodecOptions<S>): XmlModelConstructor<S>;
18
50
  //# sourceMappingURL=model.d.ts.map
package/dist/xml/model.js CHANGED
@@ -1,9 +1,29 @@
1
1
  import { model } from "../model.js";
2
2
  import XML from "./xml-js.js";
3
3
  import { root } from "./schema-meta.js";
4
- import { XML_STATE, decode, encode } from "./codec.js";
5
- import "zod";
4
+ import { XML_STATE_KEY, decode, encode, xmlStateSchema } from "./codec.js";
5
+ import { z } from "zod";
6
6
  //#region src/xml/model.ts
7
+ /**
8
+ * Base class for xmlModel classes. Preserves element ordering and unknown elements
9
+ * through `schema.parse()` for nested model instances.
10
+ *
11
+ * @example
12
+ * class Device extends XMLBase.extend(
13
+ * { name: z.string() },
14
+ * xml.root({ tagname: "device" }),
15
+ * ) {}
16
+ */
17
+ var XMLBase = xmlModel(z.object({ [XML_STATE_KEY]: xmlStateSchema() }));
18
+ /**
19
+ * Like {@link XMLBase}, but also records the original `XMLElement` as `.source`
20
+ * on each instance's XML state.
21
+ *
22
+ * @example
23
+ * const device = Device.fromXML(`<device>…</device>`);
24
+ * device[XML_STATE_KEY]?.source; // XMLElement
25
+ */
26
+ var XMLBaseWithSource = xmlModel(z.object({ [XML_STATE_KEY]: xmlStateSchema({ source: true }) }));
7
27
  function xmlModel(schema, options) {
8
28
  const _schema = options ? root(schema, options) : schema;
9
29
  return class extends model(_schema) {
@@ -12,9 +32,9 @@ function xmlModel(schema, options) {
12
32
  if (XML.isRoot(input)) input = XML.elementFromRoot(input);
13
33
  const schema = this.dataSchema;
14
34
  const inputData = decode(this.dataSchema, input);
15
- const xmlState = inputData[XML_STATE];
35
+ const xmlState = inputData[XML_STATE_KEY];
16
36
  const parsed = schema.parse(inputData);
17
- parsed[XML_STATE] = xmlState;
37
+ parsed[XML_STATE_KEY] = xmlState;
18
38
  return this.fromData(parsed);
19
39
  }
20
40
  static toXML(instance) {
@@ -28,6 +48,6 @@ function xmlModel(schema, options) {
28
48
  };
29
49
  }
30
50
  //#endregion
31
- export { xmlModel };
51
+ export { XMLBase, XMLBaseWithSource, xmlModel };
32
52
 
33
53
  //# sourceMappingURL=model.js.map
@@ -15,8 +15,8 @@ type UserPropOptions = {
15
15
  tagname?: string | UserCodecOptions["propertyTagname"];
16
16
  inline?: boolean;
17
17
  match?: RegExp | ((el: XMLElement) => boolean);
18
- decode?: (ctx: PropertyDecodingContext) => Partial<Record<string, unknown>> | undefined;
19
- encode?: (ctx: PropertyEncodingContext) => XMLElement | undefined;
18
+ decode?: (ctx: PropertyDecodingContext, next: () => void) => void;
19
+ encode?: (ctx: PropertyEncodingContext, next: () => void) => void;
20
20
  };
21
21
  export declare function root<S extends z.ZodType>(schema: S, options: UserRootOptions<S>): S;
22
22
  export declare function root(options: UserRootOptions): z.GlobalMeta;
@@ -1,3 +1,4 @@
1
+ import XML from "./xml-js.js";
1
2
  import { getParentSchema, isZodType } from "../util/zod.js";
2
3
  import "zod";
3
4
  //#region src/xml/schema-meta.ts
@@ -19,21 +20,40 @@ function normalizePropOptions(options) {
19
20
  if (options.decode !== void 0) {
20
21
  const userDecode = options.decode;
21
22
  partial.decodeAsProperty = function(ctx) {
22
- const res = userDecode(ctx);
23
- if (typeof res !== "undefined") Object.assign(ctx.result, res);
23
+ const next = () => {
24
+ const val = ctx.property.options.decode({
25
+ options: ctx.property.options,
26
+ xml: ctx.property.xml
27
+ });
28
+ ctx.result[ctx.property.name] = val;
29
+ };
30
+ userDecode(ctx, next);
24
31
  };
25
32
  }
26
33
  if (options.encode !== void 0) {
27
34
  const userEncode = options.encode;
28
35
  partial.encodeAsProperty = function(ctx) {
29
36
  const { property } = ctx;
30
- const res = userEncode(ctx);
31
- if (typeof res === "undefined") return;
32
- if (property.options.inlineProperty) ctx.result.elements.push(...res.elements);
33
- else {
34
- res.name = property.tagname;
35
- ctx.result.elements.push(res);
36
- }
37
+ const next = () => {
38
+ const optsWithTagname = {
39
+ ...property.options,
40
+ tagname: () => property.tagname
41
+ };
42
+ const res = property.options.encode({
43
+ options: optsWithTagname,
44
+ data: property.value
45
+ });
46
+ if (XML.isEmpty(res)) return;
47
+ if (property.options.inlineProperty) ctx.result.elements.push(...res.elements.map((el) => el.type === "element" ? {
48
+ ...el,
49
+ name: property.tagname
50
+ } : el));
51
+ else ctx.result.elements.push({
52
+ ...res,
53
+ name: property.tagname
54
+ });
55
+ };
56
+ userEncode(ctx, next);
37
57
  };
38
58
  }
39
59
  return partial;
@@ -52,7 +72,7 @@ function attr(optionsOrSchema, options) {
52
72
  decodeAsProperty(ctx) {
53
73
  const { name, options: propOptions } = ctx.property;
54
74
  const attrName = opts.name ?? name;
55
- const attrValue = ctx.xml?.attributes[attrName];
75
+ const attrValue = ctx.xml?.attributes?.[attrName];
56
76
  ctx.result[name] = propOptions.schema.parse(attrValue);
57
77
  },
58
78
  encodeAsProperty(ctx) {
@@ -73,16 +93,27 @@ var xml = {
73
93
  function getOwnUserOptions(schema) {
74
94
  return schema.meta()?.[metaKey] ?? {};
75
95
  }
96
+ var INHERITABLE_KEYS = [
97
+ "tagname",
98
+ "propertyTagname",
99
+ "inlineProperty",
100
+ "propertyMatch",
101
+ "decodeAsProperty",
102
+ "encodeAsProperty"
103
+ ];
76
104
  function getUserOptions(schema) {
77
105
  const own = getOwnUserOptions(schema);
78
106
  const parentSchema = getParentSchema(schema);
79
107
  if (!parentSchema) return own;
108
+ const parentOptions = getUserOptions(parentSchema);
109
+ const inherited = {};
110
+ for (const key of INHERITABLE_KEYS) if (parentOptions[key] !== void 0 && own[key] === void 0) inherited[key] = parentOptions[key];
80
111
  return {
81
- ...getUserOptions(parentSchema),
112
+ ...inherited,
82
113
  ...own
83
114
  };
84
115
  }
85
116
  //#endregion
86
- export { getUserOptions, root, xml };
117
+ export { getUserOptions, prop, root, xml };
87
118
 
88
119
  //# sourceMappingURL=schema-meta.js.map
@@ -120,21 +120,6 @@ export type ParseOptions = IgnoreOptions & ChangingKeyNames & {
120
120
  * @default false
121
121
  */
122
122
  addParent?: boolean;
123
- /**
124
- * Whether to always wrap sub-elements in an array, even when there is only one.
125
- * Pass an array of element names to restrict this behaviour to those names only.
126
- * Only applicable in compact mode.
127
- * @default false
128
- */
129
- alwaysArray?: boolean | Array<string>;
130
- /**
131
- * Whether to always emit an `elements` array even on empty elements.
132
- * `<a></a>` becomes `{ elements: [{ type: "element", name: "a", elements: [] }] }`
133
- * instead of `{ elements: [{ type: "element", name: "a" }] }`.
134
- * Only applicable in non-compact mode.
135
- * @default false
136
- */
137
- alwaysChildren?: boolean;
138
123
  /**
139
124
  * Whether to parse the contents of processing instructions as attributes.
140
125
  * `<?go to="there"?>` becomes `{ go: { attributes: { to: "there" } } }`
@@ -221,14 +206,8 @@ declare function isEmpty(xml: object): xml is XMLVoid;
221
206
  */
222
207
  declare function getContent(xml: XMLElement): string;
223
208
  /**
224
- * Creates a minimal element structure wrapping the given text content.
225
- * When no tag name is provided, returns a fragment with a text child.
226
- * When a tag name is provided, returns a full element with the given name and optional attributes.
227
- *
209
+ * Creates a text-only fragment wrapping the given content.
228
210
  * @param content - The text content to wrap (defaults to empty string).
229
- * @param tag - Optional element tag name.
230
- * @param attributes - Optional attributes; only valid when `tag` is provided.
231
- * @throws {TypeError} When `attributes` are provided without a `tag`.
232
211
  */
233
212
  declare function fromContent(content: string): {
234
213
  elements: [{
@@ -236,6 +215,12 @@ declare function fromContent(content: string): {
236
215
  text: string;
237
216
  }] | [];
238
217
  };
218
+ /**
219
+ * Creates a full element wrapping the given text content.
220
+ * @param content - The text content to wrap (defaults to empty string).
221
+ * @param tag - Element tag name.
222
+ * @param attributes - Optional attributes.
223
+ */
239
224
  declare function fromContent(content: string, tag: string, attributes?: XMLElement["attributes"]): {
240
225
  type: "element";
241
226
  name: string;
@@ -25,14 +25,22 @@ var ZXMLNode = z.discriminatedUnion("type", [
25
25
  ]);
26
26
  var ZXMLRoot = z.object({ elements: z.array(ZXMLNode) });
27
27
  function parse(xml, options = {}) {
28
- const strippedOptions = { ...options };
29
- delete strippedOptions["compact"];
30
- const res = (0, import_lib.xml2js)(xml, strippedOptions);
28
+ if ("compact" in options) throw new Error("xml-model always uses non-compact mode (compact is forced to false)");
29
+ if ("alwaysChildren" in options) throw new Error("xml-model always parses with alwaysChildren: true");
30
+ const res = (0, import_lib.xml2js)(xml, {
31
+ ...options,
32
+ compact: false,
33
+ alwaysChildren: true
34
+ });
31
35
  if ("elements" in res) return res;
32
36
  throw new Error("Got empty XML");
33
37
  }
34
38
  function stringify(xml, options = {}) {
35
- return (0, import_lib.js2xml)(xml, options);
39
+ if ("compact" in options) throw new Error("xml-model always uses non-compact mode (compact is forced to false)");
40
+ return (0, import_lib.js2xml)(xml, {
41
+ ...options,
42
+ compact: false
43
+ });
36
44
  }
37
45
  function isRoot(xml) {
38
46
  return Object.keys(xml).length === 1 && "elements" in xml && Array.isArray(xml.elements);
@@ -51,11 +59,11 @@ function isEmpty(xml) {
51
59
  * @throws {TypeError} When the element has multiple or non-text children.
52
60
  */
53
61
  function getContent(xml) {
54
- if (xml.elements?.length === 1) {
62
+ if (!xml.elements.length) return "";
63
+ if (xml.elements.length === 1) {
55
64
  const content = xml.elements[0];
56
65
  if (content.type === "text") return content.text;
57
66
  }
58
- if (!xml.elements) return "";
59
67
  throw new TypeError(`can't get text from XMLElement: ${JSON.stringify(xml)}`);
60
68
  }
61
69
  function fromContent(content = "", tag, attributes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xml-model",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.5",
4
4
  "description": "allows transparent XML <-> Object conversion in typescript",
5
5
  "license": "MIT",
6
6
  "author": "MathisTLD",
@@ -34,10 +34,10 @@
34
34
  "fmt:check": "vp fmt --check",
35
35
  "prepare": "vp config"
36
36
  },
37
- "dependencies": {},
38
37
  "devDependencies": {
39
38
  "@types/node": "^24.10.11",
40
39
  "marmotte": "^0.4.4",
40
+ "oxc-minify": "^0.121.0",
41
41
  "typescript": "^5.9.3",
42
42
  "vite-plus": "latest",
43
43
  "vitepress": "^2.0.0-alpha.16",