Compare commits

..

4 Commits

Author SHA1 Message Date
4c60f52b5e Merge remote-tracking branch 'origin/develop' into develop
All checks were successful
Build / Publish Docs (push) Successful in 1m21s
Build / Build NPM Project (push) Successful in 1m25s
Build / Tag Version (push) Successful in 11s
# Conflicts:
#	package.json
2026-08-24 18:37:59 -04:00
35c14d01c2 Added better attribute support to fromXML 2026-08-24 18:37:29 -04:00
92f36fdb5b Bump 0.30.7
All checks were successful
Build / Publish Docs (push) Successful in 54s
Build / Build NPM Project (push) Successful in 34s
Build / Tag Version (push) Successful in 9s
2026-08-17 14:56:41 -04:00
721fe2dcf8 Patched prototype pollution
Some checks failed
Build / Tag Version (push) Has been cancelled
Build / Build NPM Project (push) Has been cancelled
Build / Publish Docs (push) Has been cancelled
2026-08-17 14:56:18 -04:00
4 changed files with 376 additions and 371 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ztimson/utils", "name": "@ztimson/utils",
"version": "0.30.6", "version": "0.30.8",
"description": "Utility library", "description": "Utility library",
"author": "Zak Timson", "author": "Zak Timson",
"license": "MIT", "license": "MIT",

View File

@@ -97,8 +97,10 @@ export function deepCopy<T>(value: T): T {
* @return {any} The des * @return {any} The des
*/ */
export function deepMerge<T>(target: any, ...sources: any[]): T { export function deepMerge<T>(target: any, ...sources: any[]): T {
const BLOCKED = new Set(['__proto__', 'constructor', 'prototype']);
sources.forEach(s => { sources.forEach(s => {
for(const key in s) { for(const key in s) {
if(BLOCKED.has(key) || !Object.prototype.hasOwnProperty.call(s, key)) continue;
if(s[key] && typeof s[key] == 'object' && !Array.isArray(s[key])) { if(s[key] && typeof s[key] == 'object' && !Array.isArray(s[key])) {
if(!target[key]) target[key] = {}; if(!target[key]) target[key] = {};
deepMerge(target[key], s[key]); deepMerge(target[key], s[key]);

View File

@@ -1,12 +1,12 @@
/** /**
* Parses an XML string into a structured JavaScript object. * Parses an XML string into a plain JavaScript object.
* Each tag becomes a key. Attributes and child tags are merged as
* sibling properties on that tag's object. Duplicate tag/attribute
* names are collapsed into arrays. Text-only tags resolve to their
* (optionally numeric) value; if a tag has both text and attributes/
* children, the text is kept under a `_text` key.
* @param {string} xml - The XML string to parse * @param {string} xml - The XML string to parse
* @returns {Object} An object with `tag`, `attributes`, and `children` properties * @returns {Object} The parsed object tree
*/
/**
* Parses an XML string into a structured JavaScript object (fast-xml-parser format).
* @param {string} xml - The XML string to parse
* @returns {Object} An object with tag names as keys and text content or nested objects as values
*/ */
export function fromXml(xml: string) { export function fromXml(xml: string) {
xml = xml.trim(); xml = xml.trim();
@@ -33,11 +33,11 @@ export function fromXml(xml: string) {
if(xml[pos] === '/' && xml[pos + 1] === '>') { if(xml[pos] === '/' && xml[pos + 1] === '>') {
pos += 2; // skip /> pos += 2; // skip />
return { [tagName]: '' }; return { [tagName]: Object.keys(attributes).length ? attributes : '' };
} }
pos++; // skip > pos++; // skip >
const children: any[] = []; const children: any[] = Object.entries(attributes).map(([k, v]) => ({ [k]: v }));
let textContent = ''; let textContent = '';
while(pos < xml.length) { while(pos < xml.length) {
@@ -49,7 +49,6 @@ export function fromXml(xml: string) {
pos++; // skip > pos++; // skip >
break; break;
} }
const startPos = pos;
const child = parseNode(); const child = parseNode();
if(typeof child === 'string') { if(typeof child === 'string') {
textContent += child; textContent += child;
@@ -64,17 +63,18 @@ export function fromXml(xml: string) {
return { [tagName]: value }; return { [tagName]: value };
} }
// If only text with no children // If nothing at all
if(children.length === 0) { if(children.length === 0) {
return { [tagName]: '' }; return { [tagName]: '' };
} }
// Merge children into object // Merge attributes/children into object
const result: any = {}; const result: any = {};
if(textContent) result._text = isNumeric(textContent) ? Number(textContent) : textContent;
for(const child of children) { for(const child of children) {
for(const [key, value] of Object.entries(child)) { for(const [key, value] of Object.entries(child)) {
if(result[key]) { if(result[key]) {
// Convert to array if duplicate tags // Convert to array if duplicate tags/attrs
if(!Array.isArray(result[key])) { if(!Array.isArray(result[key])) {
result[key] = [result[key]]; result[key] = [result[key]];
} }

View File

@@ -14,10 +14,10 @@ describe('XML Parser', () => {
expect(result).toEqual({ item: '' }); expect(result).toEqual({ item: '' });
}); });
it('should parse tag with attributes (ignored in fast-xml-parser format)', () => { it('should parse tag with attributes as merged properties', () => {
const xml = '<user id="1" name="someone" />'; const xml = '<user id="1" name="someone" />';
const result = fromXml(xml); const result = fromXml(xml);
expect(result).toEqual({ user: '' }); expect(result).toEqual({ user: { id: '1', name: 'someone' } });
}); });
it('should parse tag with text content', () => { it('should parse tag with text content', () => {
@@ -94,6 +94,8 @@ describe('XML Parser', () => {
expect(result).toEqual({ expect(result).toEqual({
root: { root: {
user: { user: {
id: '1',
name: 'someone',
email: 'someone@example.com', email: 'someone@example.com',
active: '' active: ''
} }
@@ -190,7 +192,7 @@ describe('XML Parser', () => {
}); });
describe('round-trip', () => { describe('round-trip', () => {
it('should parse toXml output back to fast-xml-parser format', () => { it('should parse toXml output back with attributes merged as properties', () => {
const obj = { const obj = {
tag: 'root', tag: 'root',
attributes: { id: '1' }, attributes: { id: '1' },
@@ -202,6 +204,7 @@ describe('XML Parser', () => {
const parsed = fromXml(xml); const parsed = fromXml(xml);
expect(parsed).toEqual({ expect(parsed).toEqual({
root: { root: {
id: '1',
child: 'text' child: 'text'
} }
}); });