@@ -10,16 +10,18 @@ var path = require('path'),
1010 visit = require ( 'rework-visit' ) ,
1111 convert = require ( 'convert-source-map' ) ,
1212 SourceMapConsumer = require ( 'source-map' ) . SourceMapConsumer ,
13- mime = require ( 'mime' ) ;
13+ mime = require ( 'mime' ) ,
14+ crypto = require ( 'crypto' ) ,
15+ defaults = require ( 'lodash.defaults' ) ;
1416
1517/**
1618 * Search for the relative file reference from the <code>startPath</code> up to the process
1719 * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
1820 * @param {string } startPath The location of the uri declaration and the place to start the search from
1921 * @param {string } uri The content of the url() statement, expected to be a relative file path
20- * @returns {string } dataURI of the file where found or <code>undefined </code> otherwise
22+ * @returns {string } the full file path of the file where found or <code>null </code> otherwise
2123 */
22- function encodeRelativeURL ( startPath , uri ) {
24+ function findFile ( startPath , uri ) {
2325
2426 /**
2527 * Test whether the given directory is the root of its own package
@@ -66,7 +68,7 @@ function encodeRelativeURL(startPath, uri) {
6668 var isWorking ;
6769 do {
6870 pathToRoot . push ( absoluteStart ) ;
69- isWorking = ( absoluteStart !== process . cwd ( ) ) && notPackage ( absoluteStart ) ;
71+ isWorking = ( absoluteStart !== process . cwd ( ) ) && notPackage ( absoluteStart ) ;
7072 absoluteStart = path . resolve ( absoluteStart , '..' ) ;
7173 } while ( isWorking ) ;
7274
@@ -83,16 +85,36 @@ function encodeRelativeURL(startPath, uri) {
8385
8486 // file exists so convert to a dataURI and end
8587 if ( fs . existsSync ( fullPath ) ) {
86- var type = mime . lookup ( fullPath ) ;
87- var contents = fs . readFileSync ( fullPath ) ;
88- var base64 = new Buffer ( contents ) . toString ( 'base64' ) ;
89- return 'data:' + type + ';base64,' + base64 ;
88+ return fullPath ;
9089 }
9190 // enqueue subdirectories that are not packages and are not in the root path
9291 else {
9392 enqueue ( queue , basePath ) ;
9493 }
9594 }
95+
96+ // not found
97+ return null ;
98+ }
99+ }
100+
101+ /**
102+ * Search for the relative file reference from the <code>startPath</code> up to the process
103+ * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>,
104+ * and encode as base64 data URI.
105+ * @param {string } startPath The location of the uri declaration and the place to start the search from
106+ * @param {string } uri The content of the url() statement, expected to be a relative file path
107+ * @returns {string } data URI of the file where found or <code>null</code> otherwise
108+ */
109+ function embedRelativeURL ( startPath , uri ) {
110+ var fullPath = findFile ( startPath , uri ) ;
111+ if ( fullPath ) {
112+ var type = mime . lookup ( fullPath ) ,
113+ contents = fs . readFileSync ( fullPath ) ,
114+ base64 = new Buffer ( contents ) . toString ( 'base64' ) ;
115+ return 'data:' + type + ';base64,' + base64 ;
116+ } else {
117+ return null ;
96118 }
97119}
98120
@@ -103,107 +125,129 @@ function encodeRelativeURL(startPath, uri) {
103125 * @param {Array.<string> } [libraryPaths] Any number of library path strings
104126 * @returns {stream.Through } A through stream that performs the operation of a gulp stream
105127 */
106- module . exports = function ( bannerWidth , libraryPaths ) {
107- var output = [ ] ;
108- var libList = ( libraryPaths || [ ] ) . filter ( function isString ( value ) {
109- return ( typeof value === 'string' ) ;
128+ module . exports = function ( options ) {
129+ defaults ( options , {
130+ libraryPaths : [ ] ,
131+ embedAssets : false
110132 } ) ;
133+
134+ var output = [ ] ,
135+ libList = options . libraryPaths . filter ( function isString ( value ) {
136+ return ( typeof value === 'string' ) ;
137+ } ) ;
138+
111139 return through . obj ( function ( file , encoding , done ) {
112140 var stream = this ;
113141
114142 // setup parameters
115- var sourcePath = file . path . replace ( path . basename ( file . path ) , '' ) ;
116- var sourceName = path . basename ( file . path , path . extname ( file . path ) ) ;
117- var mapName = sourceName + '.css. map' ;
118- var sourceMapConsumer ;
143+ var sourcePath = path . dirname ( file . path ) ,
144+ compiledName = path . basename ( file . path , path . extname ( file . path ) ) + '.css' ,
145+ mapName = compiledName + '.map' ,
146+ sourceMapConsumer ;
119147
120148 /**
121149 * Push file contents to the output stream.
122- * @param {string } ext The extension for the file, including dot
123- * @param {string|object? } contents The contents for the file or fields to assign to it
150+ * @param {string } filename The filename of the file, including extension
151+ * @param {Buffer| string|object} [ contents] Optional contents for the file or fields to assign to it
124152 * @return {vinyl.File } The file that has been pushed to the stream
125153 */
126- function pushResult ( ext , contents ) {
154+ function pushResult ( filename , contents ) {
127155 var pending = new gutil . File ( {
128- cwd : file . cwd ,
129- base : file . base ,
130- path : sourcePath + sourceName + ext ,
131- contents : ( typeof contents === 'string' ) ? new Buffer ( contents ) : null
156+ cwd : file . cwd ,
157+ base : file . base ,
158+ path : path . join ( sourcePath , filename ) ,
159+ contents : Buffer . isBuffer ( contents ) ? contents : ( typeof contents === 'string' ) ? new Buffer ( contents ) : null
132160 } ) ;
133- if ( typeof contents === 'object' ) {
134- for ( var key in contents ) {
135- pending [ key ] = contents [ key ] ;
136- }
137- }
138161 stream . push ( pending ) ;
139162 return pending ;
140163 }
141164
142165 /**
143- * Plugin for css rework that follows SASS transpilation
144- * @param {object } stylesheet AST for the CSS output from SASS
166+ * Create a plugin for css rework that performs rewriting of url() sources
167+ * @param {function({string}, {string}):{string} } uriRewriter A method that rewrites uris
145168 */
146- function reworkPlugin ( stylesheet ) {
147-
148- // visit each node (selector) in the stylesheet recursively using the official utility method
149- // each node may have multiple declarations
150- visit ( stylesheet , function visitor ( declarations ) {
151- declarations
152- . forEach ( eachDeclaration ) ;
153- } ) ;
154-
155- /**
156- * Process a declaration from the syntax tree.
157- * @param declaration
158- */
159- function eachDeclaration ( declaration ) {
160- var URL_STATEMENT_REGEX = / ( u r l \s * \( ) \s * (?: ( [ ' " ] ) ( (?: (? ! \2) .) * ) ( \2) | ( [ ^ ' " ] (?: (? ! \) ) .) * [ ^ ' " ] ) ) \s * ( \) ) / g;
161-
162- // reverse the original source-map to find the original sass file
163- var cssStart = declaration . position . start ;
164- var sassStart = sourceMapConsumer . originalPositionFor ( {
165- line : cssStart . line ,
166- column : cssStart . column
169+ function rewriteUriPlugin ( uriRewriter ) {
170+ return function reworkPlugin ( stylesheet ) {
171+
172+ // visit each node (selector) in the stylesheet recursively using the official utility method
173+ // each node may have multiple declarations
174+ visit ( stylesheet , function visitor ( declarations ) {
175+ declarations
176+ . forEach ( eachDeclaration ) ;
167177 } ) ;
168- if ( ! sassStart . source ) {
169- throw new Error ( 'failed to decode node-sass source map' ) ; // this can occur with regressions in libsass
170- }
171- var sassDir = path . dirname ( sassStart . source ) ;
172-
173- // allow multiple url() values in the declaration
174- // split by url statements and process the content
175- // additional capture groups are needed to match quotations correctly
176- // escaped quotations are not considered
177- declaration . value = declaration . value
178- . split ( URL_STATEMENT_REGEX )
179- . map ( eachSplitOrGroup )
180- . join ( '' ) ;
181178
182179 /**
183- * Encode the content portion of <code>url()</code> statements.
184- * There are 4 capture groups in the split making every 5th unmatched.
185- * @param {string } token A single split item
186- * @param i The index of the item in the split
187- * @returns {string } Every 3 or 5 items is an encoded url everything else is as is
180+ * Process a declaration from the syntax tree.
181+ * @param declaration
188182 */
189- function eachSplitOrGroup ( token , i ) {
190-
191- // we can get groups as undefined under certain match circumstances
192- var initialised = token || '' ;
193-
194- // the content of the url() statement is either in group 3 or group 5
195- var mod = i % 7 ;
196- if ( ( mod === 3 ) || ( mod === 5 ) ) {
197-
198- // remove query string or hash suffix
199- var uri = initialised . split ( / [ ? # ] / g) . shift ( ) ;
200- return uri && encodeRelativeURL ( sassDir , uri ) || initialised ;
183+ function eachDeclaration ( declaration ) {
184+ var URL_STATEMENT_REGEX = / ( u r l \s * \( ) \s * (?: ( [ ' " ] ) ( (?: (? ! \2) .) * ) ( \2) | ( [ ^ ' " ] (?: (? ! \) ) .) * [ ^ ' " ] ) ) \s * ( \) ) / g;
185+
186+ // reverse the original source-map to find the original sass file
187+ var cssStart = declaration . position . start ;
188+ var sassStart = sourceMapConsumer . originalPositionFor ( {
189+ line : cssStart . line ,
190+ column : cssStart . column
191+ } ) ;
192+ if ( ! sassStart . source ) {
193+ throw new Error ( 'failed to decode node-sass source map' ) ; // this can occur with regressions in libsass
201194 }
202- // everything else, including parentheses and quotation (where present) and media statements
203- else {
204- return initialised ;
195+ var sassDir = path . dirname ( sassStart . source ) ;
196+
197+ // allow multiple url() values in the declaration
198+ // split by url statements and process the content
199+ // additional capture groups are needed to match quotations correctly
200+ // escaped quotations are not considered
201+ declaration . value = declaration . value
202+ . split ( URL_STATEMENT_REGEX )
203+ . map ( eachSplitOrGroup )
204+ . join ( '' ) ;
205+
206+ /**
207+ * Encode the content portion of <code>url()</code> statements.
208+ * There are 4 capture groups in the split making every 5th unmatched.
209+ * @param {string } token A single split item
210+ * @param i The index of the item in the split
211+ * @returns {string } Every 3 or 5 items is an encoded url everything else is as is
212+ */
213+ function eachSplitOrGroup ( token , i ) {
214+
215+ // we can get groups as undefined under certain match circumstances
216+ var initialised = token || '' ;
217+
218+ // the content of the url() statement is either in group 3 or group 5
219+ var mod = i % 7 ;
220+ if ( ( mod === 3 ) || ( mod === 5 ) ) {
221+
222+ // remove query string or hash suffix
223+ var uri = initialised . split ( / [ ? # ] / g) . shift ( ) ;
224+ return uri && uriRewriter ( sassDir , uri ) || initialised ;
225+ }
226+ // everything else, including parentheses and quotation (where present) and media statements
227+ else {
228+ return initialised ;
229+ }
205230 }
206231 }
232+ } ;
233+ }
234+
235+ /**
236+ * A URI re-writer function that pushes the file to the output stream and rewrites the URI accordingly.
237+ * @param {string } startPath The location of the uri declaration and the place to start the search from
238+ * @param {string } uri The content of the url() statement, expected to be a relative file path
239+ * @returns {string } the new URL of the output file where found or <code>null</code> otherwise
240+ */
241+ function pushAssetToOutput ( startPath , uri ) {
242+ var fullPath = findFile ( startPath , uri ) ;
243+ if ( fullPath ) {
244+ var contents = fs . readFileSync ( fullPath ) ,
245+ hash = crypto . createHash ( 'md5' ) . update ( contents ) . digest ( 'hex' ) ,
246+ filename = [ '.' , compiledName + '.assets' , hash + path . extname ( fullPath ) ] . join ( '/' ) ;
247+ pushResult ( filename , contents ) ;
248+ return filename ;
249+ } else {
250+ return null ;
207251 }
208252 }
209253
@@ -232,12 +276,13 @@ module.exports = function (bannerWidth, libraryPaths) {
232276 ) ;
233277
234278 // rework css
235- var reworked = rework ( cssWithMap , '' )
236- . use ( reworkPlugin )
237- . toString ( {
238- sourcemap : true ,
239- sourcemapAsObject : true
240- } ) ;
279+ var plugin = rewriteUriPlugin ( options . embedAssets ? embedRelativeURL : pushAssetToOutput ) ,
280+ reworked = rework ( cssWithMap , '' )
281+ . use ( plugin )
282+ . toString ( {
283+ sourcemap : true ,
284+ sourcemapAsObject : true
285+ } ) ;
241286
242287 // adjust overall sourcemap
243288 delete reworked . map . file ;
@@ -247,8 +292,8 @@ module.exports = function (bannerWidth, libraryPaths) {
247292 } ) ;
248293
249294 // write stream output
250- pushResult ( '.css' , reworked . code + '\n/*# sourceMappingURL=' + mapName + ' */' ) ;
251- pushResult ( '.css.map' , JSON . stringify ( reworked . map , null , 2 ) ) ;
295+ pushResult ( compiledName , reworked . code + '\n/*# sourceMappingURL=' + mapName + ' */' ) ;
296+ pushResult ( mapName , JSON . stringify ( reworked . map , null , 2 ) ) ;
252297 done ( ) ;
253298 }
254299
@@ -257,19 +302,19 @@ module.exports = function (bannerWidth, libraryPaths) {
257302 * @param {string } error The error text from node-sass
258303 */
259304 function errorHandler ( error ) {
260- var analysis = / ( .* ) \: ( \d + ) \: \s * e r r o r \: \s * ( .* ) / . exec ( error ) ;
261- var resolved = path . resolve ( analysis [ 1 ] ) ;
262- var filename = [ '.scss' , '.css' ]
263- . map ( function ( ext ) {
264- return resolved + ext ;
265- } )
266- . filter ( function ( fullname ) {
267- return fs . existsSync ( fullname ) ;
268- } )
269- . pop ( ) ;
270- var message = analysis ?
271- ( ( filename || resolved ) + ':' + analysis [ 2 ] + ':0: ' + analysis [ 3 ] + '\n' ) :
272- ( 'TODO parse this error\n' + error + '\n' ) ;
305+ var analysis = / ( .* ) \: ( \d + ) \: \s * e r r o r \: \s * ( .* ) / . exec ( error ) ,
306+ resolved = path . resolve ( analysis [ 1 ] ) ,
307+ filename = [ '.scss' , '.css' ]
308+ . map ( function ( ext ) {
309+ return resolved + ext ;
310+ } )
311+ . filter ( function ( fullname ) {
312+ return fs . existsSync ( fullname ) ;
313+ } )
314+ . pop ( ) ,
315+ message = analysis ?
316+ ( ( filename || resolved ) + ':' + analysis [ 2 ] + ':0: ' + analysis [ 3 ] + '\n' ) :
317+ ( 'TODO parse this error\n' + error + '\n' ) ;
273318 if ( output . indexOf ( message ) < 0 ) {
274319 output . push ( message ) ;
275320 }
@@ -291,7 +336,7 @@ module.exports = function (bannerWidth, libraryPaths) {
291336 error : error ,
292337 includePaths : libList ,
293338 outputStyle : 'compressed' ,
294- stats : { } ,
339+ stats : { } ,
295340 sourceMap : map
296341 } ) ;
297342 }
@@ -305,10 +350,10 @@ module.exports = function (bannerWidth, libraryPaths) {
305350
306351 // display the output buffer with padding before and after and between each item
307352 if ( output . length ) {
308- var width = Number ( bannerWidth ) || 0 ;
309- var hr = new Array ( width + 1 ) ; // this is a good trick to repeat a character N times
310- var start = ( width > 0 ) ? ( hr . join ( '\u25BC' ) + '\n' ) : '' ;
311- var stop = ( width > 0 ) ? ( hr . join ( '\u25B2' ) + '\n' ) : '' ;
353+ var WIDTH = 80 ,
354+ hr = new Array ( WIDTH + 1 ) , // this is a good trick to repeat a character N times
355+ start = ( WIDTH > 0 ) ? ( hr . join ( '\u25BC' ) + '\n' ) : '' ,
356+ stop = ( WIDTH > 0 ) ? ( hr . join ( '\u25B2' ) + '\n' ) : '' ;
312357 process . stdout . write ( start + '\n' + output . join ( '\n' ) + '\n' + stop ) ;
313358 }
314359 done ( ) ;
0 commit comments