Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions spec/j2x_ordered_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,18 @@ describe("XMLBuilder", function () {
expect(output).toContain('<h1>Nested</h1>');
});

it("should preserve the sign of a negative zero text value", function () {
const builder = new XMLBuilder({ preserveOrder: true });
const result = builder.build([{ a: [{ "#text": -0 }] }]);
expect(result).toEqual(`<a>-0</a>`);
});

it("should preserve the sign of a negative zero attribute value", function () {
const builder = new XMLBuilder({ ignoreAttributes: false, preserveOrder: true });
const result = builder.build([{ a: [{ "#text": "v" }], ":@": { "@_x": -0 } }]);
expect(result).toEqual(`<a x="-0">v</a>`);
});

});

describe("XMLBuilder- array processing issue", function () {
Expand Down
19 changes: 19 additions & 0 deletions spec/j2x_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,4 +688,23 @@ describe("XMLBuilder", function () {
expect(result).toEqual(expected);
});

it("should preserve the sign of a negative zero tag value", function () {
const builder = new XMLBuilder();
const result = builder.build({ a: -0 });
expect(result).toEqual(`<a>-0</a>`);
});

it("should preserve the sign of a negative zero attribute value", function () {
const builder = new XMLBuilder({ ignoreAttributes: false });
const result = builder.build({ a: { "@_x": -0 } });
expect(result).toEqual(`<a x="-0"></a>`);
});

it("should preserve the sign of a negative zero value with oneListGroup", function () {
const jObj = { a: [-0, -0] };
const builder = new XMLBuilder({ oneListGroup: "true" });
const result = builder.build(jObj);
expect(result).toEqual(`<a>-0-0</a>`);
});

});
24 changes: 24 additions & 0 deletions spec/stopNodes_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ describe("stopNodes Builder - Basic Tests", function () {
expect(output).toContain("<fix1><nested>str</nested></fix1>");
});

it("should preserve the sign of a negative zero value in raw stopNode content", function () {
const options = {
stopNodes: ["a"],
preserveOrder: false
};

const builder = new XMLBuilder(options);
const output = builder.build({ a: -0 });

expect(output).toEqual("<a>-0</a>");
});

});

describe("preserveOrder: true", function () {
Expand Down Expand Up @@ -302,6 +314,18 @@ describe("stopNodes Builder - Basic Tests", function () {
expect(output).toContain("<pre>test > < &</pre>");
});

it("should preserve the sign of a negative zero value in raw stopNode content", function () {
const options = {
stopNodes: ["a"],
preserveOrder: true
};

const builder = new XMLBuilder(options);
const output = builder.build([{ a: [{ "#text": -0 }] }]);

expect(output).toEqual("<a>-0</a>");
});

});

});
19 changes: 12 additions & 7 deletions src/fxb.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import buildFromOrderedJs from './orderedJs2Xml.js';
import getIgnoreAttributesFn from "./ignoreAttributes.js";
import { Expression, Matcher } from 'path-expression-matcher';
import { safeComment, safeCdata, escapeAttribute } from './util.js';
import { safeComment, safeCdata, escapeAttribute, valToStr } from './util.js';
import { createValidator } from 'xml-naming';

const defaultOptions = {
Expand Down Expand Up @@ -209,11 +209,11 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {
if (attr && !this.ignoreAttributesFn(attr, jPath)) {
// Resolve the attribute name through sanitizeName
const resolvedAttr = resolveTagName(attr, true, this.options, matcher, qNameValidator);
attrStr += this.buildAttrPairStr(resolvedAttr, '' + jObj[key], isCurrentStopNode);
attrStr += this.buildAttrPairStr(resolvedAttr, valToStr(jObj[key]), isCurrentStopNode);
} else if (!attr) {
//tag value
if (key === this.options.textNodeName) {
let newval = this.options.tagValueProcessor(key, '' + jObj[key]);
let newval = this.options.tagValueProcessor(key, valToStr(jObj[key]));
val += this.replaceEntitiesValue(newval);
} else {
// Check if this is a stopNode before building
Expand All @@ -223,7 +223,7 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {

if (isStopNode) {
// Build as raw content without encoding
const textValue = '' + jObj[key];
const textValue = valToStr(jObj[key]);
if (textValue === '') {
val += this.indentate(level) + '<' + resolvedKey + this.closeTag(resolvedKey) + this.tagEndChar;
} else {
Expand Down Expand Up @@ -265,6 +265,7 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {
if (this.options.oneListGroup) {
let textValue = this.options.tagValueProcessor(resolvedKey, item);
textValue = this.replaceEntitiesValue(textValue);
textValue = valToStr(textValue);
listTagVal += textValue;
} else {
// Check if this is a stopNode before building
Expand All @@ -274,7 +275,7 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {

if (isStopNode) {
// Build as raw content without encoding
const textValue = '' + item;
const textValue = valToStr(item);
if (textValue === '') {
listTagVal += this.indentate(level) + '<' + resolvedKey + this.closeTag(resolvedKey) + this.tagEndChar;
} else {
Expand All @@ -298,7 +299,7 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {
for (let j = 0; j < L; j++) {
// Resolve attribute names inside attributesGroupName
const resolvedAttr = resolveTagName(Ks[j], true, this.options, matcher, qNameValidator);
attrStr += this.buildAttrPairStr(resolvedAttr, '' + jObj[key][Ks[j]], isCurrentStopNode);
attrStr += this.buildAttrPairStr(resolvedAttr, valToStr(jObj[key][Ks[j]]), isCurrentStopNode);
}
} else {
val += this.processTextOrObjNode(jObj[key], resolvedKey, level, matcher, qNameValidator)
Expand All @@ -310,7 +311,7 @@ Builder.prototype.j2x = function (jObj, level, matcher, qNameValidator) {

Builder.prototype.buildAttrPairStr = function (attrName, val, isStopNode) {
if (!isStopNode) {
val = this.options.attributeValueProcessor(attrName, '' + val);
val = this.options.attributeValueProcessor(attrName, valToStr(val));
val = this.replaceEntitiesValue(val);
}
if (this.options.suppressBooleanAttributes && val === "true") {
Expand Down Expand Up @@ -570,6 +571,10 @@ Builder.prototype.buildTextValNode = function (val, key, attrStr, level, matcher
// Normal processing: apply tagValueProcessor and entity replacement
let textValue = this.options.tagValueProcessor(key, val);
textValue = this.replaceEntitiesValue(textValue);
// tagValueProcessor may return the raw value unchanged (default is identity), and
// replaceEntitiesValue no-ops on non-strings, so a plain number can still reach here;
// stringify it now, sign-preserving, before it's implicitly ToString'd below.
textValue = valToStr(textValue);

if (textValue === '') {
return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar;
Expand Down
9 changes: 5 additions & 4 deletions src/orderedJs2Xml.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Expression, Matcher } from 'path-expression-matcher';
import { safeComment, safeCdata, escapeAttribute } from "./util.js";
import { safeComment, safeCdata, escapeAttribute, valToStr } from "./util.js";
import { createValidator } from 'xml-naming';

const EOL = "\n";
Expand Down Expand Up @@ -88,7 +88,7 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions, qName
if (!Array.isArray(arr)) {
// Non-array values (e.g. string tag values) should be treated as text content
if (arr !== undefined && arr !== null) {
let text = arr.toString();
let text = valToStr(arr);
text = replaceEntitiesValue(text, options);
return text;
}
Expand Down Expand Up @@ -127,6 +127,7 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions, qName
tagText = options.tagValueProcessor(tagName, tagText);
tagText = replaceEntitiesValue(tagText, options);
}
tagText = valToStr(tagText);
if (isPreviousElementTag) {
xmlStr += indentation;
}
Expand Down Expand Up @@ -235,7 +236,7 @@ function getRawContent(arr, options) {
if (!Array.isArray(arr)) {
// Non-array values return as-is
if (arr !== undefined && arr !== null) {
return arr.toString();
return valToStr(arr);
}
return "";
}
Expand All @@ -247,7 +248,7 @@ function getRawContent(arr, options) {

if (tagName === options.textNodeName) {
// Raw text content - NO processing, NO entity replacement
content += item[tagName];
content += valToStr(item[tagName]);
} else if (tagName === options.cdataPropName) {
// CDATA content
content += item[tagName][0][options.textNodeName];
Expand Down
13 changes: 10 additions & 3 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@


// String(val)/val.toString() drop the sign of -0 (e.g. String(-0) === '0'), silently
// corrupting a round-tripped negative-zero value. XML has no separate int/float syntax,
// so this is the single place every raw value gets turned into text.
export function valToStr(val) {
return typeof val === 'number' && Object.is(val, -0) ? '-0' : String(val)
}

export function safeComment(val) {
return String(val)
return valToStr(val)
.replace(/--/g, '- -') // -- is illegal anywhere in comment content
.replace(/--/g, '- -') // handle the scenario when 2 consiucative dashes appears
.replace(/-$/, '- '); // trailing - would form -- with the closing -->
}

export function safeCdata(val) {
return String(val).replace(/\]\]>/g, ']]]]><![CDATA[>')
return valToStr(val).replace(/\]\]>/g, ']]]]><![CDATA[>')
}

export function escapeAttribute(val) {
return String(val).replace(/"/g, '&quot;').replace(/'/g, '&apos;')
return valToStr(val).replace(/"/g, '&quot;').replace(/'/g, '&apos;')
}