Skip to content
Permalink
Browse files

refactor: Updating the LCS code to respect selectors.

- Added more tests
  • Loading branch information
ramitha authored and chriseppstein committed Dec 20, 2019
1 parent 5df20f9 commit ed51f5895b1680bacf31492645125ba37a4f09ef
@@ -71,5 +71,6 @@
"volta": {
"node": "12.2.0",
"yarn": "1.21.0"
}
},
"version": "1.21.0"
}
@@ -4,7 +4,7 @@ import * as postcss from "postcss";
import * as parser from "postcss-selector-parser";

import { BemSelector, BlockClassSelector } from "./interface";
import { findLcs } from "./utils";
import { findLcsMap } from "./utils";
export declare type PostcssAny = unknown;

type BemSelectorMap = Map<string, BemSelector>;
@@ -97,21 +97,23 @@ export function constructBlocksMap(bemSelectorCache: BemSelectorMap): BemToBlock
for (let elementListMap of blockListMap.values()) {
// iterate through the elements
for (let selList of elementListMap.values()) {
let lcs: string;
let lcsMap: {[key: string]: string};
// find the longest common substring(LCS) in the list of selectors
let modifiers = selList.length && selList.filter(sel => sel.modifier !== undefined);
if (modifiers) {
if (modifiers.length > 1) {
lcs = findLcs(modifiers.map(sel => sel.modifier as string));
lcsMap = findLcsMap(modifiers.map(sel => sel.modifier as string));

// update the states and substates with the LCS
modifiers.forEach(sel => {
let blockClass = resultMap.get(sel);
let lcs = blockClass && blockClass.state && lcsMap[blockClass.state];
if (blockClass && blockClass.state && lcs) {
blockClass.subState = blockClass.state.replace(`${lcs}-`, "");
blockClass.state = lcs.replace(/-$/, "");
}
});
}
// update the states and substates with the LCS
modifiers.forEach(sel => {
let blockClass = resultMap.get(sel);
if (blockClass && lcs) {
blockClass.subState = (blockClass.state as string).replace(lcs, "");
blockClass.state = lcs.replace(/-$/, "");
}
});
}
}
}
@@ -5,17 +5,41 @@ export const R_BEM_REGEX = /^.(?:((?:[a-z0-9]+-)*[a-z0-9]+)(__(?:[a-z0-9]+-)*[a-
// regex that matches block--modifier__element pattern
export const R_BME_REGEX = /^.(?:((?:[a-z0-9]+-)*[a-z0-9]+)(--(?:[a-z0-9]+-)*[a-z0-9]+)?(__(?:[a-z0-9]+-)*[a-z0-9]+)?)$/;

const COMMON_PREFIXES_FOR_MODIFIERS = ["is"];

/**
* function to find the LCS (longest common substring) from a string array
*
*/
export function findLcs(arr1: string[]): string {
const arr = arr1.concat().sort();
const a1 = arr[0];
const a2 = arr[arr.length - 1];
const L = a1.length;
let i = 0;
while (i < L && a1.charAt(i) === a2.charAt(i)) i++;
return a1.substring(0, i);
export function findLcsMap(arr: string[]): {[key: string]: string} {
// we split the string assuming BEM conventions of "-" and then group by the
// first item in each string
let wordMap: {[key: string]: string[]} = {};
// since we're assuming BEM, we can assume that the separators on the
// modifiers are '-'
let splitArr = arr.map(item => item.split("-"));
splitArr.forEach(word => {
// Here, we key on the first item in the split array. Ideally, we should
// find the longest common separators between all the items and prompt the
// user for input
if (wordMap[word[0]]) {
wordMap[word[0]].push(word.join("-"));
} else {
wordMap[word[0]] = new Array(word.join("-"));
}
});
// return only those values who have a count of greater than 1 and whose key
// is not present in COMMON_PREFIXES_FOR_MODIFIERS
let reducedWordMap = {};
for (let [key, value] of Object.entries(wordMap)) {
if (value.length > 1 && COMMON_PREFIXES_FOR_MODIFIERS.indexOf(key) < 0) {
value.forEach(item => {
// create a reverser mapping of the string to the key
reducedWordMap[item] = key;
});
}
}
return reducedWordMap;
}

/**
@@ -71,4 +71,55 @@ describe("construct-blocks", () => {
assert.deepEqual(result.get(sel2), new BlockClassSelector({ class: "image-container", state: "inverse-black" }));
assert.deepEqual(result.get(sel3), new BlockClassSelector({ class: "image", state: "inverse-blue" }));
});

it("calculates substates correctly when some of them have a common selector, not all", async () => {
let sel1 = new BemSelector(".jobs-hero--disabled");
let sel2 = new BemSelector(".jobs-hero--size-small");
let sel3 = new BemSelector(".jobs-hero--size-large");
let sel4 = new BemSelector(".jobs-hero--tilted");

let mockMap = new Map(Object.entries({
".jobs-hero--disabled": sel1,
".jobs-hero--size-small": sel2,
".jobs-hero--size-large": sel3,
".jobs-hero--tilted": sel4,
}));

let result = constructBlocksMap(mockMap);
assert.deepEqual(result.get(sel1), new BlockClassSelector({ class: undefined, state: "disabled" }));
assert.deepEqual(result.get(sel2), new BlockClassSelector({ state: "size", subState: "small" }));
assert.deepEqual(result.get(sel3), new BlockClassSelector({ state: "size", subState: "large" }));
assert.deepEqual(result.get(sel4), new BlockClassSelector({ state: "tilted", subState: undefined }));
});

it("calculates substates correctly without a clearly defined separator at the state/substate boundary.", async () => {
let sel1 = new BemSelector(".myblock__myelement--gross");
let sel2 = new BemSelector(".myblock__myelement--great");

let mockMap = new Map(Object.entries({
".myblock__myelement--gross": sel1,
".myblock__myelement--great": sel2,

}));

let result = constructBlocksMap(mockMap);
assert.deepEqual(result.get(sel1), new BlockClassSelector({ class: "myelement", state: "gross" }));
assert.deepEqual(result.get(sel2), new BlockClassSelector({ class: "myelement", state: "great" }));
});

it("calculates substates correctly when the states have a modifier is-", async () => {
let sel1 = new BemSelector(".myblock__myelement--is-disabled");
let sel2 = new BemSelector(".myblock__myelement--is-animating");

let mockMap = new Map(Object.entries({
".myblock__myelement--is-disabled": sel1,
".myblock__myelement--is-animating": sel2,

}));

let result = constructBlocksMap(mockMap);
assert.deepEqual(result.get(sel1), new BlockClassSelector({ class: "myelement", state: "is-disabled" }));
assert.deepEqual(result.get(sel2), new BlockClassSelector({ class: "myelement", state: "is-animating" }));
});

});

0 comments on commit ed51f58

Please sign in to comment.
You can’t perform that action at this time.