Skip to content
Permalink
Browse files

feat: Creating a new package for bem to css-blocks conversion.

- This creates a new npm package for converting BEM files to block files
- It also contains changes to the CLI that adds a convert method
- It iterates through the .css file, converting each of the classNames to comply with CSS blocks and rewrite the same file.
- The same should work for .scss files

TODO
- Solve for when there is more than one block in a single file
- Add more tests around the plugin
  • Loading branch information
ramitha authored and chriseppstein committed Dec 13, 2019
1 parent 38601cb commit d62b2042423d822c3b09526b145a354c4d7e6bd2
@@ -48,6 +48,9 @@
},
{
"path": "packages/@css-blocks/config"
},
{
"path": "packages/@css-blocks/bem-to-blocks"
}
],
"settings": {
@@ -0,0 +1,103 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [1.0.0-alpha.1](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2019-12-10)

**Note:** Version bump only for package @css-blocks/code-style





# [0.24.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.23.2...v0.24.0) (2019-09-16)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.23.0"></a>
# [0.23.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.22.0...v0.23.0) (2019-05-08)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.22.0"></a>
# [0.22.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.21.0...v0.22.0) (2019-05-02)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.21.0"></a>
# [0.21.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0...v0.21.0) (2019-04-07)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.20.0"></a>
# [0.20.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.8...v0.20.0) (2019-03-11)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.20.0-beta.7"></a>
# [0.20.0-beta.7](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.5...v0.20.0-beta.7) (2019-02-01)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.20.0-beta.6"></a>
# [0.20.0-beta.6](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.5...v0.20.0-beta.6) (2019-02-01)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.20.0-beta.5"></a>
# [0.20.0-beta.5](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.4...v0.20.0-beta.5) (2019-01-08)

**Note:** Version bump only for package @css-blocks/code-style





<a name="0.20.0-beta.4"></a>
# [0.20.0-beta.4](https://github.com/linkedin/css-blocks/compare/v0.20.0-beta.3...v0.20.0-beta.4) (2018-10-19)


### Features

* Manually throw error for Node 6 in Analyzer. ([5788fcc](https://github.com/linkedin/css-blocks/commit/5788fcc))





<a name="0.18.0"></a>
# [0.18.0](https://github.com/linkedin/css-blocks/compare/0.15.1...0.18.0) (2018-04-24)


### Features

* Enable root-level typedoc generation for the project. ([59c85a3](https://github.com/linkedin/css-blocks/commit/59c85a3))
@@ -0,0 +1,2 @@
# BEM to CSS Blocks

@@ -0,0 +1,4 @@
{
"$schema": "http://json.schemastore.org/tslint",
"extends": "@opticss/code-style/configs/tslint.cli.json"
}
@@ -0,0 +1,4 @@
{
"$schema": "http://json.schemastore.org/tslint",
"extends": "@opticss/code-style/configs/tslint.interactive.json"
}
@@ -0,0 +1,4 @@
{
"$schema": "http://json.schemastore.org/tslint",
"extends": "@opticss/code-style/configs/tslint.release.json"
}
@@ -0,0 +1,55 @@
{
"name": "@css-blocks/bem-to-blocks",
"author": "Ramitha Chitloor",
"description": "Tools to convert BEM files to CSS block files.",
"license": "BSD-2-Clause",
"version": "1.0.0-alpha.4",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"files": [
"bin",
"dist",
"src",
"README.md",
"CHANGELOG.md"
],
"readme": "README.md",
"keywords": [
"tslint",
"tslint-plugin"
],
"scripts": {
"test": "yarn run test:runner",
"test:runner": "mocha --opts test/mocha.opts dist/test",
"compile": "tsc --build",
"pretest": "yarn run compile",
"posttest": "yarn run lint",
"prepublish": "rm -rf dist && yarn run compile && yarn run lintall",
"lint": "tslint -t msbuild --project . -c tslint.cli.json",
"lintall": "tslint -t msbuild --project . -c tslint.release.json",
"lintfix": "tslint -t msbuild --project . -c tslint.cli.json --fix",
"coverage": "istanbul cover -i dist/src/**/*.js --dir ./build/coverage node_modules/mocha/bin/_mocha -- dist/test --opts test/mocha.opts",
"remap": "remap-istanbul -i build/coverage/coverage.json -o coverage -t html",
"watch": "watch 'yarn run test' src test --wait=1"
},
"bugs": {
"url": "https://github.com/linkedin/css-blocks/issues"
},
"repository": "https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/bem-to-blocks",
"homepage": "https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/bem-to-blocks#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@css-blocks/code-style": "^1.0.0-alpha.1",
"opticss": "^0.7.0",
"postcss": "^7.0.14"
},
"engines": {
"node": "6.* || 8.* || >= 10.*"
},
"volta": {
"node": "12.2.0",
"yarn": "1.21.0"
}
}
@@ -0,0 +1,189 @@
import * as fs from "fs-extra";
import { ParsedSelector, SelectorCache } from "opticss";
import * as postcss from "postcss";

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

type BemSelectorMap = Map<string, BemSelector>;
type ElementToBemSelectorMap = Map<string, BemSelector[]>;
type BlockToBemSelectorMap = Map<string, ElementToBemSelectorMap>;
type BemToBlockClassMap = WeakMap<BemSelector, BlockClassSelector>;

const EMPTY_ELEMENT_PLACEHOLDER = "EMPTY-ELEMENT-PLACEHOLDER";

export function convertBemToBlocks(files: Array<string>): Promise<void>[] {
let promises: Promise<void>[] = [];
files.forEach(file => {
fs.readFile(file, (_err, css) => {
let output = postcss([bemToBlocksPlugin])
.process(css, { from: file });
// rewrite the file with the processed output
promises.push(fs.writeFile(file, output.toString()));
});
});
return promises;
}

/**
* Iterates through a cache of bemSelectors and returns a map of bemSelector to
* the blockClassName. This function optimises the states and subStates from the
* name of the modifier present in the BEM selector
* @param bemSelectorCache weakmap - BemSelectorMap
BemSelector {
block: 'jobs-hero',
element: 'image-container',
modifier: undefined }
=>
BlockClassName {
class: // name of the element if present. If this is not present, then it is on the :scope
state: // name of the modifiers HCF
subState: // null if HCF is null
}, // written to a file with blockname.block.css
*/
export function constructBlocksMap(bemSelectorCache: BemSelectorMap): BemToBlockClassMap {
let blockListMap: BlockToBemSelectorMap = new Map();
let resultMap: BemToBlockClassMap = new WeakMap();

// create the resultMap and the blockListMap
for (let bemSelector of bemSelectorCache.values()) {
// create the new blockClass instance
let blockClass: BlockClassSelector = new BlockClassSelector();
if (bemSelector.element) {
blockClass.class = bemSelector.element;
}
if (bemSelector.modifier) {
blockClass.state = bemSelector.modifier;
}
// add this blockClass to the resultMap
resultMap.set(bemSelector, blockClass);

// add this selector to the blockList based on the block, and then the
// element value
let block = blockListMap.get(bemSelector.block);
if (block) {
if (bemSelector.element) {
if (block.has(bemSelector.element)) {
(block.get(bemSelector.element) as BemSelector[]).push(bemSelector);
} else {
block.set(bemSelector.element, new Array(bemSelector));
}
} else {
// the modifier is on the block itself
if (block.has(EMPTY_ELEMENT_PLACEHOLDER)) {
(block.get(EMPTY_ELEMENT_PLACEHOLDER) as BemSelector[]).push(bemSelector);
} else {
block.set(EMPTY_ELEMENT_PLACEHOLDER, new Array(bemSelector));
}
}
} else {
// if there is no existing block, create the elementMap and the add it to
// the blockMap
let elementListMap = new Map();
if (bemSelector.element) {
elementListMap.set(bemSelector.element, new Array(bemSelector));
} else {
elementListMap.set(EMPTY_ELEMENT_PLACEHOLDER, new Array(bemSelector));
}
blockListMap.set(bemSelector.block, elementListMap);
}
}

// optimize the blocks for sub-states, iterate through the blocks
for (let elementListMap of blockListMap.values()) {
// iterate through the elements
for (let selList of elementListMap.values()) {
let lcs: 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));
}
// 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(/-$/, "");
}
});
}
}
}
// TODO: detect if there is a scope node, if not create a new empty scope node
return resultMap;
}

