From 712ee25a3e0974909b674873786a87b3030eb069 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Mon, 22 Jun 2026 21:26:57 +0100 Subject: [PATCH] fix: preserve consecutive backslashes in attribute values quoteAttribute serialized attribute values with JSON.stringify (which doubles every backslash) and then tried to undo the doubling with `.replace(/([^\\])\\/g, '$1')`. That regex needs a non-backslash before each backslash and matches non-overlapping, so it cannot collapse runs of consecutive backslashes: a value such as "C:\\Users\\me" gained an extra backslash on every serialization and no longer round-tripped through setAttribute -> toString -> parse. (The single-backslash case fixed in issue #306 worked; consecutive backslashes were the unhandled residual.) Attribute values are literal text, so only the double quote needs escaping (as "). Quote the value directly, which preserves any number of backslashes and still escapes embedded quotes (issue #62 behaviour). --- src/nodes/html.ts | 10 +++++----- test/tests/quoteattributes.js | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/nodes/html.ts b/src/nodes/html.ts index 6462024..c6b3534 100644 --- a/src/nodes/html.ts +++ b/src/nodes/html.ts @@ -158,11 +158,11 @@ export default class HTMLElement extends Node { return 'null'; } - return JSON.stringify(attr.replace(/"/g, '"')) - .replace(/([^\\])\\t/g, '$1\t') - .replace(/([^\\])\\n/g, '$1\n') - .replace(/([^\\])\\r/g, '$1\r') - .replace(/([^\\])\\/g, '$1'); + // Attribute values are literal text: only the double quote needs + // escaping (as "). The previous JSON.stringify-based approach + // doubled backslashes and then failed to undo runs of consecutive + // backslashes, corrupting values such as "C:\\Users\\me". + return `"${attr.replace(/"/g, '"')}"`; } /** diff --git a/test/tests/quoteattributes.js b/test/tests/quoteattributes.js index ae7572f..8c561ff 100644 --- a/test/tests/quoteattributes.js +++ b/test/tests/quoteattributes.js @@ -28,4 +28,12 @@ describe('quote attributes', function () { foo: '[{"bar":"baz"}]', }); }); + + it('preserves consecutive backslashes through a setAttribute round-trip', function () { + const root = parse('
'); + const div = root.firstChild; + const value = 'C:\\\\Users\\\\me'; // C:\\Users\\me (doubled backslashes) + div.setAttribute('path', value); + parse(div.toString()).firstChild.getAttribute('path').should.equal(value); + }); });