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: [],