Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/__tests__/classes.mjs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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);
}
}
});
5 changes: 5 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions src/selectors/className.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading