Skip to content

Commit 81aa883

Browse files
committed
Merge pull request #87 from kizu/detector
Detection of existent codestyle
2 parents 473f019 + a01bd72 commit 81aa883

34 files changed

Lines changed: 1745 additions & 33 deletions

README.md

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ csscomb -h
6161
-V, --version output the version number
6262
-v, --verbose verbose mode
6363
-c, --config [path] configuration file path
64+
-d, --detect detect mode (would return detected options)
6465
-l, --lint in case some fixes needed returns an error
6566
```
6667
@@ -137,6 +138,8 @@ var combedLESS = comb.processString(less, 'less');
137138
138139
## Configuration
139140
141+
### Through `.csscomb.json`
142+
140143
`csscomb` is configured using [.csscomb.json](https://github.com/csscomb/csscomb.js/blob/master/.csscomb.json) file, located in the project root.
141144
142145
Example configuration:
@@ -162,6 +165,66 @@ Example configuration:
162165
}
163166
```
164167
168+
### Through `.css`-template
169+
170+
Instead of configuring all the options one by one, you can use a CSS-template file instead: CSSComb.js would detect the codestyle used in this file and would use it as a config. All the existent properties except for the `sort-order` could be configured this way.
171+
172+
To provide a template just add `"template"` with the path to the template in the `.csscomb.json`:
173+
174+
```json
175+
{
176+
"template": "example.css"
177+
}
178+
```
179+
180+
CSSComb.js would detect only those things that could be detected, so if your template don't provide examples of usage for some of the options, or if you would want to override something from it, you can write them in the `.csscomb.json` along the `"template"`:
181+
182+
```json
183+
{
184+
"template": "example.css",
185+
"leading-zero": false,
186+
"vendor-prefix-align": true
187+
}
188+
```
189+
190+
This config would detect all the options from the `example.css`, then it would use `"leading-zero": false` instead of what it detected, and then it would use `"vendor-prefix-align": true` even if there were no prefixed properties or values inside the `example.css`.
191+
192+
### Creating `.csscomb.json` from the `.css` file
193+
194+
If you want to configure everything manually, but based on the codestyle from existing `.css`-file, you can at first detect all the options using `--detect` CLI option, and then add/edit any options you like. So if you have such `example.css`:
195+
196+
```css
197+
.foo
198+
{
199+
width: #fff;
200+
}
201+
```
202+
203+
then by running
204+
205+
```bash
206+
csscomb -d template.css > .csscomb.json
207+
```
208+
209+
you would generate this `.csscomb.json`:
210+
211+
```json
212+
{
213+
"remove-empty-rulesets": true,
214+
"always-semicolon": true,
215+
"color-case": "lower",
216+
"color-shorthand": true,
217+
"strip-spaces": true,
218+
"eof-newline": true,
219+
"stick-brace": "\n",
220+
"colon-space": [
221+
"",
222+
" "
223+
],
224+
"rule-indent": " "
225+
}
226+
```
227+
165228
## Options
166229
167230
### exclude
@@ -178,7 +241,7 @@ Available value: `{Boolean}` `true`
178241
179242
Config mode: `{ "verbose": true }`
180243
```bash
181-
./bin/csscomb ./test
244+
csscomb ./test
182245
183246
✓ test/integral.origin.css
184247
test/integral.expect.css
@@ -190,8 +253,22 @@ Config mode: `{ "verbose": true }`
190253
191254
CLI mode:
192255
```bash
193-
./bin/csscomb ./test --verbose
194-
./bin/csscomb ./test -v
256+
csscomb --verbose ./test
257+
csscomb -v ./test
258+
```
259+
260+
### template
261+
262+
**Note:** see the description of the [configuring through template](#through-css-template).
263+
264+
Available value: `{String}` path to the `.css` file.
265+
266+
Example: `{ "template": "example.css" }`
267+
268+
CLI mode — just provide path to `.css` file instead of `.csscomb.json`:
269+
```bash
270+
csscomb --config example.css ./test
271+
csscomb -c example.css ./test
195272
```
196273
197274
### always-semicolon

lib/cli.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ program
1414
.usage('[options] <file ...>')
1515
.option('-v, --verbose', 'verbose mode')
1616
.option('-c, --config [path]', 'configuration file path')
17+
.option('-d, --detect', 'detect mode (would return detected options)')
1718
.option('-l, --lint', 'in case some fixes needed returns an error')
1819
.parse(process.argv);
1920

@@ -23,10 +24,34 @@ if (!program.args.length) {
2324
}
2425

2526
var configPath = program.config || (process.cwd() + '/.csscomb.json');
27+
var comb = new Comb();
28+
29+
if (program.detect) {
30+
console.log(JSON.stringify(comb.detectInFile(program.args[0]), false, 4));
31+
process.exit(0);
32+
}
2633

2734
if (fs.existsSync(configPath)) {
28-
var comb = new Comb();
29-
var config = require(configPath);
35+
var config;
36+
if (configPath.match(/\.css$/)) {
37+
config = comb.detectInFile(configPath);
38+
} else {
39+
config = require(configPath);
40+
}
41+
42+
if (config.template) {
43+
if (fs.existsSync(config.template)) {
44+
var templateConfig = comb.detectInFile(config.template);
45+
for (var attrname in templateConfig) {
46+
if (!config[attrname]) {
47+
config[attrname] = templateConfig[attrname];
48+
}
49+
}
50+
} else {
51+
console.log('Template configuration file ' + config.template + ' was not found.');
52+
process.exit(1);
53+
}
54+
}
3055

3156
console.time('spent');
3257

lib/csscomb.js

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var gonzales = require('gonzales-pe');
22
var minimatch = require('minimatch');
33
var vow = require('vow');
4+
var fs = require('fs');
45
var vfs = require('vow-fs');
56
var doNothing = function() {};
67

@@ -31,6 +32,7 @@ var Comb = function() {
3132
'vendor-prefix-align'
3233
];
3334
this._exclude = null;
35+
this._detect = false;
3436
};
3537

3638
Comb.prototype = {
@@ -65,14 +67,55 @@ Comb.prototype = {
6567
this._lint = config.lint;
6668
},
6769

70+
/**
71+
* Detects the options in the given string
72+
*
73+
* @param {String} text Stylesheet
74+
* @param {Array} options List of options to detect
75+
* @returns {Object}
76+
*/
77+
detectInString: function(text, options) {
78+
var result;
79+
this._detect = true;
80+
this._detected = {};
81+
this._handlers = [];
82+
this._options.forEach(function(option) {
83+
try {
84+
var handler = require('./options/' + option);
85+
if (!handler || options && options.indexOf(option) === -1) return;
86+
87+
handler._name = option;
88+
this._detected[option] = [];
89+
this._handlers.push(handler);
90+
} catch (e) {
91+
console.warn('Error loading "%s" handler: %s', option, e.message);
92+
}
93+
}, this);
94+
95+
result = this.processString(text);
96+
this._detect = false;
97+
return result;
98+
},
99+
100+
/**
101+
* Detects the options in the given file
102+
*
103+
* @param {String} path Path to the stylesheet
104+
* @param {Array} options List of options to detect
105+
* @returns {Object}
106+
*/
107+
detectInFile: function(path, options) {
108+
var stylesheet = fs.readFileSync(path, 'utf8');
109+
return this.detectInString(stylesheet, options);
110+
},
111+
68112
/**
69113
* Processes stylesheet tree node.
70114
*
71115
* @param {Array} tree Parsed tree
72116
* @returns {Array}
73117
*/
74118
processTree: function(tree) {
75-
76119
// We walk across complete tree for each handler,
77120
// because we need strictly maintain order in which handlers work,
78121
// despite fact that handlers work on different level of the tree.
@@ -92,7 +135,16 @@ Comb.prototype = {
92135
if (!Array.isArray(node)) return;
93136

94137
var nodeType = node.shift();
95-
handler.process(nodeType, node, level);
138+
if (this._detect) {
139+
if (handler.detect) {
140+
var detected = handler.detect(nodeType, node, level);
141+
if (detected !== undefined) {
142+
this._detected[handler._name].push(detected);
143+
}
144+
}
145+
} else {
146+
handler.process(nodeType, node, level);
147+
}
96148
node.unshift(nodeType);
97149

98150
if (nodeType === 'atrulers') level++;
@@ -120,7 +172,11 @@ Comb.prototype = {
120172
throw new Error('Undefined tree at ' + filename + ': ' + string(text) + ' => ' + string(tree));
121173
}
122174
tree = this.processTree(tree);
123-
return gonzales.astToCSS({ syntax: syntax, ast: tree });
175+
if (this._detect) {
176+
return this._getDetectedOptions(this._detected);
177+
} else {
178+
return gonzales.astToCSS({ syntax: syntax, ast: tree });
179+
}
124180
},
125181

126182
/**
@@ -238,6 +294,59 @@ Comb.prototype = {
238294
}
239295

240296
return this._shouldProcess(path);
297+
},
298+
299+
/**
300+
* Gets the detected options.
301+
*
302+
* @param {Object} detected
303+
* @returns {Object}
304+
*/
305+
_getDetectedOptions: function(detected) {
306+
var options = {};
307+
Object.keys(detected).forEach(function(option) {
308+
// List of all the detected variants from the stylesheet for the given option:
309+
var values = detected[option];
310+
var i;
311+
if (values.length) {
312+
if (values.length === 1) {
313+
options[option] = values[0];
314+
} else {
315+
// If there are more than one value for the option, find the most popular one;
316+
// `variants` would be populated with the popularity for different values.
317+
var variants = {};
318+
var bestGuess = null;
319+
var maximum = 0;
320+
for (i = values.length; i--;) {
321+
var currentValue = values[i];
322+
// Count the current value:
323+
if (variants[currentValue]) {
324+
variants[currentValue]++;
325+
} else {
326+
variants[currentValue] = 1;
327+
}
328+
// If the current variant is the most popular one, treat it as the best guess:
329+
if (variants[currentValue] >= maximum) {
330+
maximum = variants[currentValue];
331+
bestGuess = currentValue;
332+
}
333+
}
334+
if (bestGuess !== null) {
335+
options[option] = bestGuess;
336+
}
337+
}
338+
} else {
339+
// If there are no values for the option, check if there is a default one:
340+
for (i = this._handlers.length; i--;) {
341+
if (this._handlers[i]._name === option && this._handlers[i]._detectDefault !== undefined) {
342+
options[option] = this._handlers[i]._detectDefault;
343+
break;
344+
}
345+
}
346+
}
347+
}, this);
348+
349+
return options;
241350
}
242351
};
243352

lib/options/always-semicolon.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,23 @@ module.exports = {
6969
break;
7070
}
7171
}
72+
},
73+
74+
/**
75+
* Detects the value of an option at the tree node.
76+
*
77+
* @param {String} nodeType
78+
* @param {node} node
79+
*/
80+
detect: function(nodeType, node) {
81+
if (nodeType === 'block') {
82+
for (var i = node.length; i--;) {
83+
var nodeItem = node[i];
84+
var type = nodeItem[0];
85+
if (type === 'declDelim') return true;
86+
87+
if (type === 'declaration') return false;
88+
}
89+
}
7290
}
7391
};

lib/options/block-indent.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,32 @@ module.exports = {
6969
space[1] += new Array(level + 1).join(this._value);
7070
}
7171
}
72+
},
73+
74+
/**
75+
* Detects the value of an option at the tree node.
76+
*
77+
* @param {String} nodeType
78+
* @param {node} node
79+
*/
80+
detect: function(nodeType, node, level) {
81+
var result = null;
82+
if (nodeType === 'atrulers' || nodeType === 'block') {
83+
if (node[node.length - 1][0] === 's' && level > 0) {
84+
result = node[node.length - 1][1].replace(/\s*\n/g, '');
85+
86+
if (this._prev !== undefined && this._prev[0] < level) {
87+
result = result.replace(result.replace(this._prev[1], ''), '');
88+
}
89+
if (this._prev === undefined || this._prev[0] !== level) {
90+
this._prev = [level, result];
91+
}
92+
}
93+
}
94+
95+
if (result !== null) {
96+
return result;
97+
}
7298
}
7399

74100
};

0 commit comments

Comments
 (0)