Skip to content

Commit

Permalink
feat: simplify classy; nix stack tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
rafegoldberg committed Dec 24, 2021
1 parent d48a21e commit 565126c
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 201 deletions.
3 changes: 3 additions & 0 deletions src/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Classy BEM Expansion Skips pre-scoped classes that end in \`-scss\`. 1`] = `"PRESCOPED"`;
78 changes: 0 additions & 78 deletions src/classy.js

This file was deleted.

67 changes: 0 additions & 67 deletions src/classy.test.js

This file was deleted.

88 changes: 88 additions & 0 deletions src/classy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable no-param-reassign
*/
import isObject from "lodash/isPlainObject";
import flat from "lodash/flattenDeep";

export const SEPARATORS = ["-", "_"];

const splitClassStrings = (classes) =>
flat(
classes.map((c) => {
return typeof c === "string" ? c?.split(/[\s,.]/g) : c;
})
);

const expandBEMPartials = (classname, namespace) => {
if (!(classname && namespace)) return classname;

/* Replace Sass-style root selectors (&)
* with the BEM namespace...
*/
if (classname[0] === "&") return classname.replace("&", namespace);

/* Prefix BEM separator "partials"
* with the BEM namespace...
*/
if (SEPARATORS.includes(classname[0])) {
if (!classname.includes("-scss")) return `${namespace}${classname}`;
}

return classname;
};

export function classy(...args) {
/* When instantiated with the `new` keyword,
* construct a faux-instance of Classy that
* can be reused for its scope and BEM root!
*/
if (new.target) {
const { bem = "", classes = {}, scope = {} } = args?.[0] || {};
return (...selectors) => {
const cn = classy({ bem, ...scope, ...classes }, selectors);
return cn || scope?.[bem] || bem;
};
}

if (!args.length) return "";

/* Shift the first param off the args array. If
* its an object, we treat it as the CSS Module
* hash which we use to auto-scope our selectors!
*/
let [scope, ...classes] = args;

/* If the first param isnt a CSS Module hash,
* add the initial arg back in to `classes`
* and set the `scope` to an empty object.
*/
if (!isObject(scope)) {
classes = [scope, ...classes];
scope = {};
}

/* Pluck the `bem` namespace out of the `scope`.
*/ const { bem = "" } = scope || {};

/* Flatten nested arrays.
*/ classes = flat(classes);

/* Split stringy lists in to arrays.
*/ classes = splitClassStrings(classes);

return classes
.filter((cn) => typeof cn === "string" && cn)
.map((cn) => {
/* BEM EXPANSIONS
*/ cn = expandBEMPartials(cn, bem);

/* CSS MODULE AUTO-SCOPING
*/ if (cn in scope) cn = scope[cn];

return cn;
})
.join(" ");
}

classy.SEPARATORS = SEPARATORS;

export default classy;
76 changes: 76 additions & 0 deletions src/classy/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Classy from ".";

const mockScope = {
RootElem: "bPDD5MZYqy37J8a5Ed",
RootElem_mod: "3ptteOdB8YbvcENwoY6cnW",
"RootElem-kid": "1c9QFfxxmSn49Hghkx4GmC",
};

describe("Classy", () => {
describe("Class List Normalization", () => {
it("Concatenates variable-length arguments.", () => {
expect(Classy("test1", "test2", "test3")).toBe("test1 test2 test3");
});

it("Flattens deeply nested arrays of strings.", () => {
expect(Classy(["test1", ["test2", ["test3"]]])).toBe("test1 test2 test3");
});

it("Expands comma-separated strings.", () => {
expect(Classy("test1,test2, test3")).toBe("test1 test2 test3");
});

it("Expands dot-separated strings.", () => {
expect(Classy(".test1.test2.test3")).toBe("test1 test2 test3");
});

it("Expands space-separated strings.", () => {
expect(Classy("test1 test2 test3")).toBe("test1 test2 test3");
});
});

describe("Classy Instances", () => {
it("Can be created with the `new` keyword.", () => {
const cn = new Classy({
classes: mockScope,
});
expect(cn("RootElem")).toBe("bPDD5MZYqy37J8a5Ed");
});
});

describe("CSS Module Auto-Scoping", () => {
it("Accepts an initial hash of scoped selectors.", () => {
expect(Classy(mockScope, "RootElem")).toBe("bPDD5MZYqy37J8a5Ed");
});
});

describe("BEM Expansion", () => {
const bem = new Classy({
bem: "RootElem",
});

it("Replaces Sass-style root selectors (&) with the BEM base.", () => {
expect(bem("&")).toBe("RootElem");
});

it('BEM "-element" partials', () => {
expect(bem("-kid")).toBe("RootElem-kid");
});

it('BEM "_modifier" partials', () => {
expect(bem("_mod")).toBe("RootElem_mod");
});

it("Skips pre-scoped classes that end in `-scss`.", () => {
/** Sometimes we pass pre-scoped classes to classy. In our dev environment,
* the generated classes can start with an underscore, which causes Classy
* to attempt to prefix them with the BEM root!
* @todo: i can't actually remember what/where/why this was a problem;
* is it still happening? there's probably a better fix...
*/
const prescopedClass = "_1adDfJfhK6H-scss";
const scope = { [prescopedClass]: "PRESCOPED" };
expect(Classy(scope, prescopedClass)).toMatchSnapshot();
});
});
});
Loading

0 comments on commit 565126c

Please sign in to comment.