Skip to content

Commit 13ece20

Browse files
committed
Inline CSS modules in HTML
1 parent 1253b1d commit 13ece20

File tree

10 files changed

+334
-0
lines changed

10 files changed

+334
-0
lines changed

.eslintrc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"parser": "babel-eslint",
3+
"rules": {
4+
"indent": [2, 4, {"SwitchCase": 1}],
5+
"quotes": [2, "single"],
6+
"linebreak-style": [2, "unix"],
7+
"semi": [2, "always"],
8+
"camelcase": [2, {"properties": "always"}],
9+
"brace-style": [2, "1tbs", {"allowSingleLine": true}]
10+
},
11+
"env": {
12+
"es6": true,
13+
"node": true,
14+
"browser": false,
15+
"mocha": true
16+
},
17+
"extends": "eslint:recommended",
18+
"ecmaFeatures": {
19+
"experimentalObjectRestSpread": false
20+
}
21+
}

.gitignore

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Source: https://github.com/github/gitignore/blob/master/Node.gitignore
2+
3+
# Logs
4+
logs
5+
*.log
6+
npm-debug.log*
7+
8+
# Runtime data
9+
pids
10+
*.pid
11+
*.seed
12+
13+
# Directory for instrumented libs generated by jscoverage/JSCover
14+
lib-cov
15+
16+
# Coverage directory used by tools like istanbul
17+
coverage
18+
19+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20+
.grunt
21+
22+
# node-waf configuration
23+
.lock-wscript
24+
25+
# Compiled binary addons (http://nodejs.org/api/addons.html)
26+
build/Release
27+
28+
# Dependency directory
29+
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
30+
node_modules
31+
32+
# Optional npm cache directory
33+
.npm
34+
35+
# Optional REPL history
36+
.node_repl_history
37+
38+
39+
40+
# Custom
41+
/lib/*.js

.npmignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.gitignore
2+
.eslintrc
3+
.travis.yml
4+
node_modules/
5+
npm-debug.log
6+
/lib/*.es6
7+
/test/

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
sudo: false
2+
language: node_js
3+
node_js:
4+
- stable
5+
- "0.12"

README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# PostHTML CSS Modules
2+
[![npm version](https://badge.fury.io/js/posthtml-css-modules.svg)](http://badge.fury.io/js/posthtml-css-modules)
3+
[![Build Status](https://travis-ci.org/maltsev/posthtml-css-modules.svg?branch=master)](https://travis-ci.org/maltsev/posthtml-css-modules)
4+
5+
[PostHTML](https://github.com/posthtml/posthtml) plugin that inlines [CSS modules](https://github.com/css-modules/css-modules) in HTML.
6+
7+
8+
## Usage
9+
I suggest to use [postcss-modules](https://github.com/outpunk/postcss-modules) to generate CSS modules.
10+
11+
### Global file
12+
Let's say we have `cssClasses.json` with all CSS modules inside:
13+
```json
14+
{
15+
"title": "_title_116zl_1 _heading_9dkf",
16+
"profile": {
17+
"user": "_profile_user_f93j"
18+
}
19+
}
20+
```
21+
22+
Now we can inline these CSS modules in our HTML:
23+
```js
24+
var posthtml = require('posthtml');
25+
26+
posthtml([require('posthtml-css-modules')('./cssClasses.json')])
27+
.process(
28+
'<h1 css-module="title">My profile</h1>' +
29+
// You can also use nested modules
30+
'<div css-module="profile.user">John</div>'
31+
)
32+
.then(function (result) {
33+
console.log(result.html);
34+
});
35+
36+
// <h1 class="_title_116zl_1 _heading_9dkf">My profile</h1>
37+
// <div class="_profile_user_f93j">John</div>
38+
```
39+
40+
### Directory with several files
41+
CSS modules could be also separated in a several files.
42+
For example, `profile.js` and `article.js` inside directory `cssModules/`:
43+
```js
44+
// profile.js
45+
module.exports = {
46+
user: '_profile_user_f93j'
47+
}
48+
```
49+
50+
```js
51+
// article.js
52+
module.exports = {
53+
title: '_article__tile _heading'
54+
}
55+
```
56+
You can use both JS and JSON for declaration, as long as the file could be required via `require()`.
57+
58+
```js
59+
var posthtml = require('posthtml');
60+
61+
posthtml([require('posthtml-css-modules')('./cssModules/')])
62+
.process(
63+
'<div class="baseWrapper" css-module="profile.user">John</div>' +
64+
'<h2 css-module="article.title"></h2>'
65+
)
66+
.then(function (result) {
67+
console.log(result.html);
68+
});
69+
70+
// <div class="baseWrapper _profile_user_f93j">John</div>
71+
// <h2 class="_article__tile _heading"></h2>
72+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./lib/cssModules').default;

lib/cssModules.es6

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import _get from 'lodash.get';
4+
import parseAttrs from 'posthtml-attrs-parser';
5+
6+
7+
export default (cssModulesPath) => {
8+
return function cssModules(tree) {
9+
tree.match({attrs: {'css-module': /\w+/}}, node => {
10+
const attrs = parseAttrs(node.attrs);
11+
const cssModuleName = attrs['css-module'];
12+
delete attrs['css-module'];
13+
14+
attrs.class = attrs.class || [];
15+
attrs.class.push(getCssClassName(cssModulesPath, cssModuleName));
16+
node.attrs = attrs.compose();
17+
18+
return node;
19+
});
20+
};
21+
};
22+
23+
24+
function getCssClassName(cssModulesPath, cssModuleName) {
25+
if (fs.lstatSync(cssModulesPath).isDirectory()) {
26+
let cssModulesDir = cssModulesPath;
27+
let cssModuleNameParts = cssModuleName.split('.');
28+
let cssModulesFile = cssModuleNameParts.shift();
29+
cssModuleName = cssModuleNameParts.join('.');
30+
cssModulesPath = path.join(cssModulesDir, cssModulesFile);
31+
}
32+
33+
const cssModules = require(path.resolve(cssModulesPath));
34+
const cssClassName = _get(cssModules, cssModuleName);
35+
if (! cssClassName) {
36+
throw getError('CSS module "' + cssModuleName + '" is not found');
37+
} else if (typeof cssClassName !== 'string') {
38+
throw getError('CSS module "' + cssModuleName + '" is not a string');
39+
}
40+
41+
return cssClassName;
42+
}
43+
44+
45+
function getError(message) {
46+
const fullMessage = '[posthtml-css-modules] ' + message;
47+
return new Error(fullMessage);
48+
}

package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "posthtml-css-modules",
3+
"version": "0.1.0",
4+
"description": "Use CSS modules in HTML",
5+
"main": "index.js",
6+
"author": "Kirill Maltsev <[email protected]>",
7+
"license": "MIT",
8+
"scripts": {
9+
"compile": "rm -f lib/*.js && node_modules/.bin/babel -d lib/ lib/",
10+
"lint": "node_modules/.bin/eslint *.js lib/*.es6 test/",
11+
"pretest": "npm run lint && npm run compile",
12+
"test": "node_modules/.bin/_mocha --compilers js:babel-core/register --recursive --check-leaks",
13+
"prepublish": "npm run compile"
14+
},
15+
"keywords": [
16+
"posthtml",
17+
"posthtml-plugin",
18+
"html",
19+
"postproccessor",
20+
"css",
21+
"css-modules"
22+
],
23+
"babel": {
24+
"presets": [
25+
"es2015"
26+
]
27+
},
28+
"dependencies": {
29+
"lodash.get": "^4.0.2",
30+
"posthtml-attrs-parser": "^0.1.1"
31+
},
32+
"devDependencies": {
33+
"babel-cli": "^6.4.5",
34+
"babel-core": "^6.4.5",
35+
"babel-eslint": "^4.1.8",
36+
"babel-preset-es2015": "^6.3.13",
37+
"eslint": "^1.10.3",
38+
"expect": "^1.14.0",
39+
"mocha": "^2.4.5",
40+
"posthtml": "^0.8.2"
41+
},
42+
"repository": {
43+
"type": "git",
44+
"url": "git://github.com/maltsev/posthtml-css-modules.git"
45+
},
46+
"bugs": {
47+
"url": "https://github.com/maltsev/posthtml-css-modules/issues"
48+
},
49+
"homepage": "https://github.com/maltsev/posthtml-css-modules"
50+
}

test/classes.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"title": "__title __heading",
3+
"user": {
4+
"profile": {
5+
"photo": "__user__profile__photo"
6+
}
7+
}
8+
}

test/cssModules.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import path from 'path';
2+
import expect from 'expect';
3+
import posthtml from 'posthtml';
4+
import cssModules from '..';
5+
6+
const classesPath = path.join(__dirname, 'classes.json');
7+
const classesDir = path.dirname(classesPath);
8+
9+
10+
describe('posthtml-css-modules', () => {
11+
it('should inline CSS module from the file (flat config)', () => {
12+
return init(
13+
'<div class="foob" css-module="title"></div>',
14+
'<div class="foob __title __heading"></div>',
15+
classesPath
16+
);
17+
});
18+
19+
20+
it('should inline CSS module from the file (deep config)', () => {
21+
return init(
22+
'<div css-module="user.profile.photo"></div>',
23+
'<div class="__user__profile__photo"></div>',
24+
classesPath
25+
);
26+
});
27+
28+
29+
it('should inline CSS module from the directory', () => {
30+
return init(
31+
'<div css-module="classes.title"></div>',
32+
'<div class="__title __heading"></div>',
33+
classesDir
34+
);
35+
});
36+
37+
38+
it('should throw an error if the file with the CSS modules is not found', () => {
39+
return init(
40+
'<div></div>',
41+
'<div></div>',
42+
classesPath
43+
).catch(error => {
44+
expect(error.message)
45+
.toInclude('Cannot find module')
46+
.toInclude('config/notExists.json');
47+
});
48+
});
49+
50+
51+
it('should throw an error if the CSS module is not found in the file', () => {
52+
return init(
53+
'<div css-module="notExists"></div>',
54+
'<div css-module="notExists"></div>',
55+
classesPath
56+
).catch(error => {
57+
expect(error.message)
58+
.toInclude('[posthtml-css-modules] CSS module "notExists" is not found');
59+
});
60+
});
61+
62+
63+
it('should throw an error if the file with the CSS module is not found in the directory', () => {
64+
return init(
65+
'<div css-module="notExists"></div>',
66+
'<div css-module="notExists"></div>',
67+
classesDir
68+
).catch(error => {
69+
expect(error.message)
70+
.toInclude('Cannot find module')
71+
.toInclude('test/notExists');
72+
});
73+
});
74+
});
75+
76+
77+
function init(html, expectedHtml, options) {
78+
return posthtml([cssModules(options)]).process(html).then(result => {
79+
expect(result.html).toBe(expectedHtml);
80+
});
81+
}

0 commit comments

Comments
 (0)