mirror of
https://github.com/remy/inliner.git
synced 2024-09-11 17:25:30 +03:00
Merge pull request #51 from remy/feature/refactor
Full refactor across to cheerio
This commit is contained in:
commit
1eb1238c94
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "node-compress"]
|
||||
path = node-compress
|
||||
url = git://github.com/kkaefer/node-compress.git
|
8
.jscsrc
Normal file
8
.jscsrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"preset": "node-style-guide",
|
||||
"requireCapitalizedComments": null,
|
||||
"requireSpacesInAnonymousFunctionExpression": {
|
||||
"beforeOpeningCurlyBrace": true
|
||||
},
|
||||
"excludeFiles": ["node_modules/**"]
|
||||
}
|
16
.jshintrc
Normal file
16
.jshintrc
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"browser": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"devel": true,
|
||||
"eqeqeq": true,
|
||||
"forin": true,
|
||||
"indent": 2,
|
||||
"noarg": true,
|
||||
"node": true,
|
||||
"quotmark": "single",
|
||||
"undef": true,
|
||||
"strict": false,
|
||||
"unused": true
|
||||
}
|
||||
|
16
.travis.yml
Normal file
16
.travis.yml
Normal file
@ -0,0 +1,16 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
- "0.8"
|
||||
before_install:
|
||||
- npm install -g npm
|
||||
before_script:
|
||||
- npm install
|
||||
notifications:
|
||||
email: false
|
||||
sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
18
README.md
18
README.md
@ -14,13 +14,8 @@ Turns your web page to a single HTML file with everything inlined - perfect for
|
||||
|
||||
Check out a working copy of the source code with [Git](http://git-scm.com), or install `inliner` via [npm](http://npmjs.org) (the recommended way). The latter will also install `inliner` into the system's `bin` path.
|
||||
|
||||
$ npm install inliner -g
|
||||
|
||||
Or
|
||||
|
||||
$ git clone https://github.com/remy/inliner.git
|
||||
|
||||
`inliner` uses a `package.json` to describe the dependancies, and if you install via a github clone, ensure you run `npm install` from the `inliner` directory to install the dependancies (or manually install [jsdom](https://github.com/tmpvar/jsdom "tmpvar/jsdom - GitHub") and [uglify-js](https://github.com/mishoo/UglifyJS "mishoo/UglifyJS - GitHub")).
|
||||
$ npm install -g inliner
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
@ -40,16 +35,16 @@ To use inline inside your own script:
|
||||
// compressed and inlined HTML page
|
||||
console.log(html);
|
||||
});
|
||||
|
||||
|
||||
Or:
|
||||
|
||||
|
||||
var inliner = new Inliner('http://remysharp.com');
|
||||
|
||||
|
||||
inliner.on('progress', function (event) {
|
||||
console.error(event);
|
||||
}).on('end', function (html) {
|
||||
// compressed and inlined HTML page
|
||||
console.log(html);
|
||||
console.log(html);
|
||||
});
|
||||
|
||||
Note that if you include the inliner script via a git submodule, it requires jsdom & uglifyjs to be installed via `npm install jsdom uglify-js`, otherwise you should be good to run.
|
||||
@ -71,4 +66,3 @@ Once you've inlined the crap out of the page, add the `manifest="self.appcache"`
|
||||
## Limitations / Caveats
|
||||
|
||||
- Whitespace compression might get a little heavy handed - all whitespace is collapsed from n spaces to one space.
|
||||
|
108
bin/inliner
108
bin/inliner
@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var path = require('path'),
|
||||
fs = require('fs'),
|
||||
querystring = require('querystring'),
|
||||
util = require('util');
|
||||
|
||||
fs.realpath(__filename, function(error, script) {
|
||||
var servedir, root, port;
|
||||
if (error) throw error;
|
||||
|
||||
var Inliner = require(path.join(path.dirname(script), '../inliner')),
|
||||
program = require('commander');
|
||||
|
||||
program
|
||||
.version(Inliner.version)
|
||||
.usage('[options] http://yoursite.com')
|
||||
.option('-v, --verbose', 'echo on STDERR the progress of inlining')
|
||||
.option('-n, --nocompress', "don't compress CSS or HTML - useful for debugging")
|
||||
.option('-i, --images', "don't encode images - keeps files size small, but more requests")
|
||||
//.option('-s, --share', "create jsbin.com url for HTML page")
|
||||
//.option('-h, --help', "help - you're looking at it");
|
||||
|
||||
program.on('--help', function () {
|
||||
console.log(' Examples:');
|
||||
console.log('');
|
||||
console.log(' $ inliner -v http://twitter.com > twitter.html');
|
||||
console.log(' $ inliner -ni http://twitter.com > twitter.html');
|
||||
console.log('');
|
||||
console.log(' For more details see http://github.com/remy/inliner/');
|
||||
console.log('');
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
var options = Inliner.defaults(),
|
||||
verbose = false,
|
||||
share = false;
|
||||
|
||||
if (program.nocompress) {
|
||||
options.compressCSS = false;
|
||||
options.collapseWhitespace = false;
|
||||
}
|
||||
|
||||
if (program.share) {
|
||||
share = true;
|
||||
|
||||
// because JS Bin is a damn sight easier to debug when it's not all on one line
|
||||
options.compressCSS = false;
|
||||
options.collapseWhitespace = false;
|
||||
}
|
||||
|
||||
options.images = !program.images;
|
||||
verbose = program.verbose;
|
||||
|
||||
if (program.args.length == 0) {
|
||||
// ripped out of commander.js - should really be process.usage()
|
||||
process.stdout.write(program.helpInformation());
|
||||
program.emit('--help');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
var url = program.args[0];
|
||||
|
||||
if (fs.existsSync(url)) {
|
||||
// then it's a file
|
||||
} else if (url.indexOf('http') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
var inliner = new Inliner(url, options, function (html) {
|
||||
if (share) {
|
||||
// post to jsbin
|
||||
var data = querystring.stringify({
|
||||
html: html,
|
||||
javascript: '',
|
||||
format: 'plain',
|
||||
method: 'save'
|
||||
});
|
||||
|
||||
// note: when making a POST request using node, for PHP to pick it up, the content-type is crutial - I never knew that :(
|
||||
var request = Inliner.makeRequest('http://jsbin.com/save', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded', 'content-length': data.length, 'X-Requested-With' : 'XMLHttpRequest' }
|
||||
});
|
||||
request.on('response', function (res) {
|
||||
var body = '';
|
||||
res.on('data', function (chunk) {
|
||||
body += chunk;
|
||||
});
|
||||
res.on('end', function () {
|
||||
util.print(body);
|
||||
});
|
||||
});
|
||||
request.write(data);
|
||||
request.end();
|
||||
} else {
|
||||
// using util.print because console.log evalutes sprintf commands - which we don't want to do
|
||||
util.print(html);
|
||||
}
|
||||
});
|
||||
|
||||
if (verbose) {
|
||||
inliner.on('progress', function (event) {
|
||||
console.error(event);
|
||||
});
|
||||
}
|
||||
});
|
92
cli/index.js
Executable file
92
cli/index.js
Executable file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var alias = {
|
||||
V: 'version',
|
||||
h: 'help',
|
||||
d: 'debug',
|
||||
v: 'verbose',
|
||||
i: 'images',
|
||||
n: 'nocompress',
|
||||
};
|
||||
|
||||
var argv = process.argv.slice(2).reduce(function reduce(acc, arg) {
|
||||
if (arg.indexOf('-') === 0) {
|
||||
arg = arg.slice(1);
|
||||
|
||||
if (alias[arg] !== undefined) {
|
||||
acc[alias[arg]] = true;
|
||||
} else if (arg.indexOf('-') === 0) {
|
||||
acc[arg.slice(1)] = true;
|
||||
} else {
|
||||
acc[arg] = true;
|
||||
}
|
||||
} else {
|
||||
acc._.push(arg);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { _: [] });
|
||||
|
||||
if (argv.debug) {
|
||||
require('debug').enable('inliner');
|
||||
}
|
||||
|
||||
// checks for available update and returns an instance
|
||||
var updateNotifier = require('update-notifier');
|
||||
var pkg = require(__dirname + '/../package.json');
|
||||
var notifier = updateNotifier({ pkg: pkg });
|
||||
if (notifier.update) {
|
||||
// notify using the built-in convenience method
|
||||
notifier.notify();
|
||||
}
|
||||
|
||||
var Inliner = require('../');
|
||||
var url = argv._.shift();
|
||||
|
||||
var argvKeys = Object.keys(argv).filter(function filter(item) {
|
||||
return item !== '_';
|
||||
});
|
||||
|
||||
if (!url && argvKeys.length === 0 || argv.help) {
|
||||
// show USAGE!
|
||||
console.log(require('fs').readFileSync(__dirname + '/../docs/usage.txt', 'utf8'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (argv.version) {
|
||||
console.log(Inliner.version);
|
||||
}
|
||||
|
||||
var options = Inliner.defaults();
|
||||
|
||||
if (argv.nocompress) {
|
||||
options.compressCSS = false;
|
||||
options.collapseWhitespace = false;
|
||||
}
|
||||
|
||||
options.images = !argv.images;
|
||||
|
||||
var inliner = new Inliner(url, argv, function result(error, html) {
|
||||
if (error) {
|
||||
var message = Inliner.errors[error.code] || error.message;
|
||||
console.error(message);
|
||||
|
||||
if (argv.debug) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(html);
|
||||
});
|
||||
|
||||
if (argv.verbose) {
|
||||
inliner.on('progress', function progress(event) {
|
||||
console.error(event);
|
||||
});
|
||||
|
||||
inliner.on('jobs', function jobs(event) {
|
||||
console.error(event);
|
||||
});
|
||||
}
|
19
docs/usage.txt
Normal file
19
docs/usage.txt
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
Usage:
|
||||
|
||||
$ inliner [options] url-or-filename
|
||||
|
||||
Options:
|
||||
|
||||
-h, --help output usage information
|
||||
-V, --version output the version number
|
||||
-v, --verbose echo on STDERR the progress of inlining
|
||||
-n, --nocompress don't compress CSS or HTML - useful for debugging
|
||||
-i, --images don't encode images - keeps files size small, but more requests
|
||||
|
||||
Examples:
|
||||
|
||||
$ inliner -v https://twitter.com > twitter.html
|
||||
$ inliner -ni local-file.html > local-file.min.html
|
||||
|
||||
For more details see http://github.com/remy/inliner/
|
566
inliner.js
566
inliner.js
@ -1,566 +0,0 @@
|
||||
var URL = require('url'),
|
||||
util = require('util'),
|
||||
jsmin = require('./jsmin'),
|
||||
events = require('events'),
|
||||
Buffer = require('buffer').Buffer,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
jsdom = require('jsdom'),
|
||||
uglifyjs = require('uglify-js'),
|
||||
jsp = uglifyjs.parser,
|
||||
pro = uglifyjs.uglify,
|
||||
compress = null, // import only when required - might make the command line tool easier to use
|
||||
http = {
|
||||
http: require('http'),
|
||||
https: require('https')
|
||||
};
|
||||
|
||||
function compressCSS(css) {
|
||||
return css
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/:\s+/g, ':')
|
||||
.replace(/\/\*.*?\*\//g, '')
|
||||
.replace(/\} /g, '}')
|
||||
.replace(/ \{/g, '{')
|
||||
// .replace(/\{ /g, '{')
|
||||
.replace(/; /g, ';')
|
||||
.replace(/\n+/g, '');
|
||||
}
|
||||
|
||||
function removeComments(element) {
|
||||
if (!element || !element.childNodes) return;
|
||||
var nodes = element.childNodes,
|
||||
i = nodes.length;
|
||||
|
||||
while (i--) {
|
||||
if (nodes[i].nodeName === '#comment' && nodes[i].nodeValue.indexOf('[') !== 0) {
|
||||
element.removeChild(nodes[i]);
|
||||
}
|
||||
removeComments(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function Inliner(url, options, callback) {
|
||||
var root = url,
|
||||
inliner = this;
|
||||
|
||||
this.requestCache = {};
|
||||
this.requestCachePending = {};
|
||||
|
||||
// inherit EventEmitter so that we can send events with progress
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
if (typeof options == 'function') {
|
||||
callback = options;
|
||||
options = Inliner.defaults();
|
||||
} else if (options === undefined) {
|
||||
options = Inliner.defaults();
|
||||
}
|
||||
|
||||
inliner.options = options;
|
||||
|
||||
inliner.total = 1;
|
||||
inliner.todo = 1;
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
|
||||
inliner.on('error', function (data) {
|
||||
console.error(data + ' :: ' + url);
|
||||
});
|
||||
|
||||
inliner.get(url, function (html) {
|
||||
inliner.todo--;
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
|
||||
// workaround for https://github.com/tmpvar/jsdom/issues/172
|
||||
// need to remove empty script tags as it casues jsdom to skip the env callback
|
||||
html = html.replace(/<\script(:? type=['"|].*?['"|])><\/script>/ig, '');
|
||||
|
||||
if (!html) {
|
||||
inliner.emit('end', '');
|
||||
callback && callback('');
|
||||
} else {
|
||||
// BIG ASS PROTECTIVE TRY/CATCH - mostly because of this: https://github.com/tmpvar/jsdom/issues/319
|
||||
try {
|
||||
|
||||
jsdom.env(html, [
|
||||
'http://code.jquery.com/jquery.min.js'
|
||||
], {
|
||||
url: url
|
||||
}, function(errors, window) {
|
||||
// remove jQuery that was included with jsdom
|
||||
window.$('script:last').remove();
|
||||
|
||||
var todo = { scripts: true, images: inliner.options.images, links: true, styles: true },
|
||||
assets = {
|
||||
scripts: window.$('script'),
|
||||
images: window.$('img').filter(function(){ return this.src.indexOf('data:') == -1; }),
|
||||
links: window.$('link[rel=stylesheet]'),
|
||||
styles: window.$('style')
|
||||
},
|
||||
breakdown = {},
|
||||
images = {};
|
||||
|
||||
inliner.total = 1;
|
||||
|
||||
for (var key in todo) {
|
||||
if (todo[key] === true && assets[key]) {
|
||||
breakdown[key] = assets[key].length;
|
||||
inliner.total += assets[key].length;
|
||||
inliner.todo += assets[key].length;
|
||||
} else {
|
||||
assets[key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
|
||||
function finished() {
|
||||
var items = 0,
|
||||
html = '';
|
||||
for (var key in breakdown) {
|
||||
items += breakdown[key];
|
||||
}
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
|
||||
if (items === 0) {
|
||||
// manually remove the comments
|
||||
var els = removeComments(window.document.documentElement);
|
||||
// collapse the white space
|
||||
if (inliner.options.collapseWhitespace) {
|
||||
// TODO put white space helper back in
|
||||
window.$('pre').html(function (i, html) {
|
||||
return html.replace(/\n/g, '~~nl~~'); //.replace(/\s/g, '~~s~~');
|
||||
});
|
||||
window.$('textarea').val(function (i, v) {
|
||||
return v.replace(/\n/g, '~~nl~~').replace(/\s/g, '~~s~~');
|
||||
});
|
||||
html = window.document.innerHTML;
|
||||
html = html.replace(/\s+/g, ' ').replace(/~~nl~~/g, '\n').replace(/~~s~~/g, ' ');
|
||||
} else {
|
||||
html = window.document.innerHTML;
|
||||
}
|
||||
|
||||
html = '<!DOCTYPE html>' + html;
|
||||
callback && callback(html);
|
||||
inliner.emit('end', html);
|
||||
} else if (items < 0) {
|
||||
console.log('something went wrong on finish');
|
||||
console.dir(breakdown);
|
||||
}
|
||||
}
|
||||
|
||||
todo.images && assets.images.each(function () {
|
||||
var img = this,
|
||||
resolvedURL = URL.resolve(url, img.src);
|
||||
inliner.get(resolvedURL, { encode: true }, function (dataurl) {
|
||||
if (dataurl) images[img.src] = dataurl;
|
||||
img.src = dataurl;
|
||||
breakdown.images--;
|
||||
inliner.todo--;
|
||||
finished();
|
||||
});
|
||||
});
|
||||
|
||||
todo.styles && assets.styles.each(function () {
|
||||
var style = this;
|
||||
inliner.getImportCSS(root, this.innerHTML, function (css, url) {
|
||||
inliner.getImagesFromCSS(url, css, function (css) {
|
||||
if (inliner.options.compressCSS) inliner.emit('progress', 'compress inline css');
|
||||
window.$(style).text(css);
|
||||
|
||||
breakdown.styles--;
|
||||
inliner.todo--;
|
||||
finished();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
todo.links && assets.links.each(function () {
|
||||
var link = this,
|
||||
linkURL = URL.resolve(url, link.href);
|
||||
|
||||
inliner.get(linkURL, function (css) {
|
||||
inliner.getImagesFromCSS(linkURL, css, function (css) {
|
||||
inliner.getImportCSS(linkURL, css, function (css) {
|
||||
if (inliner.options.compressCSS) inliner.emit('progress', 'compress ' + linkURL);
|
||||
breakdown.links--;
|
||||
inliner.todo--;
|
||||
|
||||
var style = '',
|
||||
media = link.getAttribute('media');
|
||||
|
||||
if (media) {
|
||||
style = '<style>@media ' + media + '{' + css + '}</style>';
|
||||
} else {
|
||||
style = '<style>' + css + '</style>';
|
||||
}
|
||||
|
||||
window.$(link).replaceWith(style);
|
||||
finished();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function scriptsFinished() {
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
if (breakdown.scripts == 0) {
|
||||
// now compress the source JavaScript
|
||||
assets.scripts.each(function () {
|
||||
if (this.innerHTML.trim().length == 0) {
|
||||
// this is an empty script, so throw it away
|
||||
inliner.todo--;
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
return;
|
||||
}
|
||||
|
||||
var $script = window.$(this),
|
||||
src = $script.attr('src'),
|
||||
// note: not using .innerHTML as this coerses & => &
|
||||
orig_code = this.firstChild.nodeValue
|
||||
.replace(/<\/script>/gi, '<\\/script>'),
|
||||
final_code = '';
|
||||
|
||||
// only remove the src if we have a script body
|
||||
if (orig_code) {
|
||||
$script.removeAttr('src');
|
||||
}
|
||||
|
||||
// don't compress already minified code
|
||||
if(!(/\bmin\b/).test(src) && !(/google-analytics/).test(src)) {
|
||||
inliner.todo++;
|
||||
inliner.total++;
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
try {
|
||||
var ast = jsp.parse(orig_code); // parse code and get the initial AST
|
||||
|
||||
ast = pro.ast_mangle(ast); // get a new AST with mangled names
|
||||
ast = pro.ast_squeeze(ast); // get an AST with compression optimizations
|
||||
final_code = pro.gen_code(ast);
|
||||
|
||||
// some protection against putting script tags in the body
|
||||
window.$(this).text(final_code).append('\n');
|
||||
|
||||
if (src) {
|
||||
inliner.emit('progress', 'compress ' + URL.resolve(root, src));
|
||||
} else {
|
||||
inliner.emit('progress', 'compress inline script');
|
||||
}
|
||||
} catch (e) {
|
||||
// console.error(orig_code.indexOf('script>script'));
|
||||
// window.$(this).html(jsmin('', orig_code, 2));
|
||||
console.error('exception on ', src);
|
||||
console.error('exception in ' + src + ': ' + e.message);
|
||||
console.error('>>>>>> ' + orig_code.split('\n')[e.line - 1]);
|
||||
}
|
||||
inliner.todo--;
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
} else if (orig_code) {
|
||||
window.$(this).text(orig_code);
|
||||
// this.innerText = orig_code;
|
||||
}
|
||||
});
|
||||
finished();
|
||||
}
|
||||
}
|
||||
|
||||
// basically this is the jQuery instance we tacked on to the request,
|
||||
// but we're just being extra sure before we do zap it out
|
||||
todo.scripts && assets.scripts.each(function () {
|
||||
var $script = window.$(this),
|
||||
scriptURL = URL.resolve(url, (this.src||"").toString());
|
||||
|
||||
if (!this.src || scriptURL.indexOf('google-analytics.com') !== -1) { // ignore google
|
||||
breakdown.scripts--;
|
||||
inliner.todo--;
|
||||
scriptsFinished();
|
||||
} else if (this.src) {
|
||||
inliner.get(scriptURL, { not: 'text/html' }, function (data) {
|
||||
// catches an exception that was being thrown, but script escaping wasn't being caught
|
||||
if (data) $script.text(data.replace(/<\/script>/gi, '<\\/script>')); //.replace(/\/\/.*$\n/g, ''));
|
||||
// $script.before('<!-- ' + scriptURL + ' -->');
|
||||
breakdown.scripts--;
|
||||
inliner.todo--;
|
||||
scriptsFinished();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// edge case - if there's no images, nor scripts, nor links - we call finished manually
|
||||
if (assets.links.length == 0 &&
|
||||
assets.styles.length == 0 &&
|
||||
assets.images.length == 0 &&
|
||||
assets.scripts.length == 0) {
|
||||
finished();
|
||||
}
|
||||
|
||||
/** Inliner jobs:
|
||||
* 1. get all inline images and base64 encode
|
||||
* 2. get all external style sheets and move to inline
|
||||
* 3. get all image references in CSS and base64 encode and replace urls
|
||||
* 4. get all external scripts and move to inline
|
||||
* 5. compress JavaScript
|
||||
* 6. compress CSS & support media queries
|
||||
* 7. compress HTML (/>\s+</g, '> <');
|
||||
*
|
||||
* FUTURE ITEMS:
|
||||
* - support for @import
|
||||
* - javascript validation - i.e. not throwing errors
|
||||
*/
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
inliner.emit('error', 'Fatal error parsing HTML - exiting');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
util.inherits(Inliner, events.EventEmitter);
|
||||
|
||||
Inliner.version = Inliner.prototype.version = JSON.parse(require('fs').readFileSync(__dirname + '/package.json').toString()).version;
|
||||
|
||||
Inliner.prototype.get = function (url, options, callback) {
|
||||
// support no options being passed in
|
||||
if (typeof options == 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
// if we've cached this request in the past, just return the cached content
|
||||
if (this.requestCache[url] !== undefined) {
|
||||
this.emit('progress', 'cached ' + url);
|
||||
return callback && callback(this.requestCache[url]);
|
||||
} else if (this.requestCachePending[url] !== undefined) {
|
||||
this.requestCachePending[url].push(callback);
|
||||
return true;
|
||||
} else {
|
||||
this.requestCachePending[url] = [callback];
|
||||
}
|
||||
|
||||
var inliner = this;
|
||||
|
||||
// TODO remove the sync
|
||||
if (fs.existsSync(url)) {
|
||||
// then we're dealing with a file
|
||||
fs.readFile(url, 'utf8', function (err, body) {
|
||||
inliner.requestCache[url] = body;
|
||||
inliner.requestCachePending[url].forEach(function (callback, i) {
|
||||
if (i == 0 && body) {
|
||||
inliner.emit('progress', (options.encode ? 'encode' : 'get') + ' ' + url);
|
||||
} else if (body) {
|
||||
inliner.emit('progress', 'cached ' + url);
|
||||
}
|
||||
callback && callback(body);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwis continue and create a new web request
|
||||
var request = makeRequest(url),
|
||||
body = '';
|
||||
|
||||
// this tends to occur when we can't connect to the url - i.e. target is down
|
||||
// note that the main inliner app handles sending the error back to the client
|
||||
request.on('error', function (error) {
|
||||
console.error(error.message, url);
|
||||
callback && callback('');
|
||||
});
|
||||
|
||||
|
||||
request.on('response', function (res) {
|
||||
var gunzip;
|
||||
|
||||
// if we get a gzip header, then first we attempt to load the node-compress library
|
||||
// ...which annoyingly isn't supported anymore (maybe I should fork it...).
|
||||
// once loaded, we set up event listeners to handle data coming in and do a little
|
||||
// dance with the response object - which I'll explain... ==>
|
||||
if (res.headers['content-encoding'] == 'gzip') {
|
||||
console.error('loading gzip library');
|
||||
if (compress === null) {
|
||||
try {
|
||||
compress = require('./node-compress/lib/compress/');
|
||||
} catch (e) {
|
||||
console.error(url + ' sent gzipped header\nFailed to load node-compress - see http://github.com/remy/inliner for install directions. \nexiting');
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
gunzip = new compress.GunzipStream();
|
||||
|
||||
// the data event is triggered by writing to the gunzip object when the response
|
||||
// receives data (yeah, further down).
|
||||
// once all the data has finished (triggerd by the response end event), we undefine
|
||||
// the gunzip object and re-trigger the response end event. By this point, we've
|
||||
// decompressed the gzipped content, and it's human readable again.
|
||||
gunzip.on('data', function (chunk) {
|
||||
body += chunk;
|
||||
}).on('end', function () {
|
||||
gunzip = undefined;
|
||||
res.emit('end');
|
||||
});
|
||||
}
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
// only process data if we have a 200 ok
|
||||
if (res.statusCode == 200) {
|
||||
if (gunzip) {
|
||||
gunzip.write(chunk);
|
||||
} else {
|
||||
body += chunk;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// required if we're going to base64 encode the content later on
|
||||
if (options.encode) {
|
||||
res.setEncoding('binary');
|
||||
}
|
||||
|
||||
res.on('end', function () {
|
||||
if (gunzip) {
|
||||
gunzip.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
inliner.emit('progress', 'get ' + res.statusCode + ' on ' + url);
|
||||
body = ''; // ?
|
||||
callback && callback(body);
|
||||
} else if (res.headers['location']) {
|
||||
return inliner.get(res.headers['location'], options, callback);
|
||||
} else {
|
||||
if (options && options.not) {
|
||||
if (res.headers['content-type'].indexOf(options.not) !== -1) {
|
||||
body = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (options.encode && res.statusCode == 200) {
|
||||
body = 'data:' + res.headers['content-type'] + ';base64,' + new Buffer(body, 'binary').toString('base64');
|
||||
}
|
||||
|
||||
inliner.requestCache[url] = body;
|
||||
inliner.requestCachePending[url].forEach(function (callback, i) {
|
||||
if (i == 0 && body && res.statusCode == 200) {
|
||||
inliner.emit('progress', (options.encode ? 'encode' : 'get') + ' ' + url);
|
||||
} else if (body) {
|
||||
inliner.emit('progress', 'cached ' + url);
|
||||
}
|
||||
callback && callback(body);
|
||||
});
|
||||
}
|
||||
});
|
||||
}).end();
|
||||
};
|
||||
|
||||
Inliner.prototype.getImagesFromCSS = function (rooturl, rawCSS, callback) {
|
||||
if (this.options.images === false) {
|
||||
callback && callback(rawCSS);
|
||||
return;
|
||||
}
|
||||
|
||||
var inliner = this,
|
||||
images = {},
|
||||
urlMatch = /url\((?:['"]*)(?!['"]*data:)(.*?)(?:['"]*)\)/g,
|
||||
singleURLMatch = /url\(\s*(?:['"]*)(?!['"]*data:)(.*?)(?:['"]*)\s*\)/,
|
||||
matches = rawCSS.match(urlMatch),
|
||||
imageCount = matches === null ? 0 : matches.length; // TODO check!
|
||||
|
||||
inliner.total += imageCount;
|
||||
inliner.todo += imageCount;
|
||||
|
||||
function checkFinished() {
|
||||
inliner.emit('jobs', (inliner.total - inliner.todo) + '/' + inliner.total);
|
||||
if (imageCount < 0) {
|
||||
console.log('something went wrong :-S');
|
||||
} else if (imageCount == 0) {
|
||||
callback(rawCSS.replace(urlMatch, function (m, url) {
|
||||
return 'url(' + images[url] + ')';
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (imageCount) {
|
||||
matches.forEach(function (url) {
|
||||
url = url.match(singleURLMatch)[1];
|
||||
var resolvedURL = URL.resolve(rooturl, url);
|
||||
if (images[url] === undefined) {
|
||||
inliner.get(resolvedURL, { encode: true }, function (dataurl) {
|
||||
imageCount--;
|
||||
inliner.todo--;
|
||||
if (images[url] === undefined) images[url] = dataurl;
|
||||
|
||||
checkFinished();
|
||||
});
|
||||
} else {
|
||||
imageCount--;
|
||||
inliner.todo--;
|
||||
checkFinished();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(rawCSS);
|
||||
}
|
||||
};
|
||||
|
||||
Inliner.prototype.getImportCSS = function (rooturl, css, callback) {
|
||||
// if (typeof css == 'function') {
|
||||
// callback = css;
|
||||
// rooturl = '';
|
||||
// }
|
||||
|
||||
var position = css.indexOf('@import'),
|
||||
inliner = this;
|
||||
|
||||
if (position !== -1) {
|
||||
var match = css.match(/@import\s*(.*)/);
|
||||
|
||||
if (match !== null && match.length) {
|
||||
var url = match[1].replace(/url/, '').replace(/['}"]/g, '').replace(/;/, '').trim().split(' '); // clean up
|
||||
// if url has a length > 1, then we have media types to target
|
||||
var resolvedURL = URL.resolve(rooturl, url[0]);
|
||||
inliner.get(resolvedURL, function (importedCSS) {
|
||||
inliner.emit('progress', 'import ' + resolvedURL);
|
||||
if (url.length > 1) {
|
||||
url.shift();
|
||||
importedCSS = '@media ' + url.join(' ') + '{' + importedCSS + '}';
|
||||
}
|
||||
|
||||
css = css.replace(match[0], importedCSS);
|
||||
inliner.getImportCSS(rooturl, css, callback);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (inliner.options.compressCSS) css = compressCSS(css);
|
||||
callback(css, rooturl);
|
||||
}
|
||||
};
|
||||
|
||||
Inliner.defaults = function () { return { compressCSS: true, collapseWhitespace: true, images: true }; };
|
||||
|
||||
var makeRequest = Inliner.makeRequest = function (url, extraOptions) {
|
||||
var oURL = URL.parse(url),
|
||||
options = {
|
||||
host: oURL.hostname,
|
||||
port: oURL.port === undefined ? (oURL.protocol+'').indexOf('https') === 0 ? 443 : 80 : oURL.port,
|
||||
path: (oURL.pathname || '/') + (oURL.search || ''), // note 0.5.0pre doesn't fill pathname if missing
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
for (var key in extraOptions) {
|
||||
options[key] = extraOptions[key];
|
||||
}
|
||||
|
||||
return http[oURL.protocol.slice(0, -1) || 'http'].request(options);
|
||||
};
|
||||
|
||||
module.exports = Inliner;
|
||||
|
||||
if (!module.parent) {
|
||||
// if this module isn't being included in a larger app, defer to the
|
||||
// bin/inliner for the help options
|
||||
require('./bin/inliner');
|
||||
}
|
16
jquery.min.js
vendored
16
jquery.min.js
vendored
File diff suppressed because one or more lines are too long
90
lib/css.js
Normal file
90
lib/css.js
Normal file
@ -0,0 +1,90 @@
|
||||
var match = /url\((?:['"]*)(?!['"]*data:)(.*?)(?:['"]*)\)/g;
|
||||
|
||||
module.exports = {
|
||||
getImages: getImages,
|
||||
getImports: getImports,
|
||||
};
|
||||
|
||||
var Promise = require('es6-promise').Promise; // jshint ignore:line
|
||||
var debug = require('debug')('inliner');
|
||||
|
||||
function getImages(root, css) {
|
||||
var inliner = this;
|
||||
var singleURLMatch = /url\(\s*(?:['"]*)(?!['"]*data:)(.*?)(?:['"]*)\s*\)/;
|
||||
var matches = css.match(match) || [];
|
||||
var images = matches.map(function eachURL(url) {
|
||||
var source = url.match(singleURLMatch)[1];
|
||||
return {
|
||||
source: source,
|
||||
resolved: inliner.resolve(root, source),
|
||||
};
|
||||
});
|
||||
|
||||
debug('adding %s CSS assets', images.length);
|
||||
inliner.jobs.add('images', images.length);
|
||||
|
||||
return Promise.all(images.map(function map(url) {
|
||||
return inliner.image(url.resolved).then(function then(dataURL) {
|
||||
inliner.jobs.done.images();
|
||||
css = css.replace(new RegExp(url.source, 'g'), function replace() {
|
||||
return dataURL;
|
||||
});
|
||||
return css;
|
||||
});
|
||||
})).then(function then() {
|
||||
return css;
|
||||
});
|
||||
}
|
||||
|
||||
function getImports(root, css) {
|
||||
// change to a string in case the CSS is a buffer, which is the case
|
||||
// when we're reading off the local file system
|
||||
if (typeof css !== 'string') {
|
||||
css = css.toString();
|
||||
}
|
||||
var position = css.indexOf('@import');
|
||||
var inliner = this;
|
||||
|
||||
if (position !== -1) {
|
||||
inliner.jobs.add('link', 1);
|
||||
var match = (css.match(/@import\s*(.*)/) || [null, ''])[1];
|
||||
var url = match.replace(/url/, '')
|
||||
.replace(/['}"()]/g, '')
|
||||
.replace(/;/, '')
|
||||
.trim()
|
||||
.split(' '); // clean up
|
||||
|
||||
// if url has a length > 1, then we have media types to target
|
||||
var resolvedURL = inliner.resolve(root, url[0]);
|
||||
return inliner.get(resolvedURL).then(function then(res) {
|
||||
var importedCSS = res.body;
|
||||
inliner.jobs.done.links();
|
||||
inliner.emit('progress', 'import ' + resolvedURL);
|
||||
if (url.length > 1) {
|
||||
url.shift();
|
||||
importedCSS = '@media ' + url.join(' ') + '{' + importedCSS + '}';
|
||||
}
|
||||
|
||||
css = css.replace('@import ' + match, importedCSS);
|
||||
return getImports.call(inliner, root, css);
|
||||
});
|
||||
} else {
|
||||
if (inliner.options.compressCSS) {
|
||||
inliner.emit('progress', 'compress css');
|
||||
css = compress(css);
|
||||
}
|
||||
return Promise.resolve(css);
|
||||
}
|
||||
}
|
||||
|
||||
function compress(css) {
|
||||
return css
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/:\s+/g, ':')
|
||||
.replace(/\/\*.*?\*\//g, '')
|
||||
.replace(/\} /g, '}')
|
||||
.replace(/ \{/g, '{')
|
||||
// .replace(/\{ /g, '{')
|
||||
.replace(/; /g, ';')
|
||||
.replace(/\n+/g, '');
|
||||
}
|
3
lib/errors.js
Normal file
3
lib/errors.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
ENOTFOUND: 'The URL could not be loaded, possibly a typo, or no internet connection',
|
||||
};
|
23
lib/find-assets.js
Normal file
23
lib/find-assets.js
Normal file
@ -0,0 +1,23 @@
|
||||
module.exports = findAssets;
|
||||
|
||||
var cheerio = require('cheerio');
|
||||
var debug = require('debug')('inliner');
|
||||
|
||||
function findAssets(html) {
|
||||
var $ = cheerio.load(html);
|
||||
debug('loaded DOM');
|
||||
|
||||
|
||||
var images = $('img');
|
||||
var links = $('link[rel=stylesheet]');
|
||||
var styles = $('style');
|
||||
var scripts = $('script');
|
||||
|
||||
return {
|
||||
$: $,
|
||||
images: images,
|
||||
links: links,
|
||||
styles: styles,
|
||||
scripts: scripts,
|
||||
};
|
||||
}
|
34
lib/get.js
Normal file
34
lib/get.js
Normal file
@ -0,0 +1,34 @@
|
||||
var request = require('request');
|
||||
var assign = require('lodash.assign');
|
||||
var debug = require('debug')('inliner');
|
||||
|
||||
var cache = {};
|
||||
|
||||
module.exports = function get(url, options) {
|
||||
debug('request %s', url);
|
||||
|
||||
var settings = assign({}, options, {
|
||||
followRedirect: true,
|
||||
});
|
||||
|
||||
if (cache[url]) {
|
||||
return Promise.resolve(cache[url]);
|
||||
}
|
||||
|
||||
return new Promise(function promise(resolve, reject) {
|
||||
request(url, settings, function response(error, res, body) {
|
||||
if (error) {
|
||||
debug('request failed: %s', error.message);
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
debug('response: %s %s', res.statusCode, url);
|
||||
if (!error && res.statusCode === 200) {
|
||||
cache[url] = { headers: res.headers, body: body };
|
||||
resolve(cache[url]);
|
||||
} else {
|
||||
return reject(new Error(res.statusCode));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
15
lib/image.js
Normal file
15
lib/image.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = image;
|
||||
|
||||
var debug = require('debug')('inliner');
|
||||
|
||||
function image(url) {
|
||||
this.emit('progress', 'get image ' + url);
|
||||
return this.get(url, { encoding: 'binary' }).then(function then(res) {
|
||||
debug('image loaded: %s', url);
|
||||
var buffer = new Buffer(res.body, 'binary').toString('base64');
|
||||
return 'data:' + res.headers['content-type'] + ';base64,' + buffer;
|
||||
}).catch(function errorHandle(error) {
|
||||
debug('image %s failed to load', url, error);
|
||||
throw error;
|
||||
});
|
||||
}
|
363
lib/index.js
Normal file
363
lib/index.js
Normal file
@ -0,0 +1,363 @@
|
||||
module.exports = Inliner;
|
||||
|
||||
var debug = require('debug')('inliner');
|
||||
var events = require('events');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var fs = require('then-fs');
|
||||
var mime = require('mime');
|
||||
var assign = require('lodash.assign');
|
||||
var forEach = require('lodash.foreach');
|
||||
var Promise = require('es6-promise').Promise; // jshint ignore:line
|
||||
var request = require('./get');
|
||||
var findAssets = require('./find-assets');
|
||||
var version = require(path.resolve(__dirname, '..', 'package.json')).version;
|
||||
|
||||
function Inliner(url, options, callback) {
|
||||
var inliner = this;
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
this.callback = function wrapper(error, res) {
|
||||
// noop the callback once it's fired
|
||||
inliner.callback = function noop() {
|
||||
inliner.emit('error', 'callback fired again');
|
||||
};
|
||||
|
||||
callback(error, res);
|
||||
};
|
||||
this.options = assign({}, options, Inliner.defaults());
|
||||
this.jobs = {
|
||||
total: 0,
|
||||
todo: 0,
|
||||
breakdown: {
|
||||
html: 0,
|
||||
js: 0,
|
||||
links: 0,
|
||||
styles: 0,
|
||||
images: 0,
|
||||
},
|
||||
add: this.addJob.bind(this),
|
||||
done: {
|
||||
html: this.completeJob.bind(this, 'html'),
|
||||
js: this.completeJob.bind(this, 'js'),
|
||||
images: this.completeJob.bind(this, 'images'),
|
||||
links: this.completeJob.bind(this, 'links'),
|
||||
styles: this.completeJob.bind(this, 'styles'),
|
||||
},
|
||||
};
|
||||
this.isFile = false;
|
||||
|
||||
this.on('error', function localErrorHandler(event) {
|
||||
inliner.callback(event);
|
||||
});
|
||||
|
||||
// this allows the user code to get the object back before
|
||||
// it starts firing events
|
||||
if (this.url) {
|
||||
if (typeof setImmediate === 'undefined') {
|
||||
global.setImmediate = function setImmediatePolyfill(fn) {
|
||||
// :-/
|
||||
setTimeout(fn, 0);
|
||||
}
|
||||
}
|
||||
global.setImmediate(this.main.bind(this));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
util.inherits(Inliner, events.EventEmitter);
|
||||
Inliner.prototype.updateTodo = updateTodo;
|
||||
Inliner.prototype.addJob = addJob;
|
||||
Inliner.prototype.completeJob = completeJob;
|
||||
Inliner.prototype.version = version;
|
||||
Inliner.prototype.cssImages = require('./css').getImages;
|
||||
Inliner.prototype.cssImports = require('./css').getImports;
|
||||
Inliner.prototype.image = require('./image');
|
||||
Inliner.prototype.uglify = require('./javascript');
|
||||
Inliner.prototype.resolve = resolve;
|
||||
Inliner.prototype.removeComments = removeComments;
|
||||
Inliner.prototype.get = get;
|
||||
Inliner.prototype.main = main;
|
||||
|
||||
// static properties and methods
|
||||
Inliner.version = version;
|
||||
Inliner.errors = require('./errors');
|
||||
Inliner.defaults = function() {
|
||||
return {
|
||||
images: true,
|
||||
compressCSS: true,
|
||||
collapseWhitespace: true,
|
||||
};
|
||||
};
|
||||
|
||||
// main thread of functionality that does all the inlining
|
||||
function main() {
|
||||
var inliner = this;
|
||||
var url = this.url;
|
||||
|
||||
fs.exists(url)
|
||||
.then(function exists(isFile) {
|
||||
if (!isFile) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
debug('inlining file');
|
||||
|
||||
inliner.isFile = true;
|
||||
return url;
|
||||
})
|
||||
.catch(function isUrl() {
|
||||
// check for protocol on URL
|
||||
if (url.indexOf('http') !== 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
|
||||
inliner.url = url;
|
||||
|
||||
debug('inlining url');
|
||||
return url;
|
||||
})
|
||||
.then(inliner.get.bind(this))
|
||||
.then(inliner.jobs.add('html'))
|
||||
.then(function processHTML(res) {
|
||||
inliner.jobs.done.html();
|
||||
debug('processing HTML');
|
||||
|
||||
var todo = findAssets(res.body);
|
||||
var $ = todo.$;
|
||||
delete todo.$;
|
||||
|
||||
forEach(todo, function forEach(todo, key) {
|
||||
if (key === 'images' && !inliner.options.images) {
|
||||
// skip images if the user doesn't want them
|
||||
delete todo.images;
|
||||
debug('skipping images');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!todo.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
inliner.jobs.add(key, todo.length);
|
||||
});
|
||||
|
||||
var promises = [];
|
||||
|
||||
if (todo.images.length) {
|
||||
var imagePromises = todo.images.map(function images(i, image) {
|
||||
var url = inliner.resolve(inliner.url, $(image).attr('src'));
|
||||
return inliner.image(url).then(function then(dataURL) {
|
||||
$(image).attr('src', dataURL);
|
||||
}).then(inliner.jobs.done.images);
|
||||
}).get();
|
||||
|
||||
[].push.apply(promises, imagePromises);
|
||||
}
|
||||
|
||||
if (todo.links.length) {
|
||||
debug('start %s links', todo.links.length);
|
||||
var linkPromises = todo.links.map(function links(i, link) {
|
||||
var url = $(link).attr('href');
|
||||
if (url.indexOf('http') !== 0) {
|
||||
url = inliner.resolve(inliner.url, url);
|
||||
}
|
||||
inliner.emit('progress', 'processing external css ' + url);
|
||||
return inliner.get(url).then(function then(res) {
|
||||
var css = res.body;
|
||||
inliner.jobs.done.links();
|
||||
return inliner.cssImports(url, css).then(inliner.cssImages.bind(inliner, url));
|
||||
}).then(function then(css) {
|
||||
$(link).replaceWith('<style>' + css + '</style>');
|
||||
});
|
||||
});
|
||||
|
||||
[].push.apply(promises, linkPromises);
|
||||
}
|
||||
|
||||
if (todo.styles.length) {
|
||||
debug('start %s styles', todo.styles.length);
|
||||
var stylePromises = todo.styles.map(function links(i, style) {
|
||||
var css = $(style).html();
|
||||
inliner.emit('progress', 'processing inline css');
|
||||
return inliner.cssImports(url, css)
|
||||
.then(inliner.cssImages.bind(inliner, url))
|
||||
.then(function then(css) {
|
||||
$(style).html(css);
|
||||
});
|
||||
});
|
||||
|
||||
[].push.apply(promises, stylePromises);
|
||||
}
|
||||
|
||||
if (todo.scripts.length) {
|
||||
debug('start %s scripts', todo.scripts.length);
|
||||
var scriptPromises = todo.scripts.map(function links(i, script) {
|
||||
var $script = $(script);
|
||||
var src = $script.attr('src');
|
||||
var source = $(script).html();
|
||||
|
||||
var promise;
|
||||
|
||||
// ext script
|
||||
if (src) {
|
||||
$script.removeAttr('src');
|
||||
if (src.indexOf('.min.') !== -1) {
|
||||
inliner.emit('progress', 'skipping minified script ' + src);
|
||||
// ignore scripts with .min. in them - i.e. avoid minify
|
||||
// scripts that are already minifed
|
||||
return;
|
||||
} else if (src.indexOf('google-analytics') !== -1) {
|
||||
inliner.emit('progress', 'skipping analytics script');
|
||||
// ignore analytics
|
||||
return;
|
||||
}
|
||||
|
||||
var url = src;
|
||||
if (url.indexOf('http') !== 0) {
|
||||
url = inliner.resolve(inliner.url, url);
|
||||
}
|
||||
|
||||
promise = inliner.get(url);
|
||||
} else {
|
||||
inliner.emit('progress', 'processing inline script');
|
||||
promise = Promise.resolve({
|
||||
body: source,
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(inliner.uglify.bind(inliner)).then(function then(res) {
|
||||
debug('uglify: %s', res);
|
||||
$script.html(res);
|
||||
});
|
||||
});
|
||||
|
||||
[].push.apply(promises, scriptPromises);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(function then() {
|
||||
var html = '';
|
||||
inliner.removeComments($(':root')[0], $);
|
||||
// collapse the white space
|
||||
if (inliner.options.collapseWhitespace) {
|
||||
// TODO put white space helper back in
|
||||
$('pre').html(function tidyPre(i, html) {
|
||||
return html.replace(/\n/g, '~~nl~~');
|
||||
});
|
||||
$('textarea').val(function tidyTextarea(i, v) {
|
||||
return v.replace(/\n/g, '~~nl~~').replace(/\s/g, '~~s~~');
|
||||
});
|
||||
|
||||
html = $.html()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/~~nl~~/g, '\n')
|
||||
.replace(/~~s~~/g, ' ');
|
||||
} else {
|
||||
html = $.html();
|
||||
}
|
||||
|
||||
return html;
|
||||
});
|
||||
|
||||
})
|
||||
.then(function then(html) {
|
||||
inliner.callback(null, html);
|
||||
})
|
||||
.catch(function errHandler(error) {
|
||||
debug('fail', error.stack);
|
||||
inliner.callback(error);
|
||||
inliner.emit('error', error);
|
||||
});
|
||||
}
|
||||
|
||||
function get(url) {
|
||||
this.emit('progress', 'loading ' + url);
|
||||
if (this.isFile && url.indexOf('http') !== 0) {
|
||||
debug('inliner.get file: %s', url);
|
||||
return fs.readFile(url).then(function read(body) {
|
||||
return {
|
||||
body: body,
|
||||
headers: {
|
||||
'content-type': mime.lookup(url),
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
debug('inliner.get url: %s', url);
|
||||
return request(url);
|
||||
}
|
||||
}
|
||||
|
||||
function removeComments(element, $) {
|
||||
if (!element || !element.childNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nodes = element.childNodes;
|
||||
var i = nodes.length;
|
||||
|
||||
while (i--) {
|
||||
if (nodes[i].type === 'comment' && nodes[i].nodeValue.indexOf('[') !== 0) {
|
||||
$(nodes[i]).remove();
|
||||
}
|
||||
removeComments(nodes[i], $);
|
||||
}
|
||||
}
|
||||
|
||||
function resolve(from, to) {
|
||||
if (!to) {
|
||||
to = from;
|
||||
from = this.url;
|
||||
}
|
||||
|
||||
if (!this.isFile || from.indexOf('http') === 0) {
|
||||
return require('url').resolve(from, to);
|
||||
} else {
|
||||
var path = require('path');
|
||||
var base = path.dirname(from);
|
||||
return path.resolve(base, to);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function updateTodo() {
|
||||
this.jobs.todo = this.jobs.breakdown.html +
|
||||
this.jobs.breakdown.js +
|
||||
this.jobs.breakdown.links +
|
||||
this.jobs.breakdown.styles +
|
||||
this.jobs.breakdown.images +
|
||||
0;
|
||||
|
||||
this.emit('jobs', (this.jobs.total - this.jobs.todo) + '/' + this.jobs.total);
|
||||
}
|
||||
|
||||
function addJob(type) {
|
||||
var n = typeof arguments[1] === 'number' ? arguments[1] : 1;
|
||||
this.jobs.breakdown[type] += n;
|
||||
this.jobs.total += n;
|
||||
this.updateTodo();
|
||||
|
||||
debug('%s: %s', type, n);
|
||||
|
||||
// this allows me to include addJob as part of a promise chain
|
||||
return arguments[1];
|
||||
}
|
||||
|
||||
function completeJob(type) {
|
||||
this.jobs.breakdown[type]--;
|
||||
this.updateTodo();
|
||||
|
||||
// this allows me to include addJob as part of a promise chain
|
||||
return arguments[1];
|
||||
}
|
13
lib/javascript.js
Normal file
13
lib/javascript.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = uglify;
|
||||
|
||||
var UglifyJS = require('uglify-js');
|
||||
|
||||
function uglify(source) {
|
||||
this.emit('progress', 'compressing javascript');
|
||||
if (source.body) {
|
||||
source = source.body;
|
||||
}
|
||||
return UglifyJS.minify(source, {
|
||||
fromString: true,
|
||||
}).code;
|
||||
}
|
@ -1 +0,0 @@
|
||||
Subproject commit 39b5c39140f8ad088503de66546a15c55c2b951f
|
38
package.json
38
package.json
@ -1,24 +1,42 @@
|
||||
{
|
||||
"name": "inliner",
|
||||
"version": "0.1.14",
|
||||
"version": "1.0.0",
|
||||
"description": "Utility to inline images, CSS and JavaScript for a web page - useful for mobile sites",
|
||||
"homepage": "http://github.com/remy/inliner",
|
||||
"main": "inliner",
|
||||
"keywords": ["mobile", "inline", "production", "build", "minify"],
|
||||
"author": {
|
||||
"name": "Remy Sharp",
|
||||
"web": "http://github.com/remy"
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "tape test/*.test.js --cov | tap-spec"
|
||||
},
|
||||
"keywords": [
|
||||
"mobile",
|
||||
"inline",
|
||||
"production",
|
||||
"build",
|
||||
"minify"
|
||||
],
|
||||
"author": "Remy Sharp",
|
||||
"dependencies": {
|
||||
"jsdom": "0.6.5",
|
||||
"uglify-js": "1.2.2",
|
||||
"commander": "0.5.1"
|
||||
"cheerio": "^0.19.0",
|
||||
"debug": "^2.2.0",
|
||||
"es6-promise": "^2.3.0",
|
||||
"lodash.assign": "^3.2.0",
|
||||
"lodash.foreach": "^3.0.3",
|
||||
"mime": "^1.3.4",
|
||||
"request": "^2.60.0",
|
||||
"then-fs": "^2.0.0",
|
||||
"uglify-js": "^2.4.24",
|
||||
"update-notifier": "^0.5.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/remy/inliner.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"inliner": "bin/inliner"
|
||||
"inliner": "cli/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap-spec": "^4.0.2",
|
||||
"tape": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
1
test/fixtures/css-ext.result.html
vendored
Normal file
1
test/fixtures/css-ext.result.html
vendored
Normal file
File diff suppressed because one or more lines are too long
13
test/fixtures/css-ext.src.html
vendored
Normal file
13
test/fixtures/css-ext.src.html
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>External script + css</title>
|
||||
<link href="https://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
|
||||
<script src="https://code.jquery.com/mobile/1.4.2/jquery.mobile-1.4.2.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
1
test/fixtures/css-import.result.html
vendored
Normal file
1
test/fixtures/css-import.result.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>inline style</title> <style> p{ font-size:10px;} p{ font-size:10px;} p{ font-size:10px;} @media screen and orientation:landscape{p{ font-size:10px;}}</style> </head> <body> </body> </html>
|
20
test/fixtures/css-import.src.html
vendored
Normal file
20
test/fixtures/css-import.src.html
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>inline style</title>
|
||||
<style>
|
||||
/* one */
|
||||
@import url('import.css');
|
||||
/* two */
|
||||
@import url("import.css");
|
||||
/* three */
|
||||
@import 'import.css';
|
||||
/* four */
|
||||
@import url('import.css') screen and (orientation:landscape);
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
1
test/fixtures/image-css.result.html
vendored
Normal file
1
test/fixtures/image-css.result.html
vendored
Normal file
File diff suppressed because one or more lines are too long
17
test/fixtures/image-css.src.html
vendored
Normal file
17
test/fixtures/image-css.src.html
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>css image</title>
|
||||
<style>
|
||||
#image {
|
||||
background: url(image.jpg) no-repeat;
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="image"></div>
|
||||
</body>
|
||||
</html>
|
1
test/fixtures/image-inline.result.html
vendored
Normal file
1
test/fixtures/image-inline.result.html
vendored
Normal file
File diff suppressed because one or more lines are too long
10
test/fixtures/image-inline.src.html
vendored
Normal file
10
test/fixtures/image-inline.src.html
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>inline image</title>
|
||||
</head>
|
||||
<body>
|
||||
<img src="image.jpg">
|
||||
</body>
|
||||
</html>
|
BIN
test/fixtures/image.jpg
vendored
Normal file
BIN
test/fixtures/image.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
3
test/fixtures/import.css
vendored
Normal file
3
test/fixtures/import.css
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
p {
|
||||
font-size: 10px;
|
||||
}
|
1
test/fixtures/kitchen-sink.result.html
vendored
Normal file
1
test/fixtures/kitchen-sink.result.html
vendored
Normal file
File diff suppressed because one or more lines are too long
35
test/fixtures/kitchen-sink.src.html
vendored
Normal file
35
test/fixtures/kitchen-sink.src.html
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>full combo</title>
|
||||
<link href="import.css" rel="stylesheet">
|
||||
<style>
|
||||
@import url("import.css");
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
#icon {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background: url(image.jpg) no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>function doit(window) {
|
||||
var foo = 'remy';
|
||||
var bar = window.bar = 'sharp';
|
||||
return foo + bar.split('').reverse().join('');
|
||||
}
|
||||
|
||||
console.log(doit(window));
|
||||
</script>
|
||||
<script>console.log('Hello world');</script>
|
||||
<img src="image.jpg" title="Remy Sharp">
|
||||
<div id="icon"></div>
|
||||
</body>
|
||||
</html>
|
1
test/fixtures/script-ext.result.html
vendored
Normal file
1
test/fixtures/script-ext.result.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>External script</title> <script></script> </head> <body> </body> </html>
|
11
test/fixtures/script-ext.src.html
vendored
Normal file
11
test/fixtures/script-ext.src.html
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>External script</title>
|
||||
<script src="https://cdn.rawgit.com/zloirock/core-js/master/client/shim.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
1
test/fixtures/script-inline.result.html
vendored
Normal file
1
test/fixtures/script-inline.result.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>inline script</title> </head> <body> <script>function doit(o){var r="remy",e=o.bar="sharp";return r+e.split("").reverse().join("")}console.log(doit(window));</script> <script>console.log("Hello world");</script> </body> </html>
|
18
test/fixtures/script-inline.src.html
vendored
Normal file
18
test/fixtures/script-inline.src.html
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>inline script</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>function doit(window) {
|
||||
var foo = 'remy';
|
||||
var bar = window.bar = 'sharp';
|
||||
return foo + bar.split('').reverse().join('');
|
||||
}
|
||||
|
||||
console.log(doit(window));
|
||||
</script>
|
||||
<script>console.log('Hello world');</script>
|
||||
</body>
|
||||
</html>
|
53
test/index.test.js
Normal file
53
test/index.test.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
var test = require('tape');
|
||||
var Promise = require('es6-promise').Promise; // jshint ignore:line
|
||||
var fs = require('then-fs');
|
||||
var path = require('path');
|
||||
|
||||
test('inliner core functions', function coreTests(t) {
|
||||
var Inliner = require('../');
|
||||
|
||||
t.equal(typeof Inliner, 'function', 'Inliner is a function');
|
||||
t.equal(Inliner.version, require('../package.json').version);
|
||||
|
||||
var inliner = new Inliner();
|
||||
t.ok(inliner, 'inline is instantiated');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('inliner fixtures', function fixtureTests(t) {
|
||||
var Inliner = require('../');
|
||||
var files = fs.readdirSync(path.resolve(__dirname, 'fixtures'));
|
||||
var results = [];
|
||||
files = files.filter(function filter(file) {
|
||||
return file.indexOf('.src.') !== -1;
|
||||
}).filter(function filter(file) {
|
||||
// helps to diganose a single file
|
||||
// return file.indexOf('image-css.src.html') === 0;
|
||||
return file;
|
||||
}).map(function map(file) {
|
||||
file = path.resolve(__dirname, 'fixtures', file);
|
||||
results.push(fs.readFile(file.replace('.src.', '.result.'), 'utf8'));
|
||||
return file;
|
||||
});
|
||||
|
||||
t.plan(files.length);
|
||||
|
||||
Promise.all(results).then(function then(results) {
|
||||
files.map(function map(file, i) {
|
||||
new Inliner(file, function callback(error, html) {
|
||||
var basename = path.basename(file);
|
||||
if (error) {
|
||||
t.fail(error.message + ' @ ' + basename);
|
||||
console.log(error.stack);
|
||||
}
|
||||
t.equal(html.trim(), results[i].trim(), basename + ' matches');
|
||||
});
|
||||
});
|
||||
}).catch(function errHandler(error) {
|
||||
t.fail(error.message);
|
||||
console.log(error.stack);
|
||||
t.bailout();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user