4 Commits

Author SHA1 Message Date
Alexandre Gigliotti
3881565a28 0.2.2 2015-12-01 09:20:28 -08:00
Alexandre Gigliotti
d919e535ef Add support for script elements. Update tests. Update docs. 2015-12-01 09:20:24 -08:00
Alexandre Gigliotti
c7dd00818b Update build node version. 2015-11-20 16:09:34 -08:00
Alexandre Gigliotti
068dab8e8f Refactor. Update dependencies. Update tests. Make img plugin async. 2015-11-20 16:06:43 -08:00
14 changed files with 635 additions and 447 deletions

View File

@@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "iojs" - "4"
notifications: notifications:
email: email:
on_success: change on_success: change

View File

@@ -3,15 +3,14 @@
Inline local assets referenced in an HTML document. Inline local assets referenced in an HTML document.
[![npm version](https://img.shields.io/npm/v/inline-html.svg)](https://www.npmjs.com/package/inline-html) [![npm version](https://img.shields.io/npm/v/inline-html.svg)](https://www.npmjs.com/package/inline-html)
[![npm license](https://img.shields.io/npm/l/inline-html.svg)](https://www.npmjs.com/package/inline-html)
[![Travis](https://img.shields.io/travis/panosoft/inline-html.svg)](https://travis-ci.org/panosoft/inline-html) [![Travis](https://img.shields.io/travis/panosoft/inline-html.svg)](https://travis-ci.org/panosoft/inline-html)
[![David](https://img.shields.io/david/panosoft/inline-html.svg)](https://david-dm.org/panosoft/inline-html)
[![npm downloads](https://img.shields.io/npm/dm/inline-html.svg)](https://www.npmjs.com/package/inline-html)
This library parses HTML, embeds the contents of local assets that are referenced within that HTML, and returns a new inlined HTML string. This library parses HTML, embeds the contents of local assets that are referenced within that HTML, and returns a new inlined HTML string.
The following HTML elements and CSS data types are inlined: The following HTML elements and CSS data types are inlined:
- Scripts - The source path is read and inlined.
- Images - The source path is replaced with a datauri. - Images - The source path is replaced with a datauri.
- Linked LESS stylesheets - The LESS is compiled and the result is inlined within a `<style>` element. Note that `@imports` are processed as well. - Linked LESS stylesheets - The LESS is compiled and the result is inlined within a `<style>` element. Note that `@imports` are processed as well.
@@ -24,6 +23,12 @@ Also, `inline-html` calls can be statically evaluated and included in Browserify
Assuming ... Assuming ...
- `main.js`
```js
var a = 1;
```
- `main.less` - `main.less`
```css ```css
@@ -45,6 +50,7 @@ var inline = require('inline-html');
co(function * () { co(function * () {
var html = ` var html = `
<script src="main.js"></script>
<link rel="stylesheet/less" href="main.less"/> <link rel="stylesheet/less" href="main.less"/>
<style> div { background-image: url('path/to/file'); } </style> <style> div { background-image: url('path/to/file'); } </style>
<div style="background-image: url('path/to/file');"></div> <div style="background-image: url('path/to/file');"></div>
@@ -53,6 +59,9 @@ co(function * () {
html = yield inline.html(html); html = yield inline.html(html);
console.log(html); console.log(html);
/** /**
<script>
var a = 1;
</script>
<style> <style>
@font-face { src: url('data:...'); } @font-face { src: url('data:...'); }
div { background-image: url('data:...'); } div { background-image: url('data:...'); }

133
lib/css-url.js Normal file
View File

@@ -0,0 +1,133 @@
const datauri = require('datauri');
const isLocalPath = require('is-local-path');
const isTemplateExpression = require('./is-template-expression');
const path = require('path');
const postcss = require('postcss');
const postcssUrl = require('postcss-url');
const R = require('ramda');
const string = require('string');
const url = require('url');
const collapseWhitespace = str => string(str).collapseWhitespace().toString();
const forEachIndexed = R.addIndex(R.forEach);
const format = (path) => url.format(path);
const parse = (path) => url.parse(path);
const resolve = path.resolve;
const augmentError = (error, filename, files) => {
if (!error.filename) error.filename = filename;
error.files = R.uniq(R.concat(files, error.files || []));
return error;
};
/**
* Returns url path without query string and hash if present.
*
* @param path
*
* @returns path
*/
const cleanUrl = R.pipe(
parse,
R.pick(['protocol', 'host', 'pathname']),
format,
decodeURI
);
/**
* Convert local url data type paths to datauris.
*
* @param css
* @param filename
* @returns {{css: (css|any), files: Array}}
*/
const inlineUrl = R.curry((filename, css) => {
const basePath = path.dirname(filename);
var files = [];
const inline = url => {
try {
if (isLocalPath(url) && !isTemplateExpression(url)) {
url = cleanUrl(url);
url = resolve(basePath, url);
files = R.append(url, files);
url = datauri(url);
}
return url;
}
catch (error) { throw augmentError(error, filename, files); }
};
css = postcss()
.use(postcssUrl({ url: inline }))
.process(css)
.css;
files = R.uniq(files);
return { css, files };
});
const inlineStyles = ($, filename) => {
var files = [];
try {
const styles = $('style')
.toArray();
const contents = R.map(style => {
const css = $(style).html();
const result = inlineUrl(filename, css);
files = R.concat(files, result.files);
return result.css;
}, styles);
forEachIndexed((style, index) => $(style).html(contents[index]), styles);
return { $, files };
}
catch (error) { throw augmentError(error, filename, files); }
};
const prefix = 'selector {';
const suffix = '}';
const matchStyle = new RegExp(`^${prefix}\\s*(.*)\\s*${suffix}$`);
const wrap = style => `${prefix}${style}${suffix}`;
const unwrap = rule => rule.replace(matchStyle, '$1');
const inlineStyleAttributes = ($, filename) => {
var files = [];
try {
const elements = $('*')
.filter('[style]')
.toArray();
const styles = R.map(element => {
var style = $(element).attr('style');
const rule = wrap(style);
const result = inlineUrl(filename, rule);
files = R.concat(files, result.files);
style = R.pipe( collapseWhitespace, unwrap )(result.css);
return style;
}, elements);
forEachIndexed((element, index) => $(element).attr('style', styles[index]), elements);
return { $, files };
}
catch (error) { throw augmentError(error, filename, files); }
};
const inlineCssUrl = function ($, filename) {
var files = [];
try {
var result;
result = inlineStyles($, filename);
$ = result.$;
files = R.concat(files, result.files);
result = inlineStyleAttributes($, filename);
$ = result.$;
files = R.concat(files, result.files);
files = R.uniq(files);
return { $, files };
}
catch (error) { throw augmentError(error, filename, files); }
};
module.exports = inlineCssUrl;

48
lib/img.js Normal file
View File

@@ -0,0 +1,48 @@
const co = require('co');
const datauri = require('datauri').promises;
const isLocalPath = require('is-local-path');
const isTemplateExpression = require('./is-template-expression');
const path = require('path');
const R = require('ramda');
const forEachIndexed = R.addIndex(R.forEach);
const resolve = R.curry((a,b) => path.resolve(a,b));
/**
* Inline sourced image files
*
* @param {Object} $
* Parsed HTML source to inline
* @param {String} filename
* Filename used to resolve relative sources being inlined
*/
const inlineImg = co.wrap(function * ($, filename) {
var files;
const basedir = path.dirname(filename);
const getAttr = R.curry((attr, element) => $(element).attr(attr));
const setAttr = R.curry((attr, element, value) => $(element).attr(attr, value));
const getFilename = R.pipe(getAttr('src'), resolve(basedir));
try {
const images = $('img')
.filter((index, element) => {
const source = $(element).attr('src');
return isLocalPath(source) && !isTemplateExpression(source);
})
.toArray();
const filenames = R.map(getFilename, images);
files = R.uniq(filenames);
const uris = yield R.map(datauri, filenames);
forEachIndexed((image, index) => setAttr('src', image, uris[index]), images);
return { $, files };
}
catch (error) {
error.filename = filename;
error.files = files;
throw error;
}
});
module.exports = inlineImg;

View File

@@ -1,12 +1,14 @@
var co = require('co'); const cheerio = require('cheerio');
var fs = require('mz/fs'); const co = require('co');
var inlineStyle = require('./inline-style'); const fs = require('mz/fs');
var inlineImg = require('./inline-img'); const inlineCssUrl = require('./css-url');
var inlineLess = require('./inline-less'); const inlineImg = require('./img');
var R = require('ramda'); const inlineLess = require('./link-less');
var Ru = require('@panosoft/ramda-utils'); const inlineScript = require('./script');
const R = require('ramda');
const Ru = require('@panosoft/ramda-utils');
var inlineHtml = {}; var inline = {};
/** /**
* Embed referenced local assets within and HTML file. * Embed referenced local assets within and HTML file.
* *
@@ -15,44 +17,50 @@ var inlineHtml = {};
* *
* @return {Promise} * @return {Promise}
*/ */
inlineHtml.html = co.wrap(function * (html, options) { inline.html = co.wrap(function * (html, options) {
options = Ru.defaults({ options = Ru.defaults({
filename: null, filename: null,
less: {}, less: {},
verbose: false verbose: false
}, options || {}); }, options || {});
var filename = options.filename;
// Embed assets const filename = options.filename;
var files = [filename]; var files = [filename];
try { try {
var lessResult = yield inlineLess(html, filename, options.less); var $ = cheerio.load(html, {decodeEntities: false});
html = lessResult.html;
files = R.concat(files, lessResult.files);
var styleResult = inlineStyle(html, filename); var result;
html = styleResult.html; result = yield inlineLess($, filename, options);
files = R.concat(files, styleResult.files); $ = result.$;
files = R.concat(files, result.files);
var imgResult = inlineImg(html, filename); result = inlineCssUrl($, filename, options);
html = imgResult.html; $ = result.$;
files = R.concat(files, imgResult.files); files = R.concat(files, result.files);
result = yield inlineImg($, filename, options);
$ = result.$;
files = R.concat(files, result.files);
result = yield inlineScript($, filename, options);
$ = result.$;
files = R.concat(files, result.files);
html = $.xml();
files = R.uniq(files);
return (options.verbose ? { html, files } : html);
} }
catch (error) { catch (error) {
if (!error.filename) error.filename = filename; if (!error.filename) error.filename = filename;
error.files = R.uniq(R.concat(files, error.files || [])); error.files = R.uniq(R.concat(files, error.files || []));
throw error; throw error;
} }
files = R.uniq(files);
var result = { html, files };
return (options.verbose ? result : result.html);
}); });
inlineHtml.file = co.wrap(function * (filename, options) { inline.file = co.wrap(function * (filename, options) {
var html = yield fs.readFile(filename, 'utf8'); const html = yield fs.readFile(filename, 'utf8');
options = R.merge(options || {}, {filename}); options = R.merge(options || {}, {filename});
return yield inlineHtml.html(html, options); return yield inline.html(html, options);
}); });
module.exports = inlineHtml; module.exports = inline;

View File

@@ -1,61 +0,0 @@
var datauri = require('datauri');
var isLocalPath = require('is-local-path');
var isTemplateExpression = require('./is-template-expression');
var path = require('path');
var postcss = require('postcss');
var postcssUrl = require('postcss-url');
var R = require('ramda');
var url = require('url');
/**
* Returns url path without query string and hash if present.
*
* @param path
*
* @returns path
*/
var clean = function (path) {
path = url.parse(path);
path = R.pick(['protocol', 'host', 'pathname'], path);
path = url.format(path);
path = decodeURI(path);
return path;
};
/**
* Convert local url data type paths to datauris.
*
* @param css
* @param filename
* @returns {{css: (css|any), files: Array}}
*/
var inlineUrl = function (css, filename) {
var files = [];
var basePath = path.dirname(filename);
var result = postcss()
.use(postcssUrl({
url: function (urlPath) {
if (isLocalPath(urlPath) && !isTemplateExpression(urlPath)) {
try {
urlPath = clean(urlPath);
urlPath = path.resolve(basePath, urlPath);
files = R.append(urlPath, files);
urlPath = datauri(urlPath);
}
catch (error) {
error.filename = filename;
error.files = R.uniq(files);
throw error;
}
}
return urlPath;
}
}))
.process(css);
files = R.uniq(files);
return {
css: result.css,
files
};
};
module.exports = inlineUrl;

View File

@@ -1,37 +0,0 @@
var cheerio = require('cheerio');
var datauri = require('datauri');
var isLocalPath = require('is-local-path');
var isTemplateExpression = require('./is-template-expression');
var path = require('path');
var R = require('ramda');
var inline = function (html, filename) {
var files = [];
var basedir = path.dirname(filename);
var $ = cheerio.load(html, {decodeEntities: false});
var $images = $('img').filter((index, element) => {
var path = $(element).attr('src');
return isLocalPath(path) && !isTemplateExpression(path);
});
try {
$images.each((index, element) => {
var source = $(element).attr('src');
var filename = path.resolve(basedir, source);
files = R.append(filename, files);
var uri = datauri(filename);
$(element).attr('src', uri);
});
}
catch (error) {
error.filename = filename;
error.files = R.uniq(files);
throw error;
}
files = R.uniq(files);
return {
html: $.xml(),
files
};
};
module.exports = inline;

View File

@@ -1,67 +0,0 @@
var co = require('co');
var cheerio = require('cheerio');
var fs = require('mz/fs');
var isLocalPath = require('is-local-path');
var less = require('less');
var path = require('path');
var R = require('ramda');
var Ru = require('@panosoft/ramda-utils');
var render = co.wrap(function * (filename, options) {
options = R.merge(options || {}, { filename });
var contents = yield fs.readFile(filename, 'utf8');
return yield less.render(contents, options);
});
/**
* @param {String} html
* HTML source to inline
* @param {String} filename
* Filename to apply to the HTML source being inlined
* @param {Object} options
* LESS compiler options
*/
var inlineLess = co.wrap(function * (html, filename, options) {
options = Ru.defaults({
relativeUrls: true
}, options || {});
var basedir = path.dirname(filename);
// get links
var $ = cheerio.load(html, {decodeEntities: false});
var $links = $('link[rel="stylesheet/less"]')
.filter((index, element) => isLocalPath($(element).attr('href')));
// render LESS stylesheets
var files = [];
var outputs = [];
try {
$links.each((index, element) => {
var href = $(element).attr('href');
var filename = path.resolve(basedir, href);
files = R.append(filename, files);
outputs = R.append(render(filename, options), outputs);
});
outputs = yield outputs;
}
catch (error) {
if (!error.filename) error.filename = filename;
error.files = R.uniq(files);
throw error;
}
// include imported filenames in files array
files = R.concat(files, R.flatten(R.map(output => output.imports, outputs)));
files = R.uniq(files);
// replace links
$links.each((index, element) => {
var style = $('<style>').html(outputs[index].css);
$(element).replaceWith(style);
});
return {
html: $.xml(),
files
};
});
module.exports = inlineLess;

View File

@@ -1,54 +0,0 @@
var cheerio = require('cheerio');
var inlineUrl = require('./inline-css-url');
var R = require('ramda');
var string = require('string');
var prefix = 'element {';
var suffix = '}';
var wrap = function (value) {
return prefix + value + suffix;
};
var unwrap = function (value) {
var regexp = new RegExp('^' + prefix + '\\s*(.*)\\s*' + suffix + '$');
return value.replace(regexp, '$1');
};
var inlineStyle = function (html, filename) {
var files = [];
var $ = cheerio.load(html, {decodeEntities: false});
try {
// style elements
var $styles = $('style');
$styles.each((index, element) => {
var css = $(element).html();
var result = inlineUrl(css, filename);
files = R.concat(files, result.files);
$(element).html(result.css);
});
// style attributes
var $attributes = $('*').filter('[style]');
$attributes.each((index, element) => {
var css = $(element).attr('style');
css = wrap(css);
var result = inlineUrl(css, filename);
files = R.concat(files, result.files);
css = string(result.css).collapseWhitespace().toString();
css = unwrap(css);
$(element).attr('style', css);
});
}
catch (error) {
if (!error.filename) error.filename = filename;
error.files = R.uniq(R.concat(files, error.files || []));
throw error;
}
files = R.uniq(files);
return {
html: $.xml(),
files
};
};
module.exports = inlineStyle;

View File

@@ -7,8 +7,6 @@
* @param {String} path * @param {String} path
* @return {Boolean} * @return {Boolean}
*/ */
var isTemplateExpression = function (path) { const isTemplateExpression = path => /^{{.*}}$/.test(path);
return /^{{.*}}$/.test(path);
};
module.exports = isTemplateExpression; module.exports = isTemplateExpression;

58
lib/link-less.js Normal file
View File

@@ -0,0 +1,58 @@
const co = require('co');
const fs = require('mz/fs');
const isLocalPath = require('is-local-path');
const less = require('less');
const path = require('path');
const R = require('ramda');
const Ru = require('@panosoft/ramda-utils');
const forEachIndexed = R.addIndex(R.forEach);
const resolve = R.curry((a,b) => path.resolve(a,b));
const render = R.curryN(2, co.wrap(function * (options, filename) {
options = R.merge(options || {}, { filename });
const contents = yield fs.readFile(filename, 'utf8');
return yield less.render(contents, options);
}));
/**
* Inline linked less files
*
* @param {Object} $
* Parsed HTML source to inline
* @param {String} filename
* Filename to apply to the HTML source being inlined
* @param {Object} [options]
* @param {Object} [options.less]
* LESS compiler options
*/
const inlineLess = co.wrap(function * ($, filename, options) {
options = Ru.defaults({ less: {} }, options || {});
options = Ru.defaults({ relativeUrls: true }, options.less);
var files = [];
const basedir = path.dirname(filename);
const getAttr = R.curry((attr, element) => $(element).attr(attr));
const getStylesheet = R.pipe(getAttr('href'), resolve(basedir));
try {
const links = $('link[rel="stylesheet/less"]')
.filter((index, link) => isLocalPath($(link).attr('href')))
.toArray();
const stylesheets = R.map(getStylesheet, links);
files = R.concat(files, stylesheets);
const outputs = yield R.map(render(options), stylesheets);
const imports = R.flatten(R.map(R.prop('imports'), outputs));
files = R.concat(files, imports);
const styles = R.map(output => $('<style>').html(output.css), outputs);
forEachIndexed((link, index) => $(link).replaceWith(styles[index]), links);
files = R.uniq(files);
return { $, files };
}
catch (error) {
if (!error.filename) error.filename = filename;
error.files = R.uniq(files);
throw error;
}
});
module.exports = inlineLess;

36
lib/script.js Normal file
View File

@@ -0,0 +1,36 @@
const co = require('co');
const fs = require('mz/fs');
const isLocalPath = require('is-local-path');
const isTemplateExpression = require('./is-template-expression');
const path = require('path');
const R = require('ramda');
const forEachIndexed = R.addIndex(R.forEach);
const inlineScript = co.wrap(function * ($, filename) {
var basedir = path.dirname(filename);
var files;
try {
const scripts = $('script')
.filter((index, element) => {
const source = $(element).attr('src');
return isLocalPath(source) && !isTemplateExpression(source);
})
.toArray();
const getFilename = element => path.resolve(basedir, $(element).attr('src'));
const filenames = R.map(getFilename, scripts);
files = R.uniq(filenames);
const readFile = filename => fs.readFile(filename, 'utf8');
const contents = yield R.map(readFile, filenames);
const replaceScript = (script, index) => $(script).attr('src', null).html(contents[index]);
forEachIndexed(replaceScript, scripts);
return { $, files };
}
catch (error) {
error.filename = filename;
error.files = files;
throw error;
}
});
module.exports = inlineScript;

View File

@@ -1,25 +1,26 @@
{ {
"name": "inline-html", "name": "inline-html",
"version": "0.2.1", "version": "0.2.2",
"description": "Inline local assets referenced in an HTML document.", "description": "Inline local assets referenced in an HTML document.",
"repository": "panosoft/inline-html", "repository": "panosoft/inline-html",
"main": "lib/index.js", "main": "lib/index.js",
"scripts": { "scripts": {
"test": "mocha --harmony_arrow_functions" "test": "mocha",
"test:debug": "mocha --debug-brk"
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@panosoft/ramda-utils": "^0.1.12", "@panosoft/ramda-utils": "^0.2.0",
"cheerio": "^0.19.0", "cheerio": "^0.19.0",
"co": "^4.6.0", "co": "^4.6.0",
"datauri": "^0.8.0", "datauri": "^0.8.0",
"is-local-path": "^0.1.0", "is-local-path": "^0.1.0",
"less": "^2.5.1", "less": "^2.5.1",
"mz": "^2.0.0", "mz": "^2.0.0",
"postcss": "^5.0.0", "postcss": "^5.0.12",
"postcss-url": "^5.0.0", "postcss-url": "^5.0.0",
"ramda": "^0.17.1", "ramda": "^0.18.0",
"string": "^3.3.0" "string": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -18,12 +18,12 @@ describe('inline-html', () => {
return expect(inline.file(filename)).to.eventually.equal(content); return expect(inline.file(filename)).to.eventually.equal(content);
}); });
it('options.filename: ignored for css url path resolution', () => { it('options.filename: always reset to current filename and used for css url path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/css-url.html'); // file contains url path relative to itself var filename = path.resolve(__dirname, 'fixtures/css-url.html'); // file contains url path relative to itself
var options = { filename: __filename }; // sets filename relative to this test file var options = { filename: __filename }; // sets filename relative to this test file
return expect(inline.file(filename, options)).to.eventually.match(/"data:.*,.*"/); return expect(inline.file(filename, options)).to.eventually.match(/"data:.*,.*"/);
}); });
it('options.filename: ignored for img src path resolution', () => { it('options.filename: always reset to current filename and used for img src path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/img.html'); // file contains src relative to itself var filename = path.resolve(__dirname, 'fixtures/img.html'); // file contains src relative to itself
var options = { filename: __filename }; // sets filename relative to this test file var options = { filename: __filename }; // sets filename relative to this test file
return expect(inline.file(filename, options)).to.eventually.match(/"data:.*,.*"/); return expect(inline.file(filename, options)).to.eventually.match(/"data:.*,.*"/);
@@ -39,32 +39,72 @@ describe('inline-html', () => {
var html = 'HTML'; var html = 'HTML';
return expect(inline.html(html)).to.eventually.equal(html); return expect(inline.html(html)).to.eventually.equal(html);
}); });
it('preserve self closing tags', () => {
var html = '<br/>';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('preserve partials', () => {
var html = '{{> partial}}';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('preserve helpers', () => {
var html = '{{helper}}';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('inline link less', () => { it('options.filename: default to cwd for css url path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/basic.less'); var url = 'test/fixtures/file.txt'; // Note: this is relative to cwd
var html = `<link rel="stylesheet/less" href="${filename}"/>`; var uri = datauri(url);
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/);
var html = (path) => `<style>div { background-image: url('${path}'); }</style>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
}); });
it('inline link less imports', () => { it('options.filename: set basepath for css url path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/import.less'); var filename = path.resolve(__dirname, 'fixtures/fake.html');
var html = `<link rel="stylesheet/less" href="${filename}"/>`; var dirname = path.dirname(filename);
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/)
.and.not.match(/@import/); var url = 'file.txt'; // Note: path relative to filename's dirname
var uri = datauri(path.resolve(dirname, url));
var html = (path) => `<style>div { background-image: url('${path}'); }</style>`;
var options = { filename: filename };
return expect(inline.html(html(url), options)).to.eventually.equal(html(uri));
}); });
it('inline css url path in style element', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt'); it('options.filename: default to cwd for img src path resolution', () => {
var html = `<style>div {background-image: url("${filename}");}</style>`; var url = 'test/fixtures/file.txt'; // Note: path relative to cwd
return expect(inline.html(html)).to.eventually.match(/data:.*,.*/); var uri = datauri(url);
var html = (path) => `<img src="${path}"/>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
}); });
it('inline css url path in element style attribute', () => { it('options.filename: set basepath for img src path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt'); var filename = path.resolve(__dirname, 'fixtures/fake.html');
var html = `<div style="background-image: url('${filename}');"></div>`; var dirname = path.dirname(filename);
return expect(inline.html(html)).to.eventually.match(/data:.*,.*/);
var url = 'file.txt'; // Note: path relative to filename's dirname
var uri = datauri(path.resolve(dirname, url));
var html = (path) => `<img src="${path}"/>`;
var options = { filename: filename };
return expect(inline.html(html(url), options)).to.eventually.equal(html(uri));
}); });
it('inline img src', () => {
it('options.filename: included in results.files for img src if options.verbose true', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt'); var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<img src="${filename}"/>`; var html = `<img src="${filename}"/>`;
return expect(inline.html(html)).to.eventually.match(/data:.*,.*/); var options = { verbose: true };
return expect(inline.html(html, options)).to.eventually.have.property('files')
.that.is.an('array')
.that.contains(filename);
});
it('options.filename: included in results.files for css url path if options.verbose true', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<style>div { background-image: url('${filename}'); }</style>`;
var options = { verbose: true };
return expect(inline.html(html, options)).to.eventually.have.property('files')
.that.is.an('array')
.that.contains(filename);
}); });
@@ -88,154 +128,39 @@ describe('inline-html', () => {
}); });
it('options.filename: set basepath for css url path resolution', () => { describe('css-url', () => {
var filename = path.resolve(__dirname, 'fixtures/fake.html'); it('inline local url in style element', () => {
var dirname = path.dirname(filename); var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<style>div {background-image: url("${filename}");}</style>`;
var url = 'file.txt'; // Note: path relative to filename's dirname return expect(inline.html(html)).to.eventually.match(/data:.*,.*/);
var uri = datauri(path.resolve(dirname, url));
var html = (path) => `<style>div { background-image: url('${path}'); }</style>`;
var options = { filename: filename };
return expect(inline.html(html(url), options)).to.eventually.equal(html(uri));
});
it('options.filename: set basepath for img src path resolution', () => {
var filename = path.resolve(__dirname, 'fixtures/fake.html');
var dirname = path.dirname(filename);
var url = 'file.txt'; // Note: path relative to filename's dirname
var uri = datauri(path.resolve(dirname, url));
var html = (path) => `<img src="${path}"/>`;
var options = { filename: filename };
return expect(inline.html(html(url), options)).to.eventually.equal(html(uri));
});
it('options.filename: default to cwd for css url path resolution', () => {
var url = 'test/fixtures/file.txt'; // Note: this is relative to cwd
var uri = datauri(url);
var html = (path) => `<style>div { background-image: url('${path}'); }</style>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
});
it('options.filename: default to cwd for img src path resolution', () => {
var url = 'test/fixtures/file.txt'; // Note: path relative to cwd
var uri = datauri(url);
var html = (path) => `<img src="${path}"/>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
});
it('options.filename: included in results.files for img src if options.verbose true', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<img src="${filename}"/>`;
var options = { verbose: true };
return expect(inline.html(html, options)).to.eventually.have.property('files')
.that.is.an('array')
.that.contains(filename);
});
it('options.filename: included in results.files for css url path if options.verbose true', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<style>div { background-image: url('${filename}'); }</style>`;
var options = { verbose: true };
return expect(inline.html(html, options)).to.eventually.have.property('files')
.that.is.an('array')
.that.contains(filename);
});
it('preserve self closing tags', () => {
var html = '<br/>';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('preserve partials', () => {
var html = '{{> partial}}';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('preserve helpers', () => {
var html = '{{helper}}';
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore css url remote paths', () => {
var html = `<style> div { background-image: url('http://test.com/file.txt?query=string#hash'); }</style>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore img src remote paths', () => {
var html = `<img src="http://test.com/file.txt?query=string#hash"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore css url template expression paths', () => {
var html = `<style> div { background-image: url({{path}}); }</style>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore img src template expression paths', () => {
var html = `<img src="{{path}}"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore query strings and hashes on local paths', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var url = `${filename}?query=string#hash`;
var uri = datauri(filename);
var html = (source) => `<style> div { background-image: url('${source}'); }</style>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
});
it('handle assets with a space in their filename', () => {
var filename = path.resolve(__dirname, 'fixtures/file space.txt');
var uri = datauri(filename);
var html = (source) => `<style> div { background-image: url('${source}'); }</style>`;
return expect(inline.html(html(filename))).to.eventually.equal(html(uri));
});
// Error handling
// inline-img
it('throw error when html image source invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var source = 'missing.png';
var html = `<img src="${source}" >`;
var resolvedSource = path.resolve(path.dirname(filename), source);
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedSource);
}
}); });
}); it('inline local url in style attribute', () => {
// inline-style var filename = path.resolve(__dirname, 'fixtures/file.txt');
it('throw error when html style attribute syntax invalid', () => { var html = `<div style="background-image: url('${filename}');"></div>`;
return co(function * () { return expect(inline.html(html)).to.eventually.match(/data:.*,.*/);
var filename = path.resolve(__dirname, 'index.html');
var html = `<div style="background url()"></div>`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(filename);
}
}); });
}); it('ignore remote url', () => {
it('throw error when html style attribute url invalid', () => { var html = `<style> div { background-image: url('http://test.com/file.txt?query=string#hash'); }</style>`;
return co(function * () { return expect(inline.html(html)).to.eventually.equal(html);
var filename = path.resolve(__dirname, 'index.html');
var url = 'missing.png';
var resolvedUrl = path.resolve(path.dirname(filename), url);
var html = `<div style="background-image: url('${url}')"></div>`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedUrl);
}
}); });
}); it('ignore template expression url', () => {
it('throw error when html style syntax invalid', () => { var html = `<style> div { background-image: url({{path}}); }</style>`;
return co(function * () { return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore url query strings and hashes', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var url = `${filename}?query=string#hash`;
var uri = datauri(filename);
var html = (source) => `<style> div { background-image: url('${source}'); }</style>`;
return expect(inline.html(html(url))).to.eventually.equal(html(uri));
});
it('handle url with spaces', () => {
var filename = path.resolve(__dirname, 'fixtures/file space.txt');
var uri = datauri(filename);
var html = (source) => `<style> div { background-image: url('${source}'); }</style>`;
return expect(inline.html(html(filename))).to.eventually.equal(html(uri));
});
it('throw when syntax invalid in style element', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html'); var filename = path.resolve(__dirname, 'index.html');
var html = `<style>div {</style>`; var html = `<style>div {</style>`;
try { try {
@@ -246,10 +171,20 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(filename); expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(filename); expect(error).to.have.property('files').that.contains(filename);
} }
}); }));
}); it('throw when syntax invalid in style attribute', () => co(function * () {
it('throw error when html style url invalid', () => { var filename = path.resolve(__dirname, 'index.html');
return co(function * () { var html = `<div style="background url()"></div>`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(filename);
}
}));
it('throw when url invalid in style element', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html'); var filename = path.resolve(__dirname, 'index.html');
var url = 'missing.png'; var url = 'missing.png';
var resolvedUrl = path.resolve(path.dirname(filename), url); var resolvedUrl = path.resolve(path.dirname(filename), url);
@@ -262,11 +197,146 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(filename); expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedUrl); expect(error).to.have.property('files').that.contains(resolvedUrl);
} }
}); }));
it('throw when url invalid in style attribute ', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var url = 'missing.png';
var resolvedUrl = path.resolve(path.dirname(filename), url);
var html = `<div style="background-image: url('${url}')"></div>`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedUrl);
}
}));
it('include all urls in error.files up until and including invalid url in style element', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var validUrl = 'fixtures/file.txt';
var invalidUrl = 'missing.png';
var resolvedInvalidUrl = path.resolve(path.dirname(filename), invalidUrl);
var resolvedValidUrl = path.resolve(path.dirname(filename), validUrl);
var html = `
<style>div {background-image: url('${validUrl}');}</style>
<style>div {background-image: url('${invalidUrl}');}</style>
`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedValidUrl);
expect(error).to.have.property('files').that.contains(resolvedInvalidUrl);
}
}));
it('include all urls in error.files up until and including invalid url in style attribute', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var validUrl = 'fixtures/file.txt';
var invalidUrl = 'missing.png';
var resolvedInvalidUrl = path.resolve(path.dirname(filename), invalidUrl);
var resolvedValidUrl = path.resolve(path.dirname(filename), validUrl);
var html = `
<div style="background-image: url('${validUrl}')"></div>
<div style="background-image: url('${invalidUrl}')"></div>
`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedValidUrl);
expect(error).to.have.property('files').that.contains(resolvedInvalidUrl);
}
}));
it('include all urls in error.files up until and including invalid url when style element valid and style attribute invalid', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var validUrl = 'fixtures/file.txt';
var invalidUrl = 'missing.png';
var resolvedInvalidUrl = path.resolve(path.dirname(filename), invalidUrl);
var resolvedValidUrl = path.resolve(path.dirname(filename), validUrl);
var html = `
<style>div {background-image: url("${validUrl}");}</style>
<div style="background-image: url('${invalidUrl}')"></div>
`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedValidUrl);
expect(error).to.have.property('files').that.contains(resolvedInvalidUrl);
}
}));
}); });
// inline-link-less
it('throw error when link href invalid', () => { describe('img', () => {
return co(function * () { it('inline img src', () => {
var filename = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<img src="${filename}"/>`;
return expect(inline.html(html)).to.eventually.match(/data:.*,.*/);
});
it('ignore img src remote paths', () => {
var html = `<img src="http://test.com/file.txt?query=string#hash"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore img src template expression paths', () => {
var html = `<img src="{{path}}"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('throw when src invalid', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var source = 'missing.png';
var html = `<img src="${source}" >`;
var resolvedSource = path.resolve(path.dirname(filename), source);
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedSource);
}
}));
it('include all sources in error.files up until and including invalid source', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var valid = 'fixtures/file.txt';
var invalid = 'missing.png';
var resolvedInvalid = path.resolve(path.dirname(filename), invalid);
var resolvedValid = path.resolve(path.dirname(filename), valid);
var html = `
<img src="${valid}">
<img src="${invalid}">
`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedValid);
expect(error).to.have.property('files').that.contains(resolvedInvalid);
}
}));
});
describe('link-less', () => {
it('inline link less', () => {
var filename = path.resolve(__dirname, 'fixtures/basic.less');
var html = `<link rel="stylesheet/less" href="${filename}"/>`;
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/);
});
it('inline link less imports', () => {
var filename = path.resolve(__dirname, 'fixtures/import.less');
var html = `<link rel="stylesheet/less" href="${filename}"/>`;
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/)
.and.not.match(/@import/);
});
it('throw error when link href invalid', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html'); var filename = path.resolve(__dirname, 'index.html');
var href = 'missing.less'; var href = 'missing.less';
var resolvedHref = path.resolve(path.dirname(filename), href); var resolvedHref = path.resolve(path.dirname(filename), href);
@@ -279,10 +349,8 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(filename); expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedHref); expect(error).to.have.property('files').that.contains(resolvedHref);
} }
}); }));
}); it('throw error when less import invalid', () => co(function * () {
it('throw error when less import invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/index.html'); var filename = path.resolve(__dirname, 'fixtures/index.html');
var lessBasename = 'invalidImport.less'; var lessBasename = 'invalidImport.less';
var lessFilename = path.resolve(path.dirname(filename), lessBasename); var lessFilename = path.resolve(path.dirname(filename), lessBasename);
@@ -295,10 +363,8 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(lessFilename); expect(error).to.have.property('filename').that.equals(lessFilename);
expect(error).to.have.property('files').that.contains(lessFilename); expect(error).to.have.property('files').that.contains(lessFilename);
} }
}); }));
}); it('throw error when less syntax invalid', () => co(function * () {
it('throw error when less syntax invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/index.html'); var filename = path.resolve(__dirname, 'fixtures/index.html');
var lessBasename = 'invalidSyntax.less'; var lessBasename = 'invalidSyntax.less';
var lessFilename = path.resolve(path.dirname(filename), lessBasename); var lessFilename = path.resolve(path.dirname(filename), lessBasename);
@@ -311,13 +377,10 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(lessFilename); expect(error).to.have.property('filename').that.equals(lessFilename);
expect(error).to.have.property('files').that.contains(lessFilename); expect(error).to.have.property('files').that.contains(lessFilename);
} }
}); }));
}); it('throw error when less url invalid', () => co(function * () {
it('throw error when less url invalid', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'fixtures/index.html'); var filename = path.resolve(__dirname, 'fixtures/index.html');
var lessBasename = 'invalidUrl.less'; var lessBasename = 'invalidUrl.less';
var lessFilename = path.resolve(path.dirname(filename), lessBasename);
var badUrl = path.resolve(path.dirname(filename), 'missing.png'); var badUrl = path.resolve(path.dirname(filename), 'missing.png');
var html = `<link rel="stylesheet/less" href="${lessBasename}">`; var html = `<link rel="stylesheet/less" href="${lessBasename}">`;
try { try {
@@ -330,8 +393,61 @@ describe('inline-html', () => {
expect(error).to.have.property('filename').that.equals(filename); expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(badUrl); expect(error).to.have.property('files').that.contains(badUrl);
} }
}));
});
describe('script', () => {
it('inline local src', () => {
var source = path.resolve(__dirname, 'fixtures/file.txt');
var html = `<script src="${source}"></script>`;
var contents = fs.readFileSync(source, 'utf8');
return expect(inline.html(html)).to.eventually.equal(`<script>${contents}</script>`);
});
it('ignore remote src', () => {
var html = `<script src="http://test.com/file.txt?query=string#hash"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('ignore template expression src', () => {
var html = `<script src="{{path}}"/>`;
return expect(inline.html(html)).to.eventually.equal(html);
});
it('throw when source invalid', () => co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var source = 'missing.js';
var html = `<script src="${source}"></script>`;
var resolvedSource = path.resolve(path.dirname(filename), source);
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedSource);
}
})
);
it('include all sources in error.files up until and including invalid source', () => {
return co(function * () {
var filename = path.resolve(__dirname, 'index.html');
var valid = 'fixtures/file.txt';
var invalid = 'missing.js';
var resolvedInvalid = path.resolve(path.dirname(filename), invalid);
var resolvedValid = path.resolve(path.dirname(filename), valid);
var html = `
<script src="${valid}"></script>
<script src="${invalid}"></script>
`;
try {
yield inline.html(html, {filename});
throw new Error('No error thrown');
}
catch (error) {
expect(error).to.have.property('filename').that.equals(filename);
expect(error).to.have.property('files').that.contains(resolvedValid);
expect(error).to.have.property('files').that.contains(resolvedInvalid);
}
});
}); });
}); });
}); });
}); });