Skip to content

Commit

Permalink
+readme
Browse files Browse the repository at this point in the history
  • Loading branch information
remy committed Mar 14, 2011
1 parent b13ca34 commit a5ffd39
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 209 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Inliner

Turns your web page to a single HTML file with everything inlined - perfect for appcache manifests on mobile devices that you want to reduce those http requests.

## What it does

- Get a list of all the assets required to drive the page: CSS, JavaScript, images and images used in CSS
- Minify JavaScript (via [uglify-js](https://github.com/mishoo/UglifyJS "mishoo/UglifyJS - GitHub"))
- Strips white from CSS
- Base64 encode images
- Puts everything back together as a single HTML file with a simplfied doctype

## Usage

var inliner = require('./inliner').inliner;

inliner('http://remysharp.com', function (html) {
// compressed and inlined HTML page
console.log(html);
});

I plan to include a web service at some point, but obviously this won't be able to access localhost domains.

Once you've inlined the crap out of the page, add the `manifest="self.manifest"` to the `html` tag and create an empty file called self.manifest ([read more](http://remysharp.com/2011/01/31/simple-offline-application/)).

## Limitations / Caveats

- Whitespace compression might get a little heavy handed - all whitespace is collapsed from n spaces to one space.
- Doesn't support @import rules in CSS
- I've not tested it much (yet)! :)
- It was written in about 2 hours or so, so the code is a little messy, sorry!
212 changes: 3 additions & 209 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,211 +1,5 @@
var URL = require('url'),
Buffer = require('buffer').Buffer,
jsdom = require("jsdom"),
jsp = require("uglify-js").parse-js,
pro = require("uglify-js").process,
url = process.ARGV[2] || 'http://twitter.com/',
oURL = URL.parse(url);
var inliner = require('./inliner').inliner;

function resolveProtocol(url) {
return url.indexOf('//') === 0 ? oURL.protocol + url : url;
}

function get(url, callback) {
// url = resolveProtocol(url);
var oURL = URL.parse(url),
http = require('http'),
client = http.createClient(80, oURL.hostname),
request = client.request('GET', oURL.pathname, {'host': oURL.hostname});

request.end();
request.on('response', function (response) {
var body = "";

response.on('end', function () {
callback && callback(body);
});
response.on('data', function (chunk) {
if (response.statusCode == 200) body += chunk;
});
});

}

function img2base64(url, callback) {
// url = resolveProtocol(url);
var oURL = URL.parse(url),
http = require('http'),
client = http.createClient(80, oURL.hostname),
request = client.request('GET', oURL.pathname, {'host': oURL.hostname});

request.end();
request.on('response', function (response) {
var type = response.headers["content-type"],
prefix = "data:" + type + ";base64,",
body = "";

response.setEncoding('binary');
response.on('end', function () {
var base64 = new Buffer(body, 'binary').toString('base64'),
data = prefix + base64;

console.error('dataurl for ' + url + ': ' + data.length);

callback(data);
});
response.on('data', function (chunk) {
if (response.statusCode == 200) body += chunk;
});
});
}

function getImagesFromCSS(rooturl, rawCSS, callback) {
var images = {},
urlMatch = /url\((?:['"]*)(.*?)(?:['"]*)\)/g,
singleURLMatch = /url\((?:['"]*)(.*?)(?:['"]*)\)/,
matches = rawCSS.match(urlMatch),
imageCount = matches === null ? 0 : matches.length; // TODO check!

function checkFinished() {
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) {
img2base64(resolvedURL, function (dataurl) {
imageCount--;
if (images[url] === undefined) images[url] = dataurl;
checkFinished();
});
} else {
imageCount--;
checkFinished();
}
});
} else {
callback(rawCSS);
}
}

jsdom.env(url, [
'http://code.jquery.com/jquery-1.5.min.js'
], function(errors, window) {

var todo = { scripts: true, images: true, links: true, styles: true },
// todo = { styles: true },
assets = {
scripts: window.$('script[src]').filter(function () { return this.parentNode.lastChild !== this; }),
images: window.$('img'),
links: window.$('link[rel=stylesheet]'),
styles: window.$('style'),

},
breakdown = {},
items = 0,
images = {};

for (var key in todo) {
if (todo[key] === true) {
breakdown[key] = assets[key].length;
items += assets[key].length;
}
}

function finished() {
items--;
if (items === 0) {
window.$('script:last').remove();
console.log(window.document.innerHTML);
} else if (items < 0) {
console.log('something went wrong on finish');
} else {
// console.log('not finished: ' + items);
}
// console.dir(breakdown);
}

todo.images && assets.images.each(function () {
var img = this;
img2base64(URL.resolve(url, img.src), function (dataurl) {
if (dataurl) images[img.src] = dataurl;
img.src = dataurl;
breakdown.images--;
finished();
});
});

todo.styles && assets.styles.each(function () {
var style = this;
getImagesFromCSS(url, this.innerHTML, function (css) {
style.innerHTML = css;
breakdown.styles--;
finished();
});
});

todo.links && assets.links.each(function () {
var link = this;
// console.log('link: ' + link.href);
get(link.href, function (css) {
getImagesFromCSS(link.href, css, function (css) {
// console.log(css);
breakdown.links--;
window.$(link).replaceWith('<style>' + css + '</style>');
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);

if (scriptURL.indexOf('google-analytics.com') !== -1) { // ignore google
breakdown.scripts--;
finished();
} else {
get(scriptURL, function (data) {

var orig_code = data;
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
var final_code = pro.gen_code(ast); // compressed code here

$script.removeAttr('src').text(final_code);
$script.before('<!-- ' + scriptURL + ' -->');
breakdown.scripts--;
finished();
});
}
});

// console.log($scripts[$scripts.length - 1].parentNode.lastChild == $scripts[$scripts.length - 1]);

/** 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
* 7. compress HTML (/>\s+</g, '> <');
*
* FUTURE ITEMS:
* - support for @import
* - support for media queries - important!
* - compression options
* - javascript validation - i.e. not throwing errors
*/
inliner(process.ARGV[2] || 'http://remysharp.com', function (html) {
console.log(html);
});

0 comments on commit a5ffd39

Please sign in to comment.