diff --git a/src/__tests__/attributes.mjs b/src/__tests__/attributes.mjs index 95081c4..955c75c 100644 --- a/src/__tests__/attributes.mjs +++ b/src/__tests__/attributes.mjs @@ -691,3 +691,43 @@ for (const trailing of ["*", "$", "^", "~", "|"]) { t.deepEqual(tree.toString(), "[href]"); }); } + +for (const [quoteMark, value, expected] of [ + ['"', "foo", '[href="foo"]'], + ['"', "foo bar", '[href="foo bar"]'], + ['"', 'say "hi"', '[href="say \\"hi\\""]'], + ["'", "it's", "[href='it\\'s']"], + [null, "foo bar", "[href=foo\\ bar]"], + [null, "foo", "[href=foo]"], +]) { + ava(`Attribute constructor applies quoteMark ${quoteMark} to ${value}`, (t) => { + const attr = parser.attribute({ attribute: "href", operator: "=", value, quoteMark }); + t.deepEqual(attr.quoteMark, quoteMark); + t.deepEqual(attr.toString(), expected); + const reparsed = parser().astSync(attr.toString()).first.first; + t.deepEqual(reparsed.value, value); + t.deepEqual(reparsed.quoteMark, quoteMark); + }); +} + +ava("Attribute constructor keeps an explicit raw value", (t) => { + const attr = parser.attribute({ + attribute: "href", + operator: "=", + value: "a:b", + quoteMark: '"', + raws: { value: '"a\\3A b"' }, + }); + t.deepEqual(attr.toString(), '[href="a\\3A b"]'); +}); + +ava("Parsed attributes keep their original raw value state", (t) => { + for (const input of ["[href=foo]", '[href="foo"]', "[href=@{foo}]", "[href=a\\:b]"]) { + const attr = parser().astSync(input).first.first; + t.deepEqual(attr.toString(), input); + t.deepEqual(attr.clone().toString(), input); + if (!input.includes("\\") && !input.includes('"')) { + t.deepEqual(attr.raws.value, undefined); + } + } +}); diff --git a/src/parser.js b/src/parser.js index 24d35be..8bf7755 100644 --- a/src/parser.js +++ b/src/parser.js @@ -518,7 +518,14 @@ export default class Parser { } unescapeProp(node, "attribute"); unescapeProp(node, "namespace"); - this.newNode(new Attribute(node)); + const hasRawValue = node.raws && node.raws.value !== undefined; + const attribute = new Attribute(node); + // Preserve the parsed spelling, including non-standard syntax, rather + // than the quoting and escaping used for API-created attributes. + if (!hasRawValue) { + delete attribute.raws.value; + } + this.newNode(attribute); this.position++; } diff --git a/src/selectors/attribute.js b/src/selectors/attribute.js index ef991c1..d66a67e 100644 --- a/src/selectors/attribute.js +++ b/src/selectors/attribute.js @@ -82,6 +82,11 @@ export default class Attribute extends Namespace { ), }); this._constructed = true; + // A value given with a quote mark is unescaped, so it still needs quoting + // and escaping for that quote mark, unless its raw spelling was given too. + if (typeof this._value === "string" && this.raws.value === undefined) { + this._syncRawValue(); + } } /**