1 | const
|
2 | cssstats = require('cssstats'),
|
3 | extend = require('extend'),
|
4 | css = require('css'),
|
5 | cssShorthandExpand = require('css-shorthand-expand'),
|
6 | tinycolor = require('tinycolor2'),
|
7 | ntc = require('ntc'),
|
8 | JSON5 = require('json5'),
|
9 | sgUtil = require('../util'),
|
10 | Analyzer = require('./Analyzer');
|
11 |
|
12 | const storedAnalyzes = new Map();
|
13 |
|
14 | class CssAnalyzer extends Analyzer {
|
15 |
|
16 | analyze() {
|
17 |
|
18 | const
|
19 | {getFileContents, extend, convertShorthands, unifyHexValues, getFontFaces} = this,
|
20 | cssString = getFileContents.call(this).replace(/\s{2,}/g, ' ');
|
21 |
|
22 | let stats = cssstats(cssString);
|
23 | stats = extend.call(this, stats, cssString);
|
24 | stats = convertShorthands.call(this, stats);
|
25 | stats = unifyHexValues.call(this, stats);
|
26 | stats = getFontFaces.call(this, stats, cssString);
|
27 |
|
28 | return stats;
|
29 | }
|
30 |
|
31 | extend(stats, cssString) {
|
32 | const {
|
33 | flatten, uniqueCount, excludeValues, getUniqueValues,
|
34 | getUniqueMediaQueries, appendColorNames, getSortByKey, getCommentData
|
35 | } = this;
|
36 |
|
37 | stats.declarations.getUniqueValues = getUniqueValues.bind({
|
38 | stats,
|
39 | flatten,
|
40 | uniqueCount,
|
41 | excludeValues,
|
42 | appendColorNames,
|
43 | getSortByKey
|
44 | });
|
45 |
|
46 | stats.mediaQueries.getUniqueMediaQueries = getUniqueMediaQueries.bind({
|
47 | stats,
|
48 | uniqueCount,
|
49 | getSortByKey
|
50 | });
|
51 |
|
52 | stats.getCommentData = getCommentData.bind({
|
53 | cssString
|
54 | });
|
55 |
|
56 |
|
57 | return stats;
|
58 | }
|
59 |
|
60 | getFontFaces(stats, cssString) {
|
61 | const matches = cssString.match(/@font-face\s?\{[^\}]+}/gi);
|
62 |
|
63 | stats.fontFaces = {
|
64 | total: 0,
|
65 | values: []
|
66 | };
|
67 |
|
68 | if (matches === null) {
|
69 | return stats;
|
70 | }
|
71 |
|
72 | matches.map((match) => {
|
73 | const
|
74 | {stylesheet} = css.parse(match),
|
75 | fontFace = {};
|
76 | if (Array.isArray(stylesheet.rules)) {
|
77 | const [rule] = stylesheet.rules;
|
78 |
|
79 | rule.declarations.map((declaration) => {
|
80 | const {property, value} = declaration;
|
81 | if (property !== 'src') {
|
82 | fontFace[property] = value;
|
83 | }
|
84 | });
|
85 | stats.fontFaces.total++;
|
86 | stats.fontFaces.values.push(fontFace);
|
87 | }
|
88 | });
|
89 |
|
90 | stats.fontFaces.values.sort(this.getSortByKey('font-family', 'font-weight'));
|
91 | return stats;
|
92 | }
|
93 |
|
94 | unifyHexValues(stats) {
|
95 | const {properties} = stats.declarations;
|
96 |
|
97 | this.getColorProperties()
|
98 | .filter(this.removeEmpty.bind(properties))
|
99 | .map((propertyName) => {
|
100 | properties[propertyName].map((value, key) => {
|
101 | value = value.trim().toLowerCase();
|
102 | if (value.match(/^#[a-z0-9]{3}$/) !== null) {
|
103 | value = [...value].map((char) => char === '#' ? char : char + char).join('');
|
104 | }
|
105 | properties[propertyName][key] = value;
|
106 | });
|
107 | });
|
108 | return stats;
|
109 | }
|
110 |
|
111 | convertShorthands(stats) {
|
112 | const {properties} = stats.declarations;
|
113 |
|
114 | this.getShorthandProperties()
|
115 | .filter(this.removeEmpty.bind(properties))
|
116 | .map((propertyName) => {
|
117 |
|
118 | properties[propertyName].map((value) => {
|
119 | const expanded = cssShorthandExpand(propertyName, value);
|
120 |
|
121 | if (expanded !== undefined) {
|
122 | Object.keys(expanded).map((key) => {
|
123 | if (properties[key] === undefined) properties[key] = [];
|
124 | properties[key].push(expanded[key]);
|
125 | });
|
126 | }
|
127 | });
|
128 | });
|
129 | return stats;
|
130 | }
|
131 |
|
132 | getUniqueValues(propertyNames) {
|
133 | const {properties} = this.stats.declarations;
|
134 |
|
135 | if (typeof propertyNames === 'string') {
|
136 | propertyNames = [propertyNames];
|
137 | }
|
138 |
|
139 | return propertyNames
|
140 | .map((propertyName) => properties[propertyName])
|
141 | .reduce(this.flatten, [])
|
142 | .filter(this.excludeValues)
|
143 | .reduce(this.uniqueCount, [])
|
144 | .map(this.appendColorNames)
|
145 | .sort(this.getSortByKey('total'));
|
146 | }
|
147 |
|
148 | getUniqueMediaQueries() {
|
149 | const {values} = this.stats.mediaQueries;
|
150 |
|
151 | return values
|
152 | .reduce(this.uniqueCount, [])
|
153 | .sort(this.getSortByKey('total'));
|
154 | }
|
155 |
|
156 | unique(value, index, array) {
|
157 | return array.indexOf(value) === index;
|
158 | }
|
159 |
|
160 | appendColorNames(value) {
|
161 | const color = tinycolor(value.value);
|
162 | if (color.isValid()) {
|
163 | const [hex, name] = ntc.name(color.toHexString());
|
164 | value.name = name;
|
165 |
|
166 | if (hex.toLowerCase() !== color.toHexString()) {
|
167 | value.suffix = 'approx.';
|
168 | }
|
169 |
|
170 | if (color.getAlpha() < 1) {
|
171 | value.alpha = `(${color.getAlpha() * 100}%)`;
|
172 | }
|
173 |
|
174 | value.textColor = color.getAlpha() > 0.4 && color.setAlpha(1).getLuminance() < 0.4 ? '#fff' : 'inherit';
|
175 |
|
176 | }
|
177 | return value;
|
178 | }
|
179 |
|
180 | uniqueCount(array, value, index, filterArray) {
|
181 | if (filterArray.indexOf(value) === index) {
|
182 | const total = filterArray.filter(_value => _value.toLowerCase() === value.toLowerCase()).length;
|
183 | array.push({value, total});
|
184 | }
|
185 | return array;
|
186 | }
|
187 |
|
188 | flatten(array, value = []) {
|
189 | return array.concat(value);
|
190 | }
|
191 |
|
192 | excludeValues(value) {
|
193 | return ['inherit', 'none'].indexOf(value) === -1;
|
194 | }
|
195 |
|
196 | removeEmpty(key) {
|
197 | return undefined !== this[key]
|
198 | }
|
199 |
|
200 | getSortByKey(key1, key2 = 'value') {
|
201 | return (a, b) => a[key1] === b[key1] ? a[key2] < b[key2] ? -1 : 1 : b[key1] - a[key1];
|
202 | }
|
203 |
|
204 | getColorProperties() {
|
205 | return [
|
206 | 'color', 'background-color',
|
207 | 'border-top-color', 'border-right-color',
|
208 | 'border-bottom-color', 'border-left-color',
|
209 | 'fill', 'stroke']
|
210 | }
|
211 |
|
212 | getShorthandProperties() {
|
213 | return [
|
214 | 'background', 'font', 'padding', 'margin', 'border', 'border-width',
|
215 | 'border-style', 'border-color', 'border-top', 'border-right', 'border-bottom',
|
216 | 'border-left', 'outline'
|
217 | ]
|
218 | }
|
219 |
|
220 | getCommentData(selector) {
|
221 |
|
222 | const regex = new RegExp(`(${selector})([^\{\:\.\s]+(,?)[^\\{]*| +)\{[^\\}]*\/\\*JSON5(.+)(?=\\*\\/)`, 'gm');
|
223 | let output = {}, matches, parsed;
|
224 |
|
225 | while ((matches = regex.exec(this.cssString)) !== null) {
|
226 |
|
227 | if (matches.index === regex.lastIndex) {
|
228 | regex.lastIndex++;
|
229 | }
|
230 |
|
231 | const [, , valdiation, , comment] = matches;
|
232 |
|
233 | if (valdiation.trim() === '') {
|
234 | parsed = {};
|
235 | try {
|
236 | parsed = JSON5.parse(comment.trim());
|
237 | } catch (err) {
|
238 | sgUtil.log(`Unable to parse comment '${comment.trim()}'`, 'notice');
|
239 | sgUtil.log(err, 'notice');
|
240 | }
|
241 | output = extend(true, output, parsed);
|
242 | }
|
243 | }
|
244 | return output;
|
245 | }
|
246 | }
|
247 |
|
248 | module.exports = CssAnalyzer; |
\ | No newline at end of file |