UNPKG

9.88 kBJavaScriptView Raw
1/*
2 * NOTE that most of this code is from grunt-contrib-less. PLEASE USE
3 * THAT PROJECT IF YOU REQUIRE SOMETHING STABLE AND RELIABLE. This
4 * project is focused on testing experimental features, some of which
5 * may be removed in the future.
6 *
7 *
8 * grunt-contrib-less
9 * http://gruntjs.com/
10 * Copyright (c) 2013 Tyler Kellen, contributors
11 * Licensed under the MIT license.
12 *
13 *
14 * assemble-less
15 * http://github.com/assemble/assemble-less
16 * Copyright (c) 2013 Jon Schlinkert, Brian Woodward, contributors
17 * Licensed under the MIT license.
18 */
19
20
21'use strict';
22
23// Node.js
24var path = require('path');
25
26// node_modules
27var async = require('async');
28var _ = require('lodash');
29
30
31module.exports = function(grunt) {
32
33 var contrib = require('grunt-lib-contrib').init(grunt);
34
35 // Internal libs
36 var utils = require('./lib/utils');
37 var comment = require('./lib/comment').init(grunt);
38
39 var less = false;
40 var lessOptions = {
41 parse: [
42 'dumpLineNumbers',
43 'filename',
44 'optimization',
45 'paths',
46 'relativeUrls',
47 'rootpath',
48 'strictImports',
49 'syncImport'
50 ],
51 render: [
52 'cleancss',
53 'compress',
54 'ieCompat',
55 'outputSourceFiles',
56 'sourceMap',
57 'sourceMapBasepath',
58 'sourceMapFilename',
59 'sourceMapRootpath',
60 'sourceMapURL',
61 'strictMath',
62 'strictUnits'
63 ]
64 };
65
66 grunt.registerMultiTask('less', 'Compile LESS files to CSS, with experimental features.', function() {
67 var done = this.async();
68
69 // Task options.
70 var options = this.options({
71 banner: '',
72 imports: {},
73 mergeMetadata: true,
74 metadata: [],
75 process: true,
76 stripBanners: false,
77 version: 'less'
78 });
79
80 // Less.js defaults.
81 var defaults = {
82 globalVariables: '',
83 modifyVariables: '',
84 processImports: true,
85 strictMath: false,
86 strictUnits: false,
87 verbose: true
88 };
89
90 // By default, metadata at the task and target levels is merged.
91 // Set `mergeMetadata` to false if you do not want metadata to be merged.
92 if (options.mergeMetadata !== false) {
93 options.metadata = mergeOptionsArrays(this.target, 'metadata');
94 }
95
96 // Process banner.
97 options.banner = grunt.template.process(options.banner) || '';
98
99 // Read Less.js options from a specified lessrc file.
100 if (options.lessrc) {
101 var fileType = options.lessrc.split('.').pop().toLowerCase();
102 if (fileType === 'yaml' || fileType === 'yml') {
103 // if .lessrc.yml is specified, then parse as YAML
104 options = _.merge(defaults, options, grunt.file.readYAML(options.lessrc));
105 grunt.log.writeln('options: ', options);
106 } else if (fileType === 'lessrc') {
107 // otherwise, parse as JSON
108 options = _.merge(defaults, options, grunt.file.readJSON(options.lessrc));
109 grunt.log.writeln('options: ', options);
110 }
111 } else {
112 options = _.extend(defaults, options);
113 }
114
115 // Load less version specified in options, else load default
116 grunt.verbose.writeln('Loading less from ' + options.version);
117 try {
118 less = require(options.version);
119 } catch (err) {
120 var lessPath = path.join(process.cwd(), options.version);
121 grunt.verbose.writeln('lessPath: ', lessPath);
122 less = require(lessPath);
123 grunt.log.success('\nRunning Less.js v', path.basename(options.version) + '\n');
124 }
125
126 grunt.verbose.writeln('Less loaded');
127
128 if (this.files.length < 1) {
129 grunt.verbose.warn('Destination not written because no source files were provided.');
130 }
131
132 async.forEachSeries(this.files, function(f, nextFileObj) {
133 var destFile = f.dest;
134
135 var files = f.src.filter(function(filepath) {
136 // Warn on and remove invalid source files (if nonull was set).
137 if (!grunt.file.exists(filepath)) {
138 grunt.log.warn('Source file "' + filepath + '" not found.');
139 return false;
140 } else {
141 return true;
142 }
143 });
144
145 if (files.length === 0) {
146 if (f.src.length < 1) {
147 grunt.log.warn('Destination not written because no source files were found.');
148 }
149
150 // No src files, goto next target. Warn would have been issued above.
151 return nextFileObj();
152 }
153
154 var compiledMax = [];
155 var compiledMin = [];
156
157 async.concatSeries(files, function(file, next) {
158 compileLess(file, options, function(css, err) {
159 if (!err) {
160 if (css.max) {
161 compiledMax.push(css.max);
162 }
163 compiledMin.push(css.min);
164 next();
165 } else {
166 nextFileObj(err);
167 }
168 }, function (sourceMapContent) {
169 grunt.file.write(options.sourceMapFilename, sourceMapContent);
170 grunt.log.writeln('File ' + options.sourceMapFilename.cyan + ' created.');
171 });
172 }, function() {
173 if (compiledMin.length < 1) {
174 grunt.log.warn('Destination not written because compiled files were empty.');
175 } else {
176 var min = compiledMin.join(options.cleancss ? '' : grunt.util.normalizelf(grunt.util.linefeed));
177 grunt.file.write(destFile, options.banner + min);
178 grunt.log.writeln('File ' + destFile.cyan + ' created.');
179
180 // ...and report some size information.
181 if (options.report) {
182 contrib.minMaxInfo(min, compiledMax.join(grunt.util.normalizelf(grunt.util.linefeed)), options.report);
183 }
184 }
185 nextFileObj();
186 });
187
188 }, done);
189 });
190
191 var compileLess = function(srcFile, options, callback, sourceMapCallback) {
192 options = _.extend({
193 filename: srcFile,
194 process: options.process
195 }, options);
196 options.paths = options.paths || [path.dirname(srcFile)];
197
198
199 // Prepend variables to source files
200 var globalVariables = [];
201 // Append variables to source files
202 var modifyVariables = [];
203
204 var globalVars = options.globalVars || {};
205 var modifyVars = options.modifyVars || {};
206
207 _.forIn(globalVars, function(value, key) {
208 globalVariables.push('@' + key + ': ' + value + ';');
209 });
210
211 _.forIn(modifyVars, function(value, key) {
212 modifyVariables.push('@' + key + ': ' + value + ';');
213 });
214
215 // Process imports and any templates.
216 var importDirectives = [];
217 function processDirective(list, directive) {
218 _(options.paths).forEach(function(filepath) {
219 _.each(list, function(item) {
220 item = path.join(filepath, item);
221 grunt.file.expand(grunt.template.process(item)).map(function(ea) {
222 importDirectives.push('@import' + ' (' + directive + ') ' + '"' + ea + '";');
223 });
224 });
225 });
226 }
227 for (var directive in options.imports) {
228 if (options.imports.hasOwnProperty(directive)) {
229 var list = options.imports[directive];
230 list = Array.isArray(list) ? list : [list];
231 processDirective(list, directive);
232 }
233 }
234
235 importDirectives = importDirectives.join('\n');
236 modifyVariables = modifyVariables.join('\n');
237 globalVariables = globalVariables.join('\n');
238
239 var css;
240 var srcCode = importDirectives + globalVariables + grunt.file.read(srcFile) + modifyVariables;
241
242 // Read in metadata to pass to templates as context.
243 var metadata = utils.readOptionsData(options.metadata, {namespace: true});
244
245 metadata = _.merge(grunt.config.data, metadata, grunt.task.current.data.options);
246 metadata = grunt.config.process(metadata);
247
248 if (options.process === true) {options.process = {};}
249 if (typeof options.process === 'function') {
250 srcCode = options.process(srcCode, srcFile);
251 } else if (options.process) {
252 srcCode = grunt.template.process(srcCode, {data: metadata});
253 }
254
255 // Strip banners if requested.
256 if (options.stripBanners) {
257 srcCode = comment.stripBanner(srcCode, options.stripBanners);
258 }
259
260 var parser = new less.Parser(_.pick(options, lessOptions.parse));
261
262 parser.parse(srcCode, function(parse_err, tree) {
263 if (parse_err) {
264 lessError(parse_err, srcFile);
265 callback('', true);
266 }
267
268 // Load custom functions
269 if (options.customFunctions) {
270 Object.keys(options.customFunctions).forEach(function(name) {
271 less.tree.functions[name.toLowerCase()] = function() {
272 var args = [].slice.call(arguments);
273 args.unshift(less);
274 return new less.tree.Anonymous(options.customFunctions[name].apply(this, args));
275 };
276 });
277 }
278
279 var minifyOptions = _.pick(options, lessOptions.render);
280
281 if (minifyOptions.sourceMapFilename) {
282 minifyOptions.writeSourceMap = sourceMapCallback;
283 }
284
285 try {
286 css = minify(tree, minifyOptions);
287 callback(css, null);
288 } catch (e) {
289 lessError(e, srcFile);
290 callback(css, true);
291 }
292 });
293 };
294
295 /**
296 * Function from assemble
297 * https://github.com/assemble/assemble
298 */
299 var mergeOptionsArrays = function(target, name) {
300 var taskArray = grunt.config([grunt.task.current.name, 'options', name]) || [];
301 var targetArray = grunt.config([grunt.task.current.name, target, 'options', name]) || [];
302 return _.union(taskArray, targetArray);
303 };
304
305 var formatLessError = function(e) {
306 var pos = '[' + 'L' + e.line + ':' + ('C' + e.column) + ']';
307 return e.filename + ': ' + pos + ' ' + e.message;
308 };
309
310 var lessError = function(e, file) {
311 var message = less.formatError ? less.formatError(e) : formatLessError(e);
312
313 grunt.log.error(message);
314 grunt.fail.warn('Error compiling ' + file);
315 };
316
317 var minify = function(tree, options) {
318 var result = {
319 min: tree.toCSS(options)
320 };
321 if (!_.isEmpty(options)) {
322 result.max = tree.toCSS();
323 }
324 return result;
325 };
326};
\No newline at end of file