diff --git a/spec/entities_spec.js b/spec/entities_spec.js
index 09a56c4..2f35020 100644
--- a/spec/entities_spec.js
+++ b/spec/entities_spec.js
@@ -148,3 +148,31 @@ describe("External Entities", function () {
expect(output.replace(/\s+/g, "")).toEqual(expected.replace(/\s+/g, ""));
});
});
+
+describe("Carriage return", function () {
+ const CR = String.fromCharCode(13); // U+000D
+ const LF = String.fromCharCode(10); // U+000A
+ const TAB = String.fromCharCode(9); // U+0009
+
+ it("escapes a carriage return as
in text and attributes", function () {
+ const builder = new XMLBuilder({ ignoreAttributes: false });
+ expect(builder.build({ n: "A" + CR + "B" })).toEqual("A
B");
+ expect(builder.build({ n: { "@_a": "A" + CR + "B" } })).toEqual(``);
+ });
+
+ it("preserves a carriage return through a build -> parse round trip", function () {
+ // A raw CR would be silently folded to LF on parse (XML 1.0 2.11).
+ const builder = new XMLBuilder({ ignoreAttributes: false });
+ const parser = new XMLParser({ ignoreAttributes: false, htmlEntities: true });
+ for (const value of ["A" + CR + "B", "line1" + CR + LF + "line2"]) {
+ expect(parser.parse(builder.build({ n: value })).n).toEqual(value);
+ expect(parser.parse(builder.build({ n: { "@_a": value } })).n["@_a"]).toEqual(value);
+ }
+ });
+
+ it("leaves LF and TAB unescaped", function () {
+ const builder = new XMLBuilder();
+ expect(builder.build({ n: "A" + LF + "B" })).toEqual("A" + LF + "B");
+ expect(builder.build({ n: "A" + TAB + "B" })).toEqual("A" + TAB + "B");
+ });
+});
diff --git a/src/fxb.js b/src/fxb.js
index 73c61e6..92deea7 100644
--- a/src/fxb.js
+++ b/src/fxb.js
@@ -31,7 +31,8 @@ const defaultOptions = {
{ regex: new RegExp(">", "g"), val: ">" },
{ regex: new RegExp("<", "g"), val: "<" },
{ regex: new RegExp("\'", "g"), val: "'" },
- { regex: new RegExp("\"", "g"), val: """ }
+ { regex: new RegExp("\"", "g"), val: """ },
+ { regex: new RegExp("\\r", "g"), val: "
" }//a raw CR is folded to LF on parse (XML 1.0 2.11)
],
processEntities: true,
stopNodes: [],