From f06df51144d72a759802bb1784ccf1a0e49739d0 Mon Sep 17 00:00:00 2001 From: Samuel Tilly Date: Mon, 27 Jun 2016 09:06:07 +0200 Subject: [PATCH 01/13] Remove dependencies from * Remove async dependency * Remove lodash.constant dependency * Remove xtend dependency * Added test for inline-ignore in CSS * Only search content once before replacing and rebasing * Returning a promise, making callbak optional --- .eslintrc | 5 +- src/css.js | 148 ++++++++++++++++++++--------------------- src/defaults.js | 17 +++++ src/html.js | 7 +- src/util.js | 16 ----- src/util/extend.js | 10 +++ src/util/isFunction.js | 3 + test/cases/css.css | 11 +-- test/cases/css_out.css | 11 +-- test/spec.js | 2 +- 10 files changed, 124 insertions(+), 106 deletions(-) create mode 100644 src/defaults.js create mode 100644 src/util/extend.js create mode 100644 src/util/isFunction.js diff --git a/.eslintrc b/.eslintrc index 712b033..61a655f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,7 +22,8 @@ "eol-last": 2 }, "env": { - "node": true + "node": true, + "es6": true }, "extends": "eslint:recommended" -} \ No newline at end of file +} diff --git a/src/css.js b/src/css.js index f1d31d9..51cc2dd 100644 --- a/src/css.js +++ b/src/css.js @@ -1,97 +1,97 @@ "use strict"; var CleanCSS = require( "clean-css" ); -var xtend = require( "xtend" ); -var async = require( "async" ); var path = require( "path" ); -var constant = require( "lodash.constant" ); + var inline = require( "./util" ); +var extend = require( "./util/extend" ); +var isFunction = require( "./util/isFunction" ); module.exports = function( options, callback ) { - var settings = xtend( {}, inline.defaults, options ); - - var replaceUrl = function( callback ) - { - var args = this; + options = extend( require( "./defaults" )(), options ); - if( inline.isBase64Path( args.src ) ) - { - return callback( null ); // Skip - } + function replace( search, replace ) { + var re = new RegExp( "url\\(\\s?[\"']?(" + + inline.escapeSpecialChars( search ) + + ")[\"']?\\s?\\);([^\{\:]*)", "gi" ); - inline.getFileReplacement( args.src, settings, function( err, datauriContent ) - { - if( err ) - { - return inline.handleReplaceErr( err, args.src, settings.strict, callback ); + options.fileContent = options.fileContent.replace( re, function( origin, p1, p2 ) { + if ( ( !options.images && p2.indexOf( options.inlineAttribute + " " ) !== -1 ) || + ( options.images && p2.indexOf( options.inlineAttribute + "-ignore " ) === -1 ) ) { + return "url(\"" + replace + "\");" + p2; } - if( typeof( args.limit ) === "number" && datauriContent.length > args.limit * 1000 ) - { - return callback( null ); // Skip - } - - var css = "url(\"" + datauriContent + "\");"; - var re = new RegExp( "url\\(\\s?[\"']?(" + inline.escapeSpecialChars( args.src ) + ")[\"']?\\s?\\);", "g" ); - result = result.replace( re, constant( css ) ); - return callback( null ); + return "url(\"" + p1 + "\");" + p2; } ); - }; - - var rebase = function( src ) - { - var css = "url(\"" + path.join( settings.rebaseRelativeTo, src ).replace( /\\/g, "/" ) + "\");"; - var re = new RegExp( "url\\(\\s?[\"']?(" + inline.escapeSpecialChars( src ) + ")[\"']?\\s?\\);", "g" ); - result = result.replace( re, constant( css ) ); - }; - - var result = settings.fileContent; - var tasks = []; - var found = null; - - var urlRegex = /url\(\s?["']?([^)'"]+)["']?\s?\);.*/gi; - - if( settings.rebaseRelativeTo ) - { - var matches = {}; - while( ( found = urlRegex.exec( result ) ) !== null ) - { - var src = found[ 1 ]; - matches[ src ] = true; - } - for( var src in matches ) - { - if( !inline.isRemotePath( src ) && !inline.isBase64Path( src ) ) - { - rebase( src ); - } - } + return replace; } - var inlineAttributeCommentRegex = new RegExp( "\\/\\*\\s?" + settings.inlineAttribute + "\\s?\\*\\/", "i" ); - var inlineAttributeIgnoreCommentRegex = new RegExp( "\\/\\*\\s?" + settings.inlineAttribute + "-ignore\\s?\\*\\/", "i" ); - - while( ( found = urlRegex.exec( result ) ) !== null ) - { - if( !inlineAttributeIgnoreCommentRegex.test( found[ 0 ] ) && - ( settings.images || inlineAttributeCommentRegex.test( found[ 0 ] ) ) ) - { - tasks.push( replaceUrl.bind( - { - src: found[ 1 ], - limit: settings.images + function search( re ) { + var result = [], + matches; + + while( ( matches = re.exec( options.fileContent ) ) !== null ) { + result.push( matches.map( function( item ) { + return item; } ) ); } + + return result; } - async.parallel( tasks, function( err ) - { - if( !err ) - { - result = settings.cssmin ? CleanCSS.process( result ) : result; + return new Promise( function( resolve ) { + var urlRegex = /url\(\s?["']?([^)'"]+)["']?\s?\);/gi; + + resolve( search( urlRegex ).reduce( function( result, src ) { + result[ src[1] ] = true; + return result; + }, {} ) ); + } ).then( function( matches ) { + return Promise.all( Object.keys( matches ).map( function( src ) { + return new Promise( function( resolve, reject ) { + if ( inline.isBase64Path( src ) ) { + return resolve( src ); // Skip + } + + // Rebase source + if ( !inline.isRemotePath( src ) && options.rebaseRelativeTo ) { + src = replace( src, path.join( options.rebaseRelativeTo, src ).replace( /\\/g, "/" ) ); + } + + // Replace source + return inline.getFileReplacement( src, options, function( err, content ) { + if ( err ) { + if ( options.strict ) { + return reject( err ); + } else { + console.warn( "Not found, skipping: " + src ); + return resolve( src ); // Skip + } + } + + if ( typeof( options.images ) === "number" && + content.length > options.images * 1000 ) { + return resolve( src ); // Skip + } + + src = replace( src, content ); + + return resolve( src ); + } ); + } ); + } ) ); + } ).then( function() { + options.fileContent = options.cssmin ? CleanCSS.process( options.fileContent ) : options.fileContent; + if ( isFunction( callback ) ) { + callback( null, options.fileContent ); + } + return options.fileContent; + } ).catch( function( err ) { + if ( isFunction( callback ) ) { + callback( err ); } - callback( err, result ); + return Promise.reject( err ); } ); }; diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 0000000..67149a1 --- /dev/null +++ b/src/defaults.js @@ -0,0 +1,17 @@ +module.exports = function () { + return { + images: 8, + svgs: 8, + scripts: true, + links: true, + cssmin: false, + strict: false, + relativeTo: "", + rebaseRelativeTo: "", + inlineAttribute: "data-inline", + fileContent: "", + requestTransform: undefined, + scriptTransform: undefined, + linkTransform: undefined + }; +}; diff --git a/src/html.js b/src/html.js index 340460d..055c314 100644 --- a/src/html.js +++ b/src/html.js @@ -3,15 +3,16 @@ var path = require( "path" ); var constant = require( "lodash.constant" ); var unescape = require( "lodash.unescape" ); -var xtend = require( "xtend" ); var async = require( "async" ); + +var extend = require( "./util/extend" ); var inline = require( "./util" ); var css = require( "./css" ); var htmlparser = require( "htmlparser2" ); module.exports = function( options, callback ) { - var settings = xtend( {}, inline.defaults, options ); + var settings = extend( require( "./defaults" )(), options ); function replaceInlineAttribute( string ) { @@ -83,7 +84,7 @@ module.exports = function( options, callback ) return callback( null ); } - var cssOptions = xtend( {}, settings, { + var cssOptions = extend( settings, { fileContent: content.toString(), rebaseRelativeTo: path.relative( settings.relativeTo, path.join( settings.relativeTo, args.src, ".." + path.sep ) ) } ); diff --git a/src/util.js b/src/util.js index 0a83718..8464eb2 100644 --- a/src/util.js +++ b/src/util.js @@ -11,22 +11,6 @@ var util = {}; module.exports = util; -util.defaults = { - images: 8, - svgs: 8, - scripts: true, - links: true, - cssmin: false, - strict: false, - relativeTo: "", - rebaseRelativeTo: "", - inlineAttribute: "data-inline", - fileContent: "", - requestTransform: undefined, - scriptTransform: undefined, - linkTransform: undefined -}; - util.attrValueExpression = "(=[\"']([^\"']+?)[\"'])?"; /** diff --git a/src/util/extend.js b/src/util/extend.js new file mode 100644 index 0000000..de1e362 --- /dev/null +++ b/src/util/extend.js @@ -0,0 +1,10 @@ +module.exports = function ( a, b ) { + return Object.keys( a ).reduce( function( result, key ) { + if ( b && b[key] ) { + result[key] = b[key]; + } else { + result[key] = a[key]; + } + return result; + }, {} ); +}; diff --git a/src/util/isFunction.js b/src/util/isFunction.js new file mode 100644 index 0000000..eb103f4 --- /dev/null +++ b/src/util/isFunction.js @@ -0,0 +1,3 @@ +module.exports = function ( fn ) { + return ( typeof( fn ) === "function" ); +}; diff --git a/test/cases/css.css b/test/cases/css.css index 1155d13..f5d7bac 100644 --- a/test/cases/css.css +++ b/test/cases/css.css @@ -1,7 +1,8 @@ body { - background: url(assets/other.png); - background: url(assets/icon.png);/*data-inline*/ - background: url('assets/icon.png'); /* data-inline */ - background: url( "assets/icon.png" ); /* data-inline */ - background: url("data:image/png;base64,SKIP"); /* data-inline */ + background: url(assets/other.png); + background: url(assets/icon.png);/*data-inline*/ + background: url('assets/icon.png'); /* data-inline */ + background: url( "assets/icon.png" ); /* data-inline */ + background: url( "assets/icon.png" ); /* data-inline-ignore */ + background: url("data:image/png;base64,SKIP"); /* data-inline */ } diff --git a/test/cases/css_out.css b/test/cases/css_out.css index da5a405..0eeedc0 100644 --- a/test/cases/css_out.css +++ b/test/cases/css_out.css @@ -1,7 +1,8 @@ body { - background: url(assets/other.png); - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/ - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */ - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */ - background: url("data:image/png;base64,SKIP"); /* data-inline */ + background: url(assets/other.png); + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC");/*data-inline*/ + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */ + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAHklEQVQoz2NgAIP/YMBAPBjVMNAa/pMISNcwEoMVAH0ls03D44ABAAAAAElFTkSuQmCC"); /* data-inline */ + background: url("assets/icon.png"); /* data-inline-ignore */ + background: url("data:image/png;base64,SKIP"); /* data-inline */ } diff --git a/test/spec.js b/test/spec.js index 5131468..598f378 100644 --- a/test/spec.js +++ b/test/spec.js @@ -20,7 +20,7 @@ function readFile( file ) function diff( actual, expected ) { - if( actual === expected ) + if( !actual || !expected || ( actual === expected ) ) { return; } From c1405650f8d137947bb73f1bb572b212ccc1d4d0 Mon Sep 17 00:00:00 2001 From: Samuel Tilly Date: Mon, 27 Jun 2016 10:03:03 +0200 Subject: [PATCH 02/13] made option parsing a function --- src/css.js | 3 +-- src/html.js | 2 +- src/{defaults.js => options.js} | 8 +++++--- 3 files changed, 7 insertions(+), 6 deletions(-) rename src/{defaults.js => options.js} (74%) diff --git a/src/css.js b/src/css.js index 51cc2dd..75b0b7d 100644 --- a/src/css.js +++ b/src/css.js @@ -4,12 +4,11 @@ var CleanCSS = require( "clean-css" ); var path = require( "path" ); var inline = require( "./util" ); -var extend = require( "./util/extend" ); var isFunction = require( "./util/isFunction" ); module.exports = function( options, callback ) { - options = extend( require( "./defaults" )(), options ); + options = require( "./options" )( options ); function replace( search, replace ) { var re = new RegExp( "url\\(\\s?[\"']?(" + diff --git a/src/html.js b/src/html.js index 055c314..aeaa475 100644 --- a/src/html.js +++ b/src/html.js @@ -12,7 +12,7 @@ var htmlparser = require( "htmlparser2" ); module.exports = function( options, callback ) { - var settings = extend( require( "./defaults" )(), options ); + var settings = require( "./options" )( options ); function replaceInlineAttribute( string ) { diff --git a/src/defaults.js b/src/options.js similarity index 74% rename from src/defaults.js rename to src/options.js index 67149a1..cf0105d 100644 --- a/src/defaults.js +++ b/src/options.js @@ -1,5 +1,7 @@ -module.exports = function () { - return { +var extend = require( "./util/extend" ); + +module.exports = function ( options ) { + return extend( { images: 8, svgs: 8, scripts: true, @@ -13,5 +15,5 @@ module.exports = function () { requestTransform: undefined, scriptTransform: undefined, linkTransform: undefined - }; + }, options ); }; From 05f73d423b6e78e9e8149fe4c32e2e4616f7c5cd Mon Sep 17 00:00:00 2001 From: Samuel Tilly Date: Mon, 27 Jun 2016 10:04:31 +0200 Subject: [PATCH 03/13] fix typo --- src/html.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/html.js b/src/html.js index 340460d..8376df0 100644 --- a/src/html.js +++ b/src/html.js @@ -40,7 +40,7 @@ module.exports = function( options, callback ) return callback( err ); } - if( !content || typeof( args.limit ) === "number" && js.length > args.limit * 1000 ) + if( !content || typeof( args.limit ) === "number" && content.length > args.limit * 1000 ) { return callback( null ); } From 0bf2cdde7dccd08828a37094a96550da99cb704c Mon Sep 17 00:00:00 2001 From: Samuel Tilly Date: Mon, 27 Jun 2016 10:07:16 +0200 Subject: [PATCH 04/13] update node requirement to latest lts --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f59b1f..f13b4c6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "https://github.com/jrit/web-resource-inliner.git" }, "engines": { - "node": ">=4.0.0" + "node": ">=4.2.0" }, "devDependencies": { "faux-jax": "^5.0.0", From d84da3cbc1921390d82dd397db7d7123918d02aa Mon Sep 17 00:00:00 2001 From: Samuel Tilly Date: Tue, 28 Jun 2016 16:34:19 +0200 Subject: [PATCH 05/13] Rewrite html.js * Added suport for srcset * Added promise response * Removed async * Removed lodash * Removed xtend * Use htmlparser2 instead of regex * Respect original attributes * Use htmlparse2 for generating valid output --- package.json | 7 +- src/css.js | 71 ++-- src/html.js | 374 ++++++++---------- src/inline.js | 10 +- src/options.js | 9 + src/svg.js | 22 ++ src/util.js | 180 --------- src/util/escapeSpecialChars.js | 12 + src/util/getFileReplacement.js | 22 ++ src/util/getInlineFileContents.js | 6 + src/util/getInlineFilePath.js | 7 + src/util/getRemote.js | 49 +++ src/util/getTextReplacement.js | 29 ++ src/util/isBase64Path.js | 3 + src/util/isRemotePath.js | 3 + src/util/search.js | 12 + test/cases/404.html | 2 +- test/cases/css-multiline_out.html | 14 +- test/cases/css-remote-no-protocol.html | 2 +- test/cases/css-remote-no-protocol_out.html | 4 +- test/cases/css-remote-relative-to-url.html | 2 +- .../cases/css-remote-relative-to-url_out.html | 4 +- test/cases/css-remote.html | 2 +- test/cases/css-remote_out.html | 4 +- test/cases/css-transform_out.html | 2 +- test/cases/css.html | 2 +- test/cases/css_out.html | 14 +- test/cases/img-opt-in.html | 2 +- test/cases/img-opt-in_out.html | 10 +- test/cases/img-opt-out.html | 4 +- test/cases/img-opt-out_out.html | 8 +- test/cases/img-remote.html | 2 +- test/cases/img-remote_out.html | 4 +- test/cases/img-singleline_out.html | 2 +- test/cases/img-too-large.html | 2 +- test/cases/img-too-large_out.html | 4 +- test/cases/img.html | 2 +- test/cases/img_out.html | 6 +- test/cases/missing-file.html | 2 +- test/cases/script-multiline_out.html | 4 +- test/cases/script-regex-escape.html | 2 +- test/cases/script-transform.html | 2 +- test/cases/script-transform_out.html | 2 +- test/cases/script.html | 2 +- test/cases/script_out.html | 6 +- test/cases/svg/svg-opt-out.html | 2 +- test/cases/svg/svg-opt-out_out.html | 4 +- test/cases/svg/svg-too-large_out.html | 2 +- test/cases/svg/svg_out.html | 4 +- test/spec.js | 7 +- 50 files changed, 437 insertions(+), 516 deletions(-) create mode 100644 src/svg.js delete mode 100644 src/util.js create mode 100644 src/util/escapeSpecialChars.js create mode 100644 src/util/getFileReplacement.js create mode 100644 src/util/getInlineFileContents.js create mode 100644 src/util/getInlineFilePath.js create mode 100644 src/util/getRemote.js create mode 100644 src/util/getTextReplacement.js create mode 100644 src/util/isBase64Path.js create mode 100644 src/util/isRemotePath.js create mode 100644 src/util/search.js diff --git a/package.json b/package.json index f13b4c6..8826db0 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,10 @@ "mocha": "^2.0.1" }, "dependencies": { - "async": "^0.9.0", - "chalk": "^1.1.3", "clean-css": "1.1.7", "datauri": "~0.2.0", "htmlparser2": "^3.9.0", - "lodash.constant": "^3.0.0", - "lodash.unescape": "^4.0.0", - "request": "^2.72.0", - "xtend": "^4.0.0" + "request": "^2.72.0" }, "scripts": { "test": "mocha test", diff --git a/src/css.js b/src/css.js index 75b0b7d..5357d3c 100644 --- a/src/css.js +++ b/src/css.js @@ -3,8 +3,12 @@ var CleanCSS = require( "clean-css" ); var path = require( "path" ); -var inline = require( "./util" ); +var search = require( "./util/search" ); var isFunction = require( "./util/isFunction" ); +var isBase64Path = require( "./util/isBase64Path" ); +var isRemotePath = require( "./util/isRemotePath" ); +var escapeSpecialChars = require( "./util/escapeSpecialChars" ); +var getFileReplacement = require( "./util/getFileReplacement" ); module.exports = function( options, callback ) { @@ -12,7 +16,7 @@ module.exports = function( options, callback ) function replace( search, replace ) { var re = new RegExp( "url\\(\\s?[\"']?(" + - inline.escapeSpecialChars( search ) + + escapeSpecialChars( search ) + ")[\"']?\\s?\\);([^\{\:]*)", "gi" ); options.fileContent = options.fileContent.replace( re, function( origin, p1, p2 ) { @@ -27,58 +31,52 @@ module.exports = function( options, callback ) return replace; } - function search( re ) { - var result = [], - matches; - - while( ( matches = re.exec( options.fileContent ) ) !== null ) { - result.push( matches.map( function( item ) { - return item; - } ) ); + return new Promise( function( resolve, reject ) { + if ( !options.fileContent ) { + return reject( new Error( "No file content" ) ); } - return result; - } - - return new Promise( function( resolve ) { - var urlRegex = /url\(\s?["']?([^)'"]+)["']?\s?\);/gi; - - resolve( search( urlRegex ).reduce( function( result, src ) { - result[ src[1] ] = true; + return resolve( search( + /url\(\s?["']?([^)'"]+)["']?\s?\);/gi, + options.fileContent + ).reduce( function( result, src ) { + if ( !result[ src[1] ] ) { + result[ src[1] ] = [ src[0] ]; + } else { + result[ src[1] ].push( src[0] ); + } return result; }, {} ) ); } ).then( function( matches ) { return Promise.all( Object.keys( matches ).map( function( src ) { - return new Promise( function( resolve, reject ) { - if ( inline.isBase64Path( src ) ) { - return resolve( src ); // Skip - } + if ( isBase64Path( src ) ) { + return Promise.resolve(); // Skip + } - // Rebase source - if ( !inline.isRemotePath( src ) && options.rebaseRelativeTo ) { - src = replace( src, path.join( options.rebaseRelativeTo, src ).replace( /\\/g, "/" ) ); + return new Promise( function( resolve, reject ) { + var origin = src; + if ( !isRemotePath( src ) && options.rebaseRelativeTo ) { + origin = path.join( options.rebaseRelativeTo, src ).replace( /\\/g, "/" ); } // Replace source - return inline.getFileReplacement( src, options, function( err, content ) { + return getFileReplacement( origin, options, function( err, content ) { if ( err ) { if ( options.strict ) { return reject( err ); } else { console.warn( "Not found, skipping: " + src ); - return resolve( src ); // Skip + return resolve(); // Skip } } - - if ( typeof( options.images ) === "number" && - content.length > options.images * 1000 ) { - return resolve( src ); // Skip - } - - src = replace( src, content ); - - return resolve( src ); + resolve( content ); } ); + } ).then( function( content ) { + if ( !content || typeof( options.images ) === "number" && + content.length > options.images * 1000 ) { + return; // Skip + } + return replace( src, content ); } ); } ) ); } ).then( function() { @@ -88,6 +86,7 @@ module.exports = function( options, callback ) } return options.fileContent; } ).catch( function( err ) { + console.log( err ); //#Debug if ( isFunction( callback ) ) { callback( err ); } diff --git a/src/html.js b/src/html.js index 9e5addb..63e23e1 100644 --- a/src/html.js +++ b/src/html.js @@ -1,254 +1,188 @@ "use strict"; var path = require( "path" ); -var constant = require( "lodash.constant" ); -var unescape = require( "lodash.unescape" ); -var async = require( "async" ); +var htmlparser = require( "htmlparser2" ); -var extend = require( "./util/extend" ); -var inline = require( "./util" ); var css = require( "./css" ); -var htmlparser = require( "htmlparser2" ); +var svg = require( "./svg" ); +var extend = require( "./util/extend" ); +var isBase64Path = require( "./util/isBase64Path" ); + +var getTextReplacement = require( "./util/getTextReplacement" ); +var getFileReplacement = require( "./util/getFileReplacement" ); +var isFunction = require( "./util/isFunction" ); module.exports = function( options, callback ) { - var settings = require( "./options" )( options ); + options = require( "./options" )( options ); + + function replaceContent( elem ) { + var replacer = ( function() { + if ( [ "script", "link", "use" ].indexOf( elem.name ) !== -1 ) { + return getTextReplacement; + } else { + return getFileReplacement; + } + } )(); + + var limit = ( function(){ + switch ( elem.name ) { + case "img": + return options.images; + case "use": + return options.svgs; + case "link": + return options.links; + case "script": + return options.scripts; + } + } )(); - function replaceInlineAttribute( string ) - { - return string - .replace( new RegExp( " " + settings.inlineAttribute + "-ignore" + inline.attrValueExpression, "gi" ), "" ) - .replace( new RegExp( " " + settings.inlineAttribute + inline.attrValueExpression, "gi" ), "" ); - } + function inlineAttributeCheck() { + function isset( obj ) { return obj !== undefined; } - var replaceScript = function( callback ) - { - var args = this; + return ( !isset( elem.attribs[options.inlineAttribute + "-ignore"] ) && + limit || isset( elem.attribs[options.inlineAttribute] ) ); + } - args.element = replaceInlineAttribute( args.element ); + function createTextChild( content ) { + return [ { + data: "\n", + type: "text" + }, { + data: content, + type: "text" + }, { + data: "\n", + type: "text" + } ]; + } - inline.getTextReplacement( args.src, settings, function( err, content ) - { - if( err ) - { - return inline.handleReplaceErr( err, args.src, settings.strict, callback ); + return Promise.all( [ "src", "href", "setsrc", "xlink:href" ].map( function( src ) { + if ( !elem.attribs[src] || + !inlineAttributeCheck() || + isBase64Path( elem.attribs[src] ) ) { + return Promise.resolve(); // Skip } - var onTransform = function( err, content ) - { - if( err ) - { - return callback( err ); + return new Promise( function( resolve, reject ) { + // Retrive source content + return replacer( elem.attribs[ src ].split( "#" )[0], options, function( err, content ) { + if ( err ) { + if ( options.strict ) { + return reject( err ); + } else { + console.warn( "Not found, skipping: " + elem.attribs[ src ] ); + return resolve(); // Skip + } + } + return resolve( content ); + } ); + } ).then( function( content ) { + if ( !content ) { + return; // Skip } - if( !content || typeof( args.limit ) === "number" && content.length > args.limit * 1000 ) - { - return callback( null ); + // Handle transformations + if ( !options[elem.name+"Transform"] ) { + return Promise.resolve( content ); // Skip } - var html = "\n" + content + "\n"; - var re = new RegExp( inline.escapeSpecialChars( args.element ), "g" ); - result = result.replace( re, constant( html ) ); - return callback( null ); - }; - - if( options.scriptTransform ) - { - return options.scriptTransform( content, onTransform ); - } - onTransform( null, content ); - } ); - }; - - var replaceLink = function( callback ) - { - var args = this; - - args.element = replaceInlineAttribute( args.element ); - inline.getTextReplacement( args.src, settings, function( err, content ) - { - if( err ) - { - return inline.handleReplaceErr( err, args.src, settings.strict, callback ); - } - - var onTransform = function( err, content ) - { - if( err ) - { - return callback( err ); + return new Promise( function( resolve, reject ) { + options[elem.name+"Transform"]( content, function( err, content ) { + if ( err ) { + return reject( err ); + } + return resolve( content ); + } ); + } ); + } ).then( function( content ) { + if ( !content || typeof( limit ) === "number" && + content.length > limit * 1000 ) { + return; // Skip } - if( !content || typeof( args.limit ) === "number" && content.length > args.limit * 1000 ) - { - return callback( null ); + // Replace content and/or element + if ( elem.name === "img" ) { + return elem.attribs[ src ] = content; } - var cssOptions = extend( settings, { - fileContent: content.toString(), - rebaseRelativeTo: path.relative( settings.relativeTo, path.join( settings.relativeTo, args.src, ".." + path.sep ) ) - } ); - - css( cssOptions, function( err, content ) - { - if( err ) - { - return callback( err ); - } - var html = "\n" + content + "\n"; - var re = new RegExp( inline.escapeSpecialChars( args.element ), "g" ); - result = result.replace( re, constant( html ) ); - return callback( null ); - } ); - }; + if ( elem.name === "link" ) { + // Inline images for each source in the CSS + return css( extend( options, { + fileContent: content, + rebaseRelativeTo: path.relative( options.relativeTo, + path.join( options.relativeTo, elem.attribs[ src ], ".." + path.sep ) ) + } ) ).then( function( content ) { + // Convert to style element + elem.type = "style"; + elem.name = "style"; + elem.attribs = { type: "text/css" }; + return elem.children = createTextChild( content ); + } ); + } - if( options.linkTransform ) - { - return options.linkTransform( content, onTransform ); - } - onTransform( null, content ); - } ); - }; + if ( elem.name === "script" ) { + elem.type = "script"; + elem.attribs = { type: "text/javascript" }; + return elem.children = createTextChild( content ); + } - var replaceImg = function( callback ) - { - var args = this; + if ( elem.name === "use" && + elem.attribs[ src ].split( "#" ).length === 2 ) { + return svg( + content, + elem.attribs[ src ].split( "#" )[1] + ).then( function( svgElement ) { + elem.attribs = {}; + for ( var prop in svgElement ) { + if ( {}.hasOwnProperty.call( svgElement, prop ) ) { + elem[prop] = svgElement[prop]; + } + } + } ); + } + } ); + } ) ); + } - args.element = replaceInlineAttribute( args.element ); + return new Promise( function( resolve, reject ) { - inline.getFileReplacement( args.src, settings, function( err, datauriContent ) - { - if( err ) - { - return inline.handleReplaceErr( err, args.src, settings.strict, callback ); + var handler = new htmlparser.DomHandler( function ( err, dom ) { + if ( err ) { + return reject( err ); + } else { + return resolve( dom ); } - if( !datauriContent || typeof( args.limit ) === "number" && datauriContent.length > args.limit * 1000 ) - { - return callback( null ); - } - var html = ""; - var re = new RegExp( inline.escapeSpecialChars( args.element ), "g" ); - result = result.replace( re, constant( html ) ); - return callback( null ); } ); - }; - - var replaceSvg = function( callback ) - { - var args = this; - - args.element = replaceInlineAttribute( args.element ); - inline.getTextReplacement( args.src, settings, function( err, content ) - { - if( err ) - { - return inline.handleReplaceErr( err, args.src, settings.strict, callback ); - } - if( !content || typeof( args.limit ) === "number" && content.length > args.limit * 1000 ) - { - return callback( null ); - } - - var handler = new htmlparser.DomHandler( function( err, dom ) - { - if( err ) - { - return callback( err ); - } - - var svg = htmlparser.DomUtils.getElements( { id: args.id }, dom ); - if( svg.length ) - { - var use = htmlparser.DomUtils.getInnerHTML( svg[ 0 ] ); - var re = new RegExp( inline.escapeSpecialChars( args.element ), "g" ); - result = result.replace( re, constant( use ) ); - } - - return callback( null ); - },{ normalizeWhitespace: true } ); - var parser = new htmlparser.Parser( handler, { xmlMode: true } ); - parser.write( content ); - parser.done(); + var parser = new htmlparser.Parser( handler ); + parser.write( options.fileContent ); + parser.end(); + + } ).then( function( dom ) { + return Promise.all( [ + "script", + "link", + "img", + "use" + ].reduce( function( result, type ) { + return result.concat( + htmlparser.DomUtils.getElementsByTagName( type, dom ) + ); + }, [] ).map( replaceContent ) ).then( function() { + // Re-construct HTML + return htmlparser.DomUtils.getOuterHTML( dom ); } ); - }; - - var result = settings.fileContent; - var tasks = []; - var found; - - var inlineAttributeRegex = new RegExp( settings.inlineAttribute, "i" ); - var inlineAttributeIgnoreRegex = new RegExp( settings.inlineAttribute + "-ignore", "i" ); - - var scriptRegex = /\s*<\/script>/gi; - while( ( found = scriptRegex.exec( result ) ) !== null ) - { - if( !inlineAttributeIgnoreRegex.test( found[ 0 ] ) && - ( settings.scripts || inlineAttributeRegex.test( found[ 0 ] ) ) ) - { - tasks.push( replaceScript.bind( - { - element: found[ 0 ], - src: unescape( found[ 2 ] ).trim(), - attrs: inline.getAttrs( found[ 0 ], settings ), - limit: settings.scripts - } ) ); - } - } - - var linkRegex = //gi; - while( ( found = linkRegex.exec( result ) ) !== null ) - { - if( !inlineAttributeIgnoreRegex.test( found[ 0 ] ) && - ( settings.links || inlineAttributeRegex.test( found[ 0 ] ) ) ) - { - tasks.push( replaceLink.bind( - { - element: found[ 0 ], - src: unescape( found[ 2 ] ).trim(), - attrs: inline.getAttrs( found[ 0 ], settings ), - limit: settings.links - } ) ); - } - } - - var imgRegex = //gi; - while( ( found = imgRegex.exec( result ) ) !== null ) - { - if( !inlineAttributeIgnoreRegex.test( found[ 0 ] ) && - ( settings.images || inlineAttributeRegex.test( found[ 0 ] ) ) ) - { - tasks.push( replaceImg.bind( - { - element: found[ 0 ], - src: unescape( found[ 2 ] ).trim(), - attrs: inline.getAttrs( found[ 0 ], settings ), - limit: settings.images - } ) ); + } ).then( function( result ) { + if ( isFunction( callback ) ) { + callback( null, result ); } - } - - var svgRegex = /(<\/\s*use>)?/gi; - while( ( found = svgRegex.exec( result ) ) !== null ) - { - if( !inlineAttributeIgnoreRegex.test( found[ 0 ] ) && - ( settings.svgs || inlineAttributeRegex.test( found[ 0 ] ) ) ) - { - tasks.push( replaceSvg.bind( - { - element: found[ 0 ], - src: unescape( found[ 2 ] ).trim(), - attrs: inline.getAttrs( found[ 0 ], settings ), - limit: settings.svgs, - id: unescape( found[ 3 ] ).trim() - } ) ); + return result; + } ).catch( function( err ) { + if ( isFunction( callback ) ) { + callback( err, options.fileContent ); } - } - - result = replaceInlineAttribute( result ); - - async.parallel( tasks, function( err ) - { - callback( err, result ); + return Promise.reject( err ); } ); }; diff --git a/src/inline.js b/src/inline.js index d3ee9d1..4197467 100644 --- a/src/inline.js +++ b/src/inline.js @@ -8,9 +8,7 @@ "use strict"; -var inline = {}; - -module.exports = inline; - -inline.html = require( "./html" ); -inline.css = require( "./css" ); +module.exports = { + html: require( "./html" ), + css: require( "./css" ) +}; diff --git a/src/options.js b/src/options.js index cf0105d..8463425 100644 --- a/src/options.js +++ b/src/options.js @@ -1,6 +1,15 @@ var extend = require( "./util/extend" ); +function resolveContent( content ) { + if ( content instanceof Buffer ) { + return content.toString(); + } + return content; +} + module.exports = function ( options ) { + options.fileContent = resolveContent( options.fileContent ); + return extend( { images: 8, svgs: 8, diff --git a/src/svg.js b/src/svg.js new file mode 100644 index 0000000..67e1f1c --- /dev/null +++ b/src/svg.js @@ -0,0 +1,22 @@ +var htmlparser = require( "htmlparser2" ); + +module.exports = function( content, id ) { + return new Promise( function( resolve, reject ) { + var handler = new htmlparser.DomHandler( function( err, dom ) { + if( err ) { + return reject( err ); + } + + var svg = htmlparser.DomUtils.getElements( { id: id }, dom ); + if( svg.length ) { + return resolve( svg[ 0 ] ); + } + + return resolve( "" ); + + }, { normalizeWhitespace: true } ); + var parser = new htmlparser.Parser( handler, { xmlMode: true } ); + parser.write( content ); + parser.done(); + } ); +}; diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 8464eb2..0000000 --- a/src/util.js +++ /dev/null @@ -1,180 +0,0 @@ -"use strict"; - -var path = require( "path" ); -var url = require( "url" ); -var datauri = require( "datauri" ); -var fs = require( "fs" ); -var request = require( "request" ); -var chalk = require( "chalk" ); - -var util = {}; - -module.exports = util; - -util.attrValueExpression = "(=[\"']([^\"']+?)[\"'])?"; - -/** - * Escape special regex characters of a particular string - * - * @example - * "http://www.test.com" --> "http:\/\/www\.test\.com" - * - * @param {String} str - string to escape - * @return {String} string with special characters escaped - */ -util.escapeSpecialChars = function( str ) -{ - return str.replace( /(\/|\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\)/g, "\\$1" ); -}; - -util.isRemotePath = function( url ) -{ - return /^'?https?:\/\/|^\/\//.test( url ); -}; - -util.isBase64Path = function( url ) -{ - return /^'?data.*base64/.test( url ); -}; - -util.getAttrs = function( tagMarkup, settings ) -{ - var tag = tagMarkup.match( /^<[^\W>]*/ ); - if( tag ) - { - tag = tag[ 0 ]; - var attrs = tagMarkup - .replace( /^<[^\s>]*/, "" ) - .replace( /\/?>/, "" ) - .replace( />?\s?<\/[^>]*>$/, "" ) - .replace( new RegExp( settings.inlineAttribute + "-ignore" + util.attrValueExpression, "gi" ), "" ) - .replace( new RegExp( settings.inlineAttribute + util.attrValueExpression, "gi" ), "" ); - - if( tag === " "http:\/\/www\.test\.com" + * + * @param {String} str - string to escape + * @return {String} string with special characters escaped + */ +module.exports = function( str ) { + return str.replace( /(\/|\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\)/g, "\\$1" ); +}; diff --git a/src/util/getFileReplacement.js b/src/util/getFileReplacement.js new file mode 100644 index 0000000..069d709 --- /dev/null +++ b/src/util/getFileReplacement.js @@ -0,0 +1,22 @@ +var url = require( "url" ); +var datauri = require( "datauri" ); + +var getRemote = require( "./getRemote" ); +var isRemotePath = require( "./isRemotePath" ); +var getInlineFilePath = require( "./getInlineFilePath" ); + +module.exports = function( src, settings, callback ) { + if( isRemotePath( settings.relativeTo ) ) + { + getRemote( url.resolve( settings.relativeTo, src ), settings, callback, true ); + } + else if( isRemotePath( src ) ) + { + getRemote( src, settings, callback, true ); + } + else + { + var result = ( new datauri( getInlineFilePath( src, settings.relativeTo ) ) ).content; + callback( result === undefined ? new Error( "Local file not found" ) : null, result ); + } +}; diff --git a/src/util/getInlineFileContents.js b/src/util/getInlineFileContents.js new file mode 100644 index 0000000..411ad28 --- /dev/null +++ b/src/util/getInlineFileContents.js @@ -0,0 +1,6 @@ +var fs = require( "fs" ); +var getInlineFilePath = require( "./getInlineFilePath" ); + +module.exports = function( src, relativeTo ) { + return fs.readFileSync( getInlineFilePath( src, relativeTo ) ); +}; diff --git a/src/util/getInlineFilePath.js b/src/util/getInlineFilePath.js new file mode 100644 index 0000000..778d9ae --- /dev/null +++ b/src/util/getInlineFilePath.js @@ -0,0 +1,7 @@ +var path = require( "path" ); + +module.exports = function( src, relativeTo ) +{ + src = src.replace( /^\//, "" ); + return path.resolve( relativeTo, src ).replace( /\?.*$/, "" ); +}; diff --git a/src/util/getRemote.js b/src/util/getRemote.js new file mode 100644 index 0000000..6c7ac03 --- /dev/null +++ b/src/util/getRemote.js @@ -0,0 +1,49 @@ +var request = require( "request" ); + +module.exports = function getRemote( uri, settings, callback, toDataUri ) +{ + if( /^\/\//.test( uri ) ) { + uri = "https:" + uri; + } + + var requestOptions = { + uri: uri, + encoding: toDataUri ? "binary" : "", + gzip: true + }; + + if( typeof settings.requestTransform === "function" ) { + var transformedOptions = settings.requestTransform( requestOptions ); + if( transformedOptions === false ) { + return callback(); + } + if( transformedOptions === undefined ) { + return callback( new Error( uri + " requestTransform returned `undefined`" ) ); + } + requestOptions = transformedOptions || requestOptions; + } + + request( + requestOptions, + function( err, response, body ) { + if( err ) + { + return callback( err ); + } + else if( response.statusCode !== 200 ) + { + return callback( new Error( uri + " returned http " + response.statusCode ) ); + } + + if( toDataUri ) + { + var b64 = new Buffer( body.toString(), "binary" ).toString( "base64" ); + var datauriContent = "data:" + response.headers[ "content-type" ] + ";base64," + b64; + return callback( null, datauriContent ); + } + else + { + return callback( null, body ); + } + } ); +}; diff --git a/src/util/getTextReplacement.js b/src/util/getTextReplacement.js new file mode 100644 index 0000000..a961a28 --- /dev/null +++ b/src/util/getTextReplacement.js @@ -0,0 +1,29 @@ +var url = require( "url" ); + +var getRemote = require( "./getRemote" ); +var isRemotePath = require( "./isRemotePath" ); +var getInlineFileContents = require( "./getInlineFileContents" ); + +module.exports = function( src, settings, callback ) +{ + if( isRemotePath( settings.relativeTo ) || isRemotePath( src ) ) + { + getRemote( url.resolve( settings.relativeTo, src ), settings, callback ); + } + else if( isRemotePath( src ) ) + { + getRemote( src, settings, callback ); + } + else + { + try + { + var replacement = getInlineFileContents( src, settings.relativeTo ); + } + catch( err ) + { + return callback( err ); + } + return callback( null, replacement ); + } +}; diff --git a/src/util/isBase64Path.js b/src/util/isBase64Path.js new file mode 100644 index 0000000..3063dd6 --- /dev/null +++ b/src/util/isBase64Path.js @@ -0,0 +1,3 @@ +module.exports = function( url ) { + return /^'?data.*base64/.test( url ); +}; diff --git a/src/util/isRemotePath.js b/src/util/isRemotePath.js new file mode 100644 index 0000000..755a65d --- /dev/null +++ b/src/util/isRemotePath.js @@ -0,0 +1,3 @@ +module.exports = function( url ) { + return /^'?https?:\/\/|^\/\//.test( url ); +}; diff --git a/src/util/search.js b/src/util/search.js new file mode 100644 index 0000000..2646f45 --- /dev/null +++ b/src/util/search.js @@ -0,0 +1,12 @@ +module.exports = function ( re, subject ) { + var result = [], + matches; + + while( ( matches = re.exec( subject ) ) !== null ) { + result.push( matches.map( function( item ) { + return item; + } ) ); + } + + return result; +}; diff --git a/test/cases/404.html b/test/cases/404.html index a4ace3f..69f1217 100644 --- a/test/cases/404.html +++ b/test/cases/404.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/test/cases/css-multiline_out.html b/test/cases/css-multiline_out.html index 1a4756b..8d224a5 100644 --- a/test/cases/css-multiline_out.html +++ b/test/cases/css-multiline_out.html @@ -3,38 +3,38 @@ test - - - - diff --git a/test/cases/css-remote-no-protocol.html b/test/cases/css-remote-no-protocol.html index 4300a28..efdce57 100644 --- a/test/cases/css-remote-no-protocol.html +++ b/test/cases/css-remote-no-protocol.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/test/cases/css-remote-no-protocol_out.html b/test/cases/css-remote-no-protocol_out.html index ea9d47e..0a17af5 100644 --- a/test/cases/css-remote-no-protocol_out.html +++ b/test/cases/css-remote-no-protocol_out.html @@ -1,11 +1,11 @@ - - \ No newline at end of file + diff --git a/test/cases/css-remote-relative-to-url.html b/test/cases/css-remote-relative-to-url.html index 0ea2280..7d6c8b6 100644 --- a/test/cases/css-remote-relative-to-url.html +++ b/test/cases/css-remote-relative-to-url.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/test/cases/css-remote-relative-to-url_out.html b/test/cases/css-remote-relative-to-url_out.html index ea9d47e..0a17af5 100644 --- a/test/cases/css-remote-relative-to-url_out.html +++ b/test/cases/css-remote-relative-to-url_out.html @@ -1,11 +1,11 @@ - - \ No newline at end of file + diff --git a/test/cases/css-remote.html b/test/cases/css-remote.html index c59c1ee..ccf9b7b 100644 --- a/test/cases/css-remote.html +++ b/test/cases/css-remote.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/test/cases/css-remote_out.html b/test/cases/css-remote_out.html index ea9d47e..0a17af5 100644 --- a/test/cases/css-remote_out.html +++ b/test/cases/css-remote_out.html @@ -1,11 +1,11 @@ - - \ No newline at end of file + diff --git a/test/cases/css-transform_out.html b/test/cases/css-transform_out.html index 82066f0..3bca629 100644 --- a/test/cases/css-transform_out.html +++ b/test/cases/css-transform_out.html @@ -1,4 +1,4 @@ - - -