diff --git a/src/__tests__/classes.mjs b/src/__tests__/classes.mjs index 7e7786c..dc1cfec 100644 --- a/src/__tests__/classes.mjs +++ b/src/__tests__/classes.mjs @@ -1,4 +1,6 @@ import { test } from "./util/helpers.mjs"; +import apiTest from "./util/runner.mjs"; +import parser from "../../dist/index.js"; test("class name", ".one", (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, "one"); @@ -274,3 +276,79 @@ test("class selector with escaping (36)", ".not-pseudo\\:\\:focus", (t, tree) => t.deepEqual(tree.nodes[0].nodes[0].type, "class"); t.deepEqual(tree.nodes[0].nodes[0].raws.value, "not-pseudo\\:\\:focus"); }); + +for (const [value, escaped] of [ + ["hover:bg-blue", "hover\\:bg-blue"], + ["1abc", "\\31 abc"], + ["foo.bar", "foo\\.bar"], + ["foo\\bar", "foo\\\\bar"], + ["♥", "\\2665"], +]) { + apiTest(`ClassName constructor escapes ${value}`, (t) => { + const className = parser.className({ value }); + t.is(className.value, value); + t.is(className.toString(), `.${escaped}`, "constructor must escape the semantic value"); + t.is(parser().astSync(className.toString()).first.first.value, value); + }); +} + +apiTest("ClassName constructor preserves explicit raw spelling in either option order", (t) => { + for (const opts of [ + { value: "hover:bg-blue", raws: { value: "hover\\3A bg-blue" } }, + { raws: { value: "hover\\3A bg-blue" }, value: "hover:bg-blue" }, + ]) { + const className = parser.className(opts); + t.is(className.value, "hover:bg-blue"); + t.is(className.toString(), ".hover\\3A bg-blue"); + } +}); + +apiTest("ClassName constructor without a value still supports later assignment", (t) => { + const className = parser.className(); + t.is(className.value, undefined); + className.value = "hover:bg-blue"; + t.is(className.toString(), ".hover\\:bg-blue"); +}); + +apiTest("ClassName constructor leaves a plain identifier and other raw properties intact", (t) => { + const plain = parser.className({ value: "button" }); + t.is(plain.toString(), ".button"); + t.is(plain.raws, undefined); + const className = parser.className({ + value: "hover:bg-blue", + raws: { spaces: { before: "/* before */" } }, + }); + t.is( + className.toString(), + "/* before */.hover\\:bg-blue", + "other raw fields must not suppress escaping", + ); +}); + +apiTest( + "ClassName clones preserve raw spelling and escape replacement values independently", + (t) => { + const original = parser.className({ + value: "hover:bg-blue", + raws: { value: "hover\\3A bg-blue" }, + }); + const clone = original.clone(); + t.is(clone.toString(), original.toString()); + t.not(clone.raws, original.raws); + const replaced = original.clone({ value: "focus:bg-red" }); + t.is(replaced.toString(), ".focus\\:bg-red"); + t.is(original.toString(), ".hover\\3A bg-blue"); + }, +); + +apiTest("Parsed classes keep their original raw state when cloned", (t) => { + for (const input of [".button", ".♥", ".foo@{bar}", ".hover\\3A bg-blue"]) { + const original = parser().astSync(input).first.first; + const clone = original.clone(); + t.is(clone.toString(), input); + t.deepEqual(clone.raws, original.raws); + if (!input.includes("\\")) { + t.is(original.raws, undefined); + } + } +}); diff --git a/src/parser.js b/src/parser.js index edf5330..69aa3eb 100644 --- a/src/parser.js +++ b/src/parser.js @@ -975,6 +975,11 @@ export default class Parser { sourceIndex, }; node = new ClassName(unescapeProp(classNameOpts, "value")); + // Preserve the parsed spelling, including non-standard syntax, rather + // than the automatic escaping used for API-created class names. + if (!classNameOpts.raws) { + delete node.raws; + } } else if (idIndexes.has(ind)) { let idOpts = { value: value.slice(1), diff --git a/src/selectors/className.js b/src/selectors/className.js index b585d5c..c6d46c1 100644 --- a/src/selectors/className.js +++ b/src/selectors/className.js @@ -8,6 +8,10 @@ export default class ClassName extends Node { super(opts); this.type = CLASS; this._constructed = true; + const value = this.value; + if (typeof value === "string" && (!this.raws || this.raws.value === undefined)) { + this.value = value; + } } set value(v) {