@@ -10,16 +10,17 @@ 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' ) ;
1415
1516/**
1617 * Search for the relative file reference from the <code>startPath</code> up to the process
1718 * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>.
1819 * @param {string } startPath The location of the uri declaration and the place to start the search from
1920 * @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
21+ * @returns {string } the full file path of the file where found or <code>null </code> otherwise
2122 */
22- function encodeRelativeURL ( startPath , uri ) {
23+ function findFile ( startPath , uri ) {
2324
2425 /**
2526 * Test whether the given directory is the root of its own package
@@ -66,7 +67,7 @@ function encodeRelativeURL(startPath, uri) {
6667 var isWorking ;
6768 do {
6869 pathToRoot . push ( absoluteStart ) ;
69- isWorking = ( absoluteStart !== process . cwd ( ) ) && notPackage ( absoluteStart ) ;
70+ isWorking = ( absoluteStart !== process . cwd ( ) ) && notPackage ( absoluteStart ) ;
7071 absoluteStart = path . resolve ( absoluteStart , '..' ) ;
7172 } while ( isWorking ) ;
7273
@@ -83,16 +84,36 @@ function encodeRelativeURL(startPath, uri) {
8384
8485 // file exists so convert to a dataURI and end
8586 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 ;
87+ return fullPath ;
9088 }
9189 // enqueue subdirectories that are not packages and are not in the root path
9290 else {
9391 enqueue ( queue , basePath ) ;
9492 }
9593 }
94+
95+ // not found
96+ return null ;
97+ }
98+ }
99+
100+ /**
101+ * Search for the relative file reference from the <code>startPath</code> up to the process
102+ * working directory, avoiding any other directories with a <code>package.json</code> or <code>bower.json</code>,
103+ * and encode as base64 data URI.
104+ * @param {string } startPath The location of the uri declaration and the place to start the search from
105+ * @param {string } uri The content of the url() statement, expected to be a relative file path
106+ * @returns {string } data URI of the file where found or <code>null</code> otherwise
107+ */
108+ function embedRelativeURL ( startPath , uri ) {
109+ var fullPath = findFile ( startPath , uri ) ;
110+ if ( fullPath ) {
111+ var type = mime . lookup ( fullPath ) ,
112+ contents = fs . readFileSync ( fullPath ) ,
113+ base64 = new Buffer ( contents ) . toString ( 'base64' ) ;
114+ return 'data:' + type + ';base64,' + base64 ;
115+ } else {
116+ return null ;
96117 }
97118}
98119
@@ -103,107 +124,123 @@ function encodeRelativeURL(startPath, uri) {
103124 * @param {Array.<string> } [libraryPaths] Any number of library path strings
104125 * @returns {stream.Through } A through stream that performs the operation of a gulp stream
105126 */
106- module . exports = function ( bannerWidth , libraryPaths ) {
107- var output = [ ] ;
108- var libList = ( libraryPaths || [ ] ) . filter ( function isString ( value ) {
109- return ( typeof value === 'string' ) ;
110- } ) ;
127+ module . exports = function ( libraryPaths ) {
128+ var output = [ ] ,
129+ libList = ( libraryPaths || [ ] ) . filter ( function isString ( value ) {
130+ return ( typeof value === 'string' ) ;
131+ } ) ;
111132 return through . obj ( function ( file , encoding , done ) {
112133 var stream = this ;
113134
114135 // 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 ;
136+ var sourcePath = path . dirname ( file . path ) ,
137+ compiledName = path . basename ( file . path , path . extname ( file . path ) ) + '.css' ,
138+ mapName = compiledName + '.map' ,
139+ sourceMapConsumer ;
119140
120141 /**
121142 * 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
143+ * @param {string } filename The filename of the file, including extension
144+ * @param {Buffer| string|object} [ contents] Optional contents for the file or fields to assign to it
124145 * @return {vinyl.File } The file that has been pushed to the stream
125146 */
126- function pushResult ( ext , contents ) {
147+ function pushResult ( filename , contents ) {
127148 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
149+ cwd : file . cwd ,
150+ base : file . base ,
151+ path : path . join ( sourcePath , filename ) ,
152+ contents : Buffer . isBuffer ( contents ) ? contents : ( typeof contents === 'string' ) ? new Buffer ( contents ) : null
132153 } ) ;
133- if ( typeof contents === 'object' ) {
134- for ( var key in contents ) {
135- pending [ key ] = contents [ key ] ;
136- }
137- }
138154 stream . push ( pending ) ;
139155 return pending ;
140156 }
141157
142158 /**
143- * Plugin for css rework that follows SASS transpilation
144- * @param {object } stylesheet AST for the CSS output from SASS
159+ * Create a plugin for css rework that performs rewriting of url() sources
160+ * @param {function({string}, {string}):{string} } uriRewriter A method that rewrites uris
145161 */
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
162+ function rewriteUriPlugin ( uriRewriter ) {
163+ return function reworkPlugin ( stylesheet ) {
164+
165+ // visit each node (selector) in the stylesheet recursively using the official utility method
166+ // each node may have multiple declarations
167+ visit ( stylesheet , function visitor ( declarations ) {
168+ declarations
169+ . forEach ( eachDeclaration ) ;
167170 } ) ;
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 ( '' ) ;
181171
182172 /**
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
173+ * Process a declaration from the syntax tree.
174+ * @param declaration
188175 */
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 ;
176+ function eachDeclaration ( declaration ) {
177+ var URL_STATEMENT_REGEX = / ( u r l \s * \( ) \s * (?: ( [ ' " ] ) ( (?: (? ! \2) .) * ) ( \2) | ( [ ^ ' " ] (?: (? ! \) ) .) * [ ^ ' " ] ) ) \s * ( \) ) / g;
178+
179+ // reverse the original source-map to find the original sass file
180+ var cssStart = declaration . position . start ;
181+ var sassStart = sourceMapConsumer . originalPositionFor ( {
182+ line : cssStart . line ,
183+ column : cssStart . column
184+ } ) ;
185+ if ( ! sassStart . source ) {
186+ throw new Error ( 'failed to decode node-sass source map' ) ; // this can occur with regressions in libsass
201187 }
202- // everything else, including parentheses and quotation (where present) and media statements
203- else {
204- return initialised ;
188+ var sassDir = path . dirname ( sassStart . source ) ;
189+
190+ // allow multiple url() values in the declaration
191+ // split by url statements and process the content
192+ // additional capture groups are needed to match quotations correctly
193+ // escaped quotations are not considered
194+ declaration . value = declaration . value
195+ . split ( URL_STATEMENT_REGEX )
196+ . map ( eachSplitOrGroup )
197+ . join ( '' ) ;
198+
199+ /**
200+ * Encode the content portion of <code>url()</code> statements.
201+ * There are 4 capture groups in the split making every 5th unmatched.
202+ * @param {string } token A single split item
203+ * @param i The index of the item in the split
204+ * @returns {string } Every 3 or 5 items is an encoded url everything else is as is
205+ */
206+ function eachSplitOrGroup ( token , i ) {
207+
208+ // we can get groups as undefined under certain match circumstances
209+ var initialised = token || '' ;
210+
211+ // the content of the url() statement is either in group 3 or group 5
212+ var mod = i % 7 ;
213+ if ( ( mod === 3 ) || ( mod === 5 ) ) {
214+
215+ // remove query string or hash suffix
216+ var uri = initialised . split ( / [ ? # ] / g) . shift ( ) ;
217+ return uri && uriRewriter ( sassDir , uri ) || initialised ;
218+ }
219+ // everything else, including parentheses and quotation (where present) and media statements
220+ else {
221+ return initialised ;
222+ }
205223 }
206224 }
225+ } ;
226+ }
227+
228+ /**
229+ * A URI re-writer function that pushes the file to the output stream and rewrites the URI accordingly.
230+ * @param {string } startPath The location of the uri declaration and the place to start the search from
231+ * @param {string } uri The content of the url() statement, expected to be a relative file path
232+ * @returns {string } the new URL of the output file where found or <code>null</code> otherwise
233+ */
234+ function pushAssetToOutput ( startPath , uri ) {
235+ var fullPath = findFile ( startPath , uri ) ;
236+ if ( fullPath ) {
237+ var contents = fs . readFileSync ( fullPath ) ,
238+ hash = crypto . createHash ( 'md5' ) . update ( contents ) . digest ( 'hex' ) ,
239+ filename = [ '.' , compiledName + '.assets' , hash + path . extname ( fullPath ) ] . join ( '/' ) ;
240+ pushResult ( filename , contents ) ;
241+ return filename ;
242+ } else {
243+ return null ;
207244 }
208245 }
209246
@@ -233,7 +270,7 @@ module.exports = function (bannerWidth, libraryPaths) {
233270
234271 // rework css
235272 var reworked = rework ( cssWithMap , '' )
236- . use ( reworkPlugin )
273+ . use ( rewriteUriPlugin ( pushAssetToOutput ) )
237274 . toString ( {
238275 sourcemap : true ,
239276 sourcemapAsObject : true
@@ -247,8 +284,8 @@ module.exports = function (bannerWidth, libraryPaths) {
247284 } ) ;
248285
249286 // write stream output
250- pushResult ( '.css' , reworked . code + '\n/*# sourceMappingURL=' + mapName + ' */' ) ;
251- pushResult ( '.css.map' , JSON . stringify ( reworked . map , null , 2 ) ) ;
287+ pushResult ( compiledName , reworked . code + '\n/*# sourceMappingURL=' + mapName + ' */' ) ;
288+ pushResult ( mapName , JSON . stringify ( reworked . map , null , 2 ) ) ;
252289 done ( ) ;
253290 }
254291
@@ -257,19 +294,19 @@ module.exports = function (bannerWidth, libraryPaths) {
257294 * @param {string } error The error text from node-sass
258295 */
259296 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' ) ;
297+ var analysis = / ( .* ) \: ( \d + ) \: \s * e r r o r \: \s * ( .* ) / . exec ( error ) ,
298+ resolved = path . resolve ( analysis [ 1 ] ) ,
299+ filename = [ '.scss' , '.css' ]
300+ . map ( function ( ext ) {
301+ return resolved + ext ;
302+ } )
303+ . filter ( function ( fullname ) {
304+ return fs . existsSync ( fullname ) ;
305+ } )
306+ . pop ( ) ,
307+ message = analysis ?
308+ ( ( filename || resolved ) + ':' + analysis [ 2 ] + ':0: ' + analysis [ 3 ] + '\n' ) :
309+ ( 'TODO parse this error\n' + error + '\n' ) ;
273310 if ( output . indexOf ( message ) < 0 ) {
274311 output . push ( message ) ;
275312 }
@@ -291,7 +328,7 @@ module.exports = function (bannerWidth, libraryPaths) {
291328 error : error ,
292329 includePaths : libList ,
293330 outputStyle : 'compressed' ,
294- stats : { } ,
331+ stats : { } ,
295332 sourceMap : map
296333 } ) ;
297334 }
@@ -305,10 +342,10 @@ module.exports = function (bannerWidth, libraryPaths) {
305342
306343 // display the output buffer with padding before and after and between each item
307344 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' ) : '' ;
345+ var WIDTH = 80 ,
346+ hr = new Array ( WIDTH + 1 ) , // this is a good trick to repeat a character N times
347+ start = ( WIDTH > 0 ) ? ( hr . join ( '\u25BC' ) + '\n' ) : '' ,
348+ stop = ( WIDTH > 0 ) ? ( hr . join ( '\u25B2' ) + '\n' ) : '' ;
312349 process . stdout . write ( start + '\n' + output . join ( '\n' ) + '\n' + stop ) ;
313350 }
314351 done ( ) ;
0 commit comments