UNPKG

13 kBJavaScriptView Raw
1/**
2 * Export several useful method
3 * @module utils
4 */
5
6/**
7 * String block to execute file type, target path, and source files which comes from HTML file
8 * @typedef {String} block
9 * @example typical block
10 * <!-- build:<type> <path> -->
11 * <!-- build:css /style/build.css -->
12 * <link rel="stylesheet" href="/style/origin.css">
13 * <link rel="stylesheet" href="/style/complex.css">
14 * <!-- endbuild -->
15 */
16
17/**
18 * source files array, consist of object
19 * @typedef {Array} SourceArray
20 * @example typical SourceArray
21 * [{
22 * type: 'js',
23 * destiny: '/script/build.js',
24 * files: ['script/origin.js', 'script/complex.js']
25 * }];
26 */
27
28/**
29 * Module dependencies
30 */
31"use strict";
32
33var path =require('path');
34var fs = require('fs');
35var util = require('util');
36var crypto = require('crypto');
37var vfs = require('vinyl-fs');
38var through = require('through-gulp');
39var gutil = require('gulp-util');
40
41// normal regular expression
42var startReg = /<!--\s+build:\w+\s+\/?[^\s]+\s+-->/gim;
43var startMirrorReg = /<!--\s+build:\w+\s+\/?[^\s]+\s+-->/i;
44var endReg = /<!--\s*endbuild\s*-->/gim;
45var endMirrorReg = /<!--\s*endbuild\s*-->/i;
46var splitReg = /<!--\s+split\s+-->/gim;
47var jsReg = /<\s*script\s+.*?src\s*=\s*("|')([^"']+?)\1.*?><\s*\/\s*script\s*>/i;
48var cssReg = /<\s*link\s+.*?href\s*=\s*("|')([^"']+)\1.*?>/i;
49var spaceReg = /^\s*$/;
50var typeReg = /<!--\s+build:(\w+)\s+\/?[^\s]+\s+-->/i;
51var pathReg = /<!--\s+build:\w+\s+(\/?[^\s]+)\s+-->/i;
52var utils = {};
53
54// supported file type
55utils._stylesheet = ['css', 'less', 'stylus', 'sass'];
56utils._script = ['js', 'coffee', 'typescript', 'jsx'];
57
58/**
59 * split the HTML into several blocks
60 * @param {string} string
61 * @returns {Array}
62 */
63utils.getSplitBlock = function(string) {
64 return string.split(splitReg);
65};
66
67/**
68 * execute for file type
69 * @param {block} block
70 * @returns {String}
71 */
72utils.getBlockType = function(block) {
73 return typeReg.exec(block)[1];
74};
75
76/**
77 * execute for file target path
78 * @param {block} block
79 * @returns {String}
80 */
81utils.getBlockPath = function(block) {
82 return pathReg.exec(block)[1];
83};
84
85/**
86 * execute for file target path
87 * @param {String} script
88 * @returns {String}
89 * @example
90 * // return "/js/origin.js"
91 * utils.getScriptPath('<script src="/js/origin.js"></script>');
92 */
93utils.getScriptPath = function(script) {
94 if (jsReg.test(script)) return jsReg.exec(script.replace(/^\s*/, ''))[2];
95 gutil.log(gutil.colors.green('failed resolve source path from'), gutil.colors.green(script), 'the block type refers script', '\n');
96 return null;
97};
98
99/**
100 * execute for file target path
101 * @param {String} link
102 * @returns {String}
103 * @example
104 * // return "/style/origin.css"
105 * utils.getLinkPath('<link rel="stylesheet" href="/style/origin.css">')
106 */
107utils.getLinkPath = function(link) {
108 if (cssReg.test(link)) return cssReg.exec(link.replace(/^\s*/, ''))[2];
109 gutil.log(gutil.colors.green('failed resolve source path from'), gutil.colors.green(link), 'the block type refers link', '\n');
110 return null;
111};
112
113/**
114 * execute for file target path
115 * @param {String} line
116 * @param {String} mode - the content property, must exists in ['js', 'coffee', 'typescript', 'jsx'], ['css', 'less', 'sass', 'stylus']
117 * @returns {String}
118 * @see refer {@links utils.getScriptPath}, {@links utils.getLinkPath} for details
119 */
120utils.getReplacePath = function(line, mode) {
121 if (utils._script.indexOf(mode) !== -1) return utils.getScriptPath(line);
122 if (utils._stylesheet.indexOf(mode) !== -1) return utils.getLinkPath(line);
123 return null;
124};
125
126/**
127 * execute for source files path
128 * @param {block} block
129 * @returns {Array}
130 * @example - typical usage
131 * var sample =
132 * <!-- build:css /style/build.css -->
133 * <link rel="stylesheet" href="/style/origin.css">
134 * <link rel="stylesheet" href="/style/complex.css">
135 * <!-- endbuild -->
136 * // return ['style/origin.css', 'style/complex.css'];
137 * utils.getFilePath(sample);
138 */
139utils.getBlockFilePath = function(block) {
140 return block
141 .replace(startReg, '')
142 .replace(endReg, '')
143 .split('\n')
144 .filter(function(value) {
145 return !spaceReg.test(value);
146 })
147 .map(function(value) {
148 switch (true) {
149 case utils._script.indexOf(utils.getBlockType(block)) !== -1 :
150 return utils.getScriptPath(value);
151 case utils._stylesheet.indexOf(utils.getBlockType(block)) !== -1 :
152 return utils.getLinkPath(value);
153 case utils.getBlockType(block) === 'replace' :
154 return utils.getReplacePath(value, path.extname(utils.getBlockPath(block)).slice(1));
155 case utils.getBlockType(block) === 'remove' :
156 return null;
157 default :
158 return null;
159 }
160 })
161 .filter(function(value) {
162 return value !== null;
163 });
164};
165
166/**
167 * get block description structure
168 * @param block
169 * @returns {{type: String, destiny: String, files: Array}}
170 */
171utils.getBlockStructure = function(block) {
172 return {
173 type: utils.getBlockType(block),
174 destiny: utils.getBlockPath(block),
175 files: utils.getBlockFilePath(block)
176 }
177};
178
179/**
180 * check if fragment belongs block
181 * @param {String} block
182 * @returns {boolean}
183 */
184utils.isBlock = function(block) {
185 return startMirrorReg.test(block) && endMirrorReg.test(block);
186};
187
188/**
189 * execute for source files path
190 * @param {Array} blocks - Array consist of block
191 * @returns {SourceArray}
192 */
193utils.getBlockFileSource = function(blocks) {
194 return blocks
195 .filter(function(block) {
196 return utils.isBlock(block);
197 })
198 .map(function(block) {
199 return utils.getBlockStructure(block);
200 });
201};
202
203/**
204 * provide for calculate the postfix, e.g 'v0.2.0', 'md5' or just function with concat buffer as first argument
205 * @param {String|Function} postfix - the postfix for link href or script src, simple string, 'md5' or just function
206 * @param {block} block
207 * @param {Boolean} debug - whether debug environment
208 * @returns {string} - final postfix
209 */
210utils.resolvePostfix = function(postfix, block, debug) {
211 if (postfix === null || typeof postfix === 'undefined') return '';
212 if (util.isString(postfix) && postfix !== 'md5') return '?' + postfix;
213
214 var content;
215 var source = utils.prerenderOriginPath(utils.getBlockFilePath(block), debug);
216
217 content = source.reduce(function(prev, current) {
218 try {
219 return Buffer.concat([prev, fs.readFileSync(current)]) ;
220 } catch (err) {
221 gutil.log(gutil.colors.red('The file ' + current + ' not exist, maybe cause postfix deviation'));
222 return prev;
223 }
224 }, new Buffer(''));
225
226 if (util.isFunction(postfix)) {
227 return '?' + utils.escape(postfix.call(null, content));
228 }
229
230 if (postfix === 'md5') {
231 var hash = crypto.createHash('md5');
232 hash.update(content);
233 return '?' + hash.digest('hex');
234 }
235
236 return '';
237};
238
239/**
240 * resolve the HTML block, replace, remove, or add specific tags
241 * @param {block} block - typical block
242 * @param {Object} options - plugin argument object
243 * @returns {String}
244 */
245utils.generateTags = function(block, options) {
246 switch (true) {
247 case !utils.isBlock(block):
248 return block;
249 case utils._script.indexOf(utils.getBlockType(block)) !== -1 :
250 return '<script src="' + utils.getBlockPath(block) + utils.resolvePostfix(options.postfix, block, options.debug) + '"></script>';
251 case utils._stylesheet.indexOf(utils.getBlockType(block)) !== -1 :
252 return '<link rel="stylesheet" href="' + utils.getBlockPath(block) + utils.resolvePostfix(options.postfix, block, options.debug) + '"/>';
253 case utils.getBlockType(block) === 'replace' && path.extname(utils.getBlockPath(block)) === '.js' :
254 return '<script src="' + utils.getBlockPath(block) + utils.resolvePostfix(options.postfix, block, options.debug) + '"></script>';
255 case utils.getBlockType(block) === 'replace' && path.extname(utils.getBlockPath(block)) === '.css' :
256 return '<link rel="stylesheet" href="' + utils.getBlockPath(block) + utils.resolvePostfix(options.postfix, block, options.debug) + '"/>';
257 case utils.getBlockType(block) === 'remove' :
258 return null;
259 default :
260 return null;
261 }
262};
263
264/**
265 * resolve linked source files and output
266 * @param {SourceArray} sources
267 * @param {Object} options
268 * @returns {boolean}
269 */
270utils.resolveFileSource = function(sources, options) {
271 if (!sources || !options) return false;
272
273 sources = sources.filter(function(value) {
274 return (utils._script.indexOf(value.type) !== -1 || utils._stylesheet.indexOf(value.type) !== -1) && value.files.length !== 0 && value.destiny;
275 });
276
277 if (sources.length === 0) return false;
278
279 for (var i = 0; i < sources.length; i++) {
280 var parser = options[sources[i].type];
281 var files = sources[i].files;
282 var destiny = path.join('./', sources[i].destiny);
283 var stream;
284 if (!parser || parser.length === 0) {
285 stream = utils.pathTraverse(files, null, options.debug).pipe(utils.concat(destiny)).pipe(vfs.dest(path.join('./', options.directory)));
286 } else {
287 stream = utils.pathTraverse(files, parser, options.debug).pipe(utils.concat(destiny)).pipe(vfs.dest(path.join('./', options.directory)));
288 }
289 stream.on('end', function() {
290 var notify = options.notify;
291 notify ? notify.Trigger.emit(notify.Event) : utils.noop();
292 });
293 }
294};
295
296/**
297 * resolve the HTML file, replace, remove, or add specific tags
298 * @param {Array} blocks - array consist of block
299 * @param {Object} options - plugin argument object
300 * @returns {String}
301 */
302utils.resolveSourceToDestiny = function(blocks, options) {
303 var result = blocks.map(function(block) {
304 return utils.generateTags(block, options);
305 });
306
307 return result.join('\n');
308};
309
310utils.prerenderOriginPath = function(originPath, debug) {
311 if (util.isString(originPath)) return !debug ? [path.join('./', originPath)] : [path.join('./', 'test/fixture/', originPath)];
312 if (util.isArray(originPath)) {
313 return originPath.map(function(value) {
314 return !debug ? path.join('./', value) : path.join('./', 'test/fixture/', value);
315 });
316 }
317};
318
319/**
320 * resolve source files through pipeline and return final transform stream
321 * @param {Array} originPath - array consist of source file path
322 * @param {Array} flow - array consist of transform stream, like [less(),cssmin()], or [coffee(), uglify()]
323 * @param {Boolean} debug
324 * @returns {Object} - transform stream
325 */
326utils.pathTraverse = function(originPath, flow, debug) {
327 var destinyPath = utils.prerenderOriginPath(originPath, debug);
328 var stream = vfs.src(destinyPath);
329 if (util.isArray(flow)) {
330 for (var i = 0; i < flow.length; i++) {
331 let generator, options;
332 if (util.isArray(flow[i])) {
333 generator = flow[i][0];
334 options = flow[i][1];
335 }
336 if (util.isFunction(flow[i])) {
337 generator = flow[i];
338 options = {};
339 }
340 stream = stream.pipe(generator(options));
341 }
342 }
343
344 return stream;
345};
346
347/**
348 * Merge two object shallowly
349 * @param {Object} source
350 * @param {Object} destiny
351 * @returns {Object} - object after merge
352 * @example - typical usage
353 * // return { title: 'story', content: 'never say goodbye' }
354 * utils.shallowMerge({
355 * title: 'story',
356 * content: 'never say goodbye'
357 * },{
358 * title: 'love'
359 * });
360 */
361utils.shallowMerge = function(source, destiny) {
362 for (var key in source) {
363 if (source.hasOwnProperty(key)) {
364 destiny[key] = source[key];
365 }
366 }
367
368 return destiny;
369};
370
371/**
372 * Replace all the white space(\s) and enter(\n) for easier unit-test
373 * @param {String} string
374 * @returns {String} - escaped string
375 */
376utils.escape = function(string) {
377 return string.toString().replace(/[\n\s]*/gi, '');
378};
379
380/**
381 * @description - transform stream into promise, the value concat all the content
382 * @param stream
383 * @returns {Object} - return promise actually
384 */
385utils.streamToPromise = function(stream) {
386 if (util.isUndefined(stream.pipe)) return Promise.reject('argument is not stream');
387
388 return new Promise(function(resolve, reject) {
389 var destiny = new Buffer('');
390
391 stream.pipe(through(function(file, encoding, callback) {
392 destiny = Buffer.concat([destiny, file.contents || file]);
393 callback();
394 }, function(callback) {
395 resolve(destiny);
396 callback();
397 }));
398 });
399};
400
401/**
402 * concat several stream contents, like an simple gulp-concat plugin
403 * @param {String} fileName - full relative path for final file, relative to process.cwd()
404 * @returns {Object} - transform stream pipable
405 */
406utils.concat = function(fileName) {
407 var assetStorage = new Buffer(0);
408 var separator = new Buffer('\n');
409 return through(function(file, enc, callback) {
410 assetStorage = Buffer.concat([assetStorage, file.contents, separator]);
411 callback();
412 }, function(callback) {
413 this.push(new gutil.File({
414 path: fileName,
415 contents: assetStorage
416 }));
417 callback();
418 })
419};
420
421/**
422 * just a noop function
423 */
424utils.noop = function() {};
425
426// exports the object
427module.exports = utils;
\No newline at end of file