diff --git a/src/__tests__/build.test.ts b/src/__tests__/build.test.ts
index e99f7dd..bad76b6 100644
--- a/src/__tests__/build.test.ts
+++ b/src/__tests__/build.test.ts
@@ -20,8 +20,16 @@ describe('build', () => {
expect(build({ root: { item: ['a', 'b', 'c'] } })).toBe('- a
- b
- c
');
});
- it('builds empty string as self-closing tag', () => {
- expect(build({ root: { empty: '' } })).toBe('');
+ it('builds empty string as open/close tags', () => {
+ expect(build({ root: { empty: '' } })).toBe('');
+ });
+
+ it('builds null as self-closing tag', () => {
+ expect(build({ root: { nullField: null, version: '1.0' } })).toBe('1.0');
+ });
+
+ it('skips undefined values', () => {
+ expect(build({ root: { undef: undefined, kept: 'val' } })).toBe('val');
});
it('builds numeric values', () => {
diff --git a/src/__tests__/parse.test.ts b/src/__tests__/parse.test.ts
index bade996..55c83f3 100644
--- a/src/__tests__/parse.test.ts
+++ b/src/__tests__/parse.test.ts
@@ -57,7 +57,7 @@ describe('parse', () => {
describe('attributes', () => {
it('parses attributes with default prefix', () => {
const result = parse('value');
- expect(result).toEqual({ root: { '#text': 'value', '@_id': 1, '@_name': 'test' } });
+ expect(result).toEqual({ root: { '#text': 'value', '@_id': '1', '@_name': 'test' } });
});
it('parses attributes on nested elements', () => {
@@ -67,7 +67,7 @@ describe('parse', () => {
it('parses self-closing tag with attributes', () => {
const result = parse(' ');
- expect(result).toEqual({ root: { item: { '@_id': 1 } } });
+ expect(result).toEqual({ root: { item: { '@_id': '1' } } });
});
it('ignores attributes when ignoreAttributes is true', () => {
@@ -81,12 +81,12 @@ describe('parse', () => {
const result = parse('text', {
attributeNamePrefix: '@',
});
- expect(result).toEqual({ root: { '#text': 'text', '@id': 1 } });
+ expect(result).toEqual({ root: { '#text': 'text', '@id': '1' } });
});
it('handles attributes with single quotes', () => {
const result = parse("text");
- expect(result).toEqual({ root: { '#text': 'text', '@_id': 42 } });
+ expect(result).toEqual({ root: { '#text': 'text', '@_id': '42' } });
});
});
@@ -105,8 +105,8 @@ describe('parse', () => {
const result = parse('text', {
removeNSPrefix: true,
});
- // xmlns:sf -> removeNS -> sf, sf:id -> removeNS -> id
- expect(result).toEqual({ root: { '#text': 'text', '@_sf': 'http://example.com', '@_id': 1 } });
+ // xmlns:sf is stripped entirely, sf:id -> removeNS -> id
+ expect(result).toEqual({ root: { '#text': 'text', '@_id': '1' } });
});
});
@@ -346,9 +346,9 @@ describe('parse', () => {
expect(execResult.success).toBe(true);
expect(execResult.line).toBe(-1);
// xsi:nil="true" self-closing tags have the nil attribute (ns-prefix stripped)
- expect(execResult.compileProblem).toEqual({ '@_nil': true });
- expect(execResult.exceptionMessage).toEqual({ '@_nil': true });
- expect(execResult.exceptionStackTrace).toEqual({ '@_nil': true });
+ expect(execResult.compileProblem).toEqual({ '@_nil': 'true' });
+ expect(execResult.exceptionMessage).toEqual({ '@_nil': 'true' });
+ expect(execResult.exceptionStackTrace).toEqual({ '@_nil': 'true' });
});
});
});
diff --git a/src/build.ts b/src/build.ts
index e3cc6d9..229e48f 100644
--- a/src/build.ts
+++ b/src/build.ts
@@ -23,7 +23,14 @@ export function build(obj: Record, options?: BuildOptions): str
const parts: string[] = [];
function serialize(key: string, value: unknown, depth: number): void {
- if (value === null || value === undefined) return;
+ if (value === undefined) return;
+
+ if (value === null) {
+ const indent = format ? indentBy.repeat(depth) : '';
+ const nl = format ? '\n' : '';
+ parts.push(`${indent}<${key}/>${nl}`);
+ return;
+ }
const indent = format ? indentBy.repeat(depth) : '';
const nl = format ? '\n' : '';
@@ -76,11 +83,7 @@ export function build(obj: Record, options?: BuildOptions): str
} else {
// Primitive value
const str = String(value);
- if (str === '') {
- parts.push(`${indent}<${key}/>${nl}`);
- } else {
- parts.push(`${indent}<${key}>${escapeXml(str)}${key}>${nl}`);
- }
+ parts.push(`${indent}<${key}>${escapeXml(str)}${key}>${nl}`);
}
}
diff --git a/src/parse.ts b/src/parse.ts
index b0e749a..c75d5ed 100644
--- a/src/parse.ts
+++ b/src/parse.ts
@@ -260,10 +260,13 @@ function parseAttributes(
const re = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
let match: RegExpExecArray | null;
while ((match = re.exec(str)) !== null) {
- let name = match[1]!;
+ const rawName = match[1]!;
let value: string = (match[2] ?? match[3])!;
- if (rmNS) name = removeNS(name);
+ // When stripping namespace prefixes, drop xmlns and xmlns:* declarations entirely
+ if (rmNS && (rawName === 'xmlns' || rawName.startsWith('xmlns:'))) continue;
+ const name = rmNS ? removeNS(rawName) : rawName;
if (processEnt) value = decodeEntities(value);
- attrs[prefix + name] = parseValue(value, coerce);
+ // Attribute values are always strings — never coerce
+ attrs[prefix + name] = value;
}
}