UNPKG

6.48 kBJavaScriptView Raw
1const
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
12const storedAnalyzes = new Map();
13
14class 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 // This is necessary to avoid infinite loops with zero-width matches
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
248module.exports = CssAnalyzer;
\No newline at end of file