xml-sax-ts 0.1.3 → 0.3.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.
package/README.md CHANGED
@@ -51,6 +51,62 @@ const root = parseXmlString("<root><a>1</a><b/></root>");
51
51
  console.log(root.name); // "root"
52
52
  ```
53
53
 
54
+ ### Project to plain objects
55
+
56
+ ```ts
57
+ import { buildObject, parseXmlString } from "xml-sax-ts";
58
+
59
+ const root = parseXmlString("<root id='1'><item>1</item><item>2</item></root>");
60
+ const obj = buildObject(root);
61
+ // { "@_id": "1", item: ["1", "2"] }
62
+ ```
63
+
64
+ ### Streaming object builder
65
+
66
+ ```ts
67
+ import { ObjectBuilder, XmlSaxParser } from "xml-sax-ts";
68
+
69
+ const builder = new ObjectBuilder();
70
+ const parser = new XmlSaxParser({
71
+ onOpenTag: builder.onOpenTag,
72
+ onText: builder.onText,
73
+ onCdata: builder.onCdata,
74
+ onCloseTag: builder.onCloseTag
75
+ });
76
+
77
+ parser.feed("<root><item>1</item>");
78
+ parser.feed("<item>2</item></root>");
79
+ parser.close();
80
+
81
+ const obj = builder.getResult();
82
+ // { item: ["1", "2"] }
83
+ ```
84
+
85
+ ### Object to XML
86
+
87
+ ```ts
88
+ import { objectToXml } from "xml-sax-ts";
89
+
90
+ const xml = objectToXml({
91
+ root: {
92
+ "@_id": "1",
93
+ item: ["1", "2"],
94
+ }
95
+ });
96
+
97
+ // <root id="1"><item>1</item><item>2</item></root>
98
+ ```
99
+
100
+ ```ts
101
+ import { buildObject, objectToXml, parseXmlString } from "xml-sax-ts";
102
+
103
+ const root = parseXmlString("<root id='1'><item>1</item></root>");
104
+ const obj = buildObject(root);
105
+ const xml = objectToXml(obj, { rootName: "root" });
106
+
107
+ // <root id="1"><item>1</item></root>
108
+ ```
109
+
54
110
  ### Serialize to XML
55
111
 
56
112
  ```ts
@@ -108,6 +164,42 @@ Convenience function that parses a complete XML string into an `XmlNode` tree us
108
164
 
109
165
  Low-level tree builder. Attach its `onOpenTag`, `onText`, `onCdata`, and `onCloseTag` methods to a parser, then call `getRoot()` to retrieve the resulting `XmlNode`.
110
166
 
167
+ ### `buildObject(root, options?)`
168
+
169
+ Projects an `XmlNode` tree into a plain object. Attributes are prefixed (default `@_`), text is stored under `#text`, repeated elements are arrays, and elements with only text return the text directly.
170
+
171
+ ### `ObjectBuilder`
172
+
173
+ Streaming builder that produces the same object shape as `buildObject` without building a full `XmlNode` tree. Attach its `onOpenTag`, `onText`, `onCdata`, and `onCloseTag` methods to the parser.
174
+
175
+ #### `ObjectBuilderOptions`
176
+
177
+ | Option | Type | Default | Description |
178
+ | ------------------ | ------------------------------------------------------------ | --------- | ---------------------------------------------- |
179
+ | `attributePrefix` | `string` | `"@_"` | Prefix for attribute keys |
180
+ | `textKey` | `string` | `"#text"` | Key used for text nodes |
181
+ | `stripNamespaces` | `boolean` | `false` | Strip namespace prefixes from names |
182
+ | `arrayElements` | `Set\<string\> \| (name: string, path: string[]) => boolean` | — | Force specific elements to always be arrays |
183
+ | `coalesceText` | `boolean` | `true` | Merge adjacent text nodes into a single string |
184
+
185
+ ### `buildXmlNode(obj, options?)`
186
+
187
+ Converts a plain object into an `XmlNode` tree using the same attribute/text conventions as `buildObject`.
188
+
189
+ ### `objectToXml(obj, options?)`
190
+
191
+ Builds an `XmlNode` with `buildXmlNode` and serializes it with `serializeXml`.
192
+
193
+ #### `XmlBuilderOptions`
194
+
195
+ | Option | Type | Default | Description |
196
+ | ------------------ | ------------------------------------------------------------ | --------- | ---------------------------------------------- |
197
+ | `attributePrefix` | `string` | `"@_"` | Prefix for attribute keys |
198
+ | `textKey` | `string` | `"#text"` | Key used for text nodes |
199
+ | `stripNamespaces` | `boolean` | `false` | Strip namespace prefixes from names |
200
+ | `arrayElements` | `Set\<string\> \| (name: string, path: string[]) => boolean` | — | Force specific elements to always be arrays |
201
+ | `rootName` | `string` | — | Root element name when object has multiple keys |
202
+
111
203
  ### `serializeXml(node, options?)`
112
204
 
113
205
  Serializes an `XmlNode` back to an XML string.
@@ -127,7 +219,7 @@ Custom error class thrown on parse errors. Includes `offset`, `line`, and `colum
127
219
 
128
220
  ### Exported types
129
221
 
130
- `OpenTag` · `CloseTag` · `XmlAttribute` · `ProcessingInstruction` · `Doctype` · `XmlNode` · `XmlChild` · `XmlPosition` · `ParserOptions` · `SerializeOptions`
222
+ `OpenTag` · `CloseTag` · `XmlAttribute` · `ProcessingInstruction` · `Doctype` · `XmlNode` · `XmlChild` · `XmlPosition` · `ParserOptions` · `SerializeOptions` · `ObjectBuilderOptions` · `ArrayElementSelector` · `XmlObjectMap` · `XmlObjectValue` · `XmlBuilderOptions` · `XmlInputObject` · `XmlInputValue` · `ObjectToXmlOptions`
131
223
 
132
224
  ## Features
133
225
 
package/dist/index.cjs CHANGED
@@ -723,10 +723,371 @@ function escapeAttribute(value) {
723
723
  return escapeText(value).replace(/"/g, "&quot;");
724
724
  }
725
725
 
726
+ // src/object.ts
727
+ var DEFAULT_OBJECT_OPTIONS = {
728
+ attributePrefix: "@_",
729
+ textKey: "#text",
730
+ stripNamespaces: false,
731
+ coalesceText: true
732
+ };
733
+ function stripNamespace(name) {
734
+ const index = name.indexOf(":");
735
+ if (index === -1) {
736
+ return name;
737
+ }
738
+ return name.slice(index + 1);
739
+ }
740
+ function resolveName(value) {
741
+ if (typeof value !== "string") {
742
+ return {
743
+ name: value.name,
744
+ localName: value.local,
745
+ prefix: value.prefix,
746
+ uri: value.uri
747
+ };
748
+ }
749
+ const index = value.indexOf(":");
750
+ if (index === -1) {
751
+ return { name: value, localName: value, prefix: "", uri: "" };
752
+ }
753
+ return {
754
+ name: value,
755
+ localName: value.slice(index + 1),
756
+ prefix: value.slice(0, index),
757
+ uri: ""
758
+ };
759
+ }
760
+ function buildObject(root, options = {}) {
761
+ const settings = buildSettings(options);
762
+ return buildNode(root, settings, []);
763
+ }
764
+ function buildXmlNode(obj, options = {}) {
765
+ const settings = buildXmlSettings(options);
766
+ const root = resolveRoot(obj, settings);
767
+ const rootName = normalizeName(root.name, settings);
768
+ return buildElement(rootName, root.value, settings, []);
769
+ }
770
+ function objectToXml(obj, options = {}) {
771
+ const node = buildXmlNode(obj, options);
772
+ return serializeXml(node, options);
773
+ }
774
+ var ObjectBuilder = class {
775
+ constructor(options = {}) {
776
+ this.stack = [];
777
+ this.root = null;
778
+ this.rootName = null;
779
+ this.onOpenTag = (tag) => {
780
+ const name = normalizeName(tag.name, this.options);
781
+ const attributes = normalizeAttributes(tag.attributes, this.options);
782
+ const state = {
783
+ name,
784
+ attributes,
785
+ textParts: [],
786
+ children: /* @__PURE__ */ Object.create(null)
787
+ };
788
+ this.rootName ?? (this.rootName = name);
789
+ this.stack.push(state);
790
+ };
791
+ this.onText = (text) => {
792
+ if (!text) {
793
+ return;
794
+ }
795
+ const current = this.stack[this.stack.length - 1];
796
+ if (!current) {
797
+ return;
798
+ }
799
+ current.textParts.push(text);
800
+ };
801
+ this.onCdata = (text) => {
802
+ this.onText(text);
803
+ };
804
+ this.onCloseTag = () => {
805
+ const state = this.stack.pop();
806
+ if (!state) {
807
+ return;
808
+ }
809
+ const value = finalizeElement(state, this.options);
810
+ const parent = this.stack[this.stack.length - 1];
811
+ if (!parent) {
812
+ this.root = value;
813
+ return;
814
+ }
815
+ const path = this.stack.map((entry) => entry.name);
816
+ addChild(parent.children, state.name, value, this.options, path);
817
+ };
818
+ this.options = buildSettings(options);
819
+ }
820
+ getResult() {
821
+ if (this.root === null) {
822
+ throw new Error("No root element found");
823
+ }
824
+ return this.root;
825
+ }
826
+ getRootName() {
827
+ if (!this.rootName) {
828
+ throw new Error("No root element found");
829
+ }
830
+ return this.rootName;
831
+ }
832
+ };
833
+ function buildSettings(options) {
834
+ return { ...DEFAULT_OBJECT_OPTIONS, ...options };
835
+ }
836
+ function buildXmlSettings(options) {
837
+ return { ...DEFAULT_OBJECT_OPTIONS, ...options };
838
+ }
839
+ function buildNode(node, options, path) {
840
+ const name = normalizeName(node.name, options);
841
+ const attributes = normalizeAttributeMap(node.attributes ?? {}, options);
842
+ const state = {
843
+ attributes,
844
+ textParts: [],
845
+ children: /* @__PURE__ */ Object.create(null)
846
+ };
847
+ const children = node.children ?? [];
848
+ for (const child of children) {
849
+ if (typeof child === "string") {
850
+ if (child) {
851
+ state.textParts.push(child);
852
+ }
853
+ continue;
854
+ }
855
+ const value = buildNode(child, options, [...path, name]);
856
+ const childName = normalizeName(child.name, options);
857
+ addChild(state.children, childName, value, options, [...path, name]);
858
+ }
859
+ return finalizeElement(state, options);
860
+ }
861
+ function normalizeName(name, options) {
862
+ if (options.stripNamespaces) {
863
+ return stripNamespace(name);
864
+ }
865
+ return name;
866
+ }
867
+ function normalizeXmlName(name, options) {
868
+ if (options.stripNamespaces) {
869
+ return stripNamespace(name);
870
+ }
871
+ return name;
872
+ }
873
+ function normalizeAttributes(attributes, options) {
874
+ const result = /* @__PURE__ */ Object.create(null);
875
+ for (const [key, attr] of Object.entries(attributes)) {
876
+ const name = normalizeName(key, options);
877
+ result[name] = attr.value;
878
+ }
879
+ return result;
880
+ }
881
+ function normalizeAttributeMap(attributes, options) {
882
+ const result = /* @__PURE__ */ Object.create(null);
883
+ for (const [key, value] of Object.entries(attributes)) {
884
+ const name = normalizeName(key, options);
885
+ result[name] = value;
886
+ }
887
+ return result;
888
+ }
889
+ function addChild(target, name, value, options, path) {
890
+ const forcedArray = shouldForceArray(name, path, options);
891
+ const existing = target[name];
892
+ if (existing === void 0) {
893
+ target[name] = forcedArray ? [value] : value;
894
+ return;
895
+ }
896
+ if (Array.isArray(existing)) {
897
+ existing.push(value);
898
+ return;
899
+ }
900
+ target[name] = [existing, value];
901
+ }
902
+ function shouldForceArray(name, path, options) {
903
+ const rule = options.arrayElements;
904
+ if (!rule) {
905
+ return false;
906
+ }
907
+ if (rule instanceof Set) {
908
+ return rule.has(name);
909
+ }
910
+ return rule(name, path);
911
+ }
912
+ function resolveRoot(obj, options) {
913
+ if (isRecord(obj)) {
914
+ const keys = Object.keys(obj);
915
+ if (keys.length === 1) {
916
+ const name = keys[0] ?? "";
917
+ return { name, value: obj[name] };
918
+ }
919
+ }
920
+ if (!options.rootName) {
921
+ throw new Error("Root element name is required when object has multiple keys");
922
+ }
923
+ return { name: options.rootName, value: obj };
924
+ }
925
+ function buildElement(name, value, options, path) {
926
+ const attributes = /* @__PURE__ */ Object.create(null);
927
+ const children = [];
928
+ const nextPath = [...path, name];
929
+ if (Array.isArray(value)) {
930
+ for (const item of value) {
931
+ appendContent(children, item, options, nextPath);
932
+ }
933
+ return finalizeNode(name, attributes, children);
934
+ }
935
+ if (isPrimitive(value)) {
936
+ const text = coerceText(value);
937
+ if (text !== null) {
938
+ children.push(text);
939
+ }
940
+ return finalizeNode(name, attributes, children);
941
+ }
942
+ if (isRecord(value)) {
943
+ for (const [key, entryValue] of Object.entries(value)) {
944
+ if (isAttributeKey(key, options)) {
945
+ const attrName = normalizeXmlName(key.slice(options.attributePrefix.length), options);
946
+ const attrValue = coerceText(entryValue);
947
+ if (attrValue !== null) {
948
+ attributes[attrName] = attrValue;
949
+ }
950
+ continue;
951
+ }
952
+ if (key === options.textKey) {
953
+ appendText(children, entryValue, options);
954
+ continue;
955
+ }
956
+ const childName = normalizeXmlName(key, options);
957
+ addChildElements(children, childName, entryValue, options, nextPath);
958
+ }
959
+ }
960
+ return finalizeNode(name, attributes, children);
961
+ }
962
+ function addChildElements(children, name, value, options, path) {
963
+ const forcedArray = shouldForceArray(name, path, options);
964
+ const items = Array.isArray(value) ? value : forcedArray ? [value] : [value];
965
+ for (const item of items) {
966
+ if (item === void 0 || item === null) {
967
+ children.push({ name });
968
+ continue;
969
+ }
970
+ children.push(buildElement(name, item, options, path));
971
+ }
972
+ }
973
+ function appendContent(children, value, options, path) {
974
+ if (value === void 0 || value === null) {
975
+ return;
976
+ }
977
+ if (Array.isArray(value)) {
978
+ for (const item of value) {
979
+ appendContent(children, item, options, path);
980
+ }
981
+ return;
982
+ }
983
+ if (isPrimitive(value)) {
984
+ const text = coerceText(value);
985
+ if (text !== null) {
986
+ children.push(text);
987
+ }
988
+ return;
989
+ }
990
+ if (isRecord(value)) {
991
+ for (const [key, entryValue] of Object.entries(value)) {
992
+ const childName = normalizeXmlName(key, options);
993
+ addChildElements(children, childName, entryValue, options, path);
994
+ }
995
+ }
996
+ }
997
+ function appendText(children, value, options) {
998
+ if (value === void 0 || value === null) {
999
+ return;
1000
+ }
1001
+ if (Array.isArray(value)) {
1002
+ const parts = value.map((item) => coerceText(item)).filter((item) => item !== null);
1003
+ if (parts.length === 0) {
1004
+ return;
1005
+ }
1006
+ if (options.coalesceText) {
1007
+ children.push(parts.join(""));
1008
+ return;
1009
+ }
1010
+ for (const part of parts) {
1011
+ children.push(part);
1012
+ }
1013
+ return;
1014
+ }
1015
+ const text = coerceText(value);
1016
+ if (text !== null) {
1017
+ children.push(text);
1018
+ }
1019
+ }
1020
+ function finalizeNode(name, attributes, children) {
1021
+ const node = { name };
1022
+ if (Object.keys(attributes).length > 0) {
1023
+ node.attributes = attributes;
1024
+ }
1025
+ if (children.length > 0) {
1026
+ node.children = children;
1027
+ }
1028
+ return node;
1029
+ }
1030
+ function isAttributeKey(key, options) {
1031
+ if (!options.attributePrefix) {
1032
+ return false;
1033
+ }
1034
+ return key.startsWith(options.attributePrefix) && key.length > options.attributePrefix.length;
1035
+ }
1036
+ function isRecord(value) {
1037
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1038
+ }
1039
+ function isPrimitive(value) {
1040
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1041
+ }
1042
+ function coerceText(value) {
1043
+ if (value === void 0 || value === null) {
1044
+ return null;
1045
+ }
1046
+ if (typeof value === "string") {
1047
+ return value;
1048
+ }
1049
+ if (typeof value === "number" || typeof value === "boolean") {
1050
+ return String(value);
1051
+ }
1052
+ if (value instanceof Date) {
1053
+ return value.toISOString();
1054
+ }
1055
+ return null;
1056
+ }
1057
+ function finalizeElement(state, options) {
1058
+ const hasAttributes = Object.keys(state.attributes).length > 0;
1059
+ const hasChildren = Object.keys(state.children).length > 0;
1060
+ const hasText = state.textParts.length > 0;
1061
+ const textValue = options.coalesceText ? state.textParts.join("") : state.textParts.length <= 1 ? state.textParts[0] ?? "" : state.textParts.slice();
1062
+ if (!hasAttributes && !hasChildren) {
1063
+ if (!hasText) {
1064
+ return "";
1065
+ }
1066
+ return textValue;
1067
+ }
1068
+ const result = /* @__PURE__ */ Object.create(null);
1069
+ for (const [key, value] of Object.entries(state.attributes)) {
1070
+ result[`${options.attributePrefix}${key}`] = value;
1071
+ }
1072
+ for (const [key, value] of Object.entries(state.children)) {
1073
+ result[key] = value;
1074
+ }
1075
+ if (hasText) {
1076
+ result[options.textKey] = textValue;
1077
+ }
1078
+ return result;
1079
+ }
1080
+
1081
+ exports.ObjectBuilder = ObjectBuilder;
726
1082
  exports.TreeBuilder = TreeBuilder;
727
1083
  exports.XmlSaxError = XmlSaxError;
728
1084
  exports.XmlSaxParser = XmlSaxParser;
1085
+ exports.buildObject = buildObject;
1086
+ exports.buildXmlNode = buildXmlNode;
1087
+ exports.objectToXml = objectToXml;
729
1088
  exports.parseXmlString = parseXmlString;
1089
+ exports.resolveName = resolveName;
730
1090
  exports.serializeXml = serializeXml;
1091
+ exports.stripNamespace = stripNamespace;
731
1092
  //# sourceMappingURL=index.cjs.map
732
1093
  //# sourceMappingURL=index.cjs.map