/**
* PostCSS plugin for transforming BEM to CSS blocks
*/
export const bemToBlocksPlugin: postcss.Plugin<PostcssAny> = postcss.plugin("bem-to-blocks-plugin", (options) => {
options = options || {};

return (root, result) => {
const cache = new SelectorCache();
const bemSelectorCache: BemSelectorMap = new Map();

// in this pass, we collect all the selectors
root.walkRules(rule => {
let parsedSelList = cache.getParsedSelectors(rule);
parsedSelList.forEach(parsedSel => {
parsedSel.eachSelectorNode(node => {
if (node.value) {
let bemSelector = new BemSelector(node.value);
if (bemSelector) {
// add it to the cache so it's available for the next pass
bemSelectorCache.set(node.value, bemSelector);
} else {
console.error(`${parsedSel} does not comply with BEM standards. Consider a refactor`);
}
}
});
});
});

// convert selectors to block selectors
let bemToBlockClassMap: BemToBlockClassMap = constructBlocksMap(bemSelectorCache);

// rewrite into a CSS block
root.walkRules(rule => {
// iterate through each rule
let parsedSelList = cache.getParsedSelectors(rule);
let modifiedSelList: ParsedSelector[] = new Array();
parsedSelList.forEach(sel => {
// this contains the selector combinators
let modifiedCompoundSelector = sel.clone();

modifiedCompoundSelector.eachSelectorNode(node => {
// we only need to modify class names. We can ignore everything else,
// like existing attributes, pseudo selectors, comments, imports,
// exports, etc
if (node.type === "class" && node.value) {
let bemSelector = bemSelectorCache.get(node.value);
// get the block class from the bemSelector
let blockClassName = bemSelector && bemToBlockClassMap.get(bemSelector);

// if the selector was previously cached
if (blockClassName) {
// we need to use the below method instead of node.value as the
// attributes brackets get escaped when doing node.value = blockClassName.toString()
node.setPropertyWithoutEscape("value", blockClassName.toString());
}
}
});

modifiedSelList.push(modifiedCompoundSelector);
});

// if the selector nodes were modified, then create a new rule for it
if (modifiedSelList.toString()) {
let newRule = rule.clone();
newRule.selector = modifiedSelList.toString();
rule.replaceWith(newRule);
}
});
result.root = root;
};
});

0 comments on commit d62b204

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