diff --git a/lib/index.js b/lib/index.js index 62a0b1c..0b9e0ba 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,21 @@ var co = require('co'); -var cheerio = require('cheerio'); var fs = require('mz/fs'); var inlineStyle = require('./inline-style'); var inlineImg = require('./inline-img'); -var inlineLinkLess = require('./inline-link-less'); +var inlineLess = require('./inline-less'); var R = require('ramda'); var Ru = require('@panosoft/ramda-utils'); -var inline = co.wrap(function * (html, options) { +/** + * Embed referenced local assets within and HTML file. + * + * @param {String} html + * Filename or html string. + * @param {Object} options + * + * @return {Promise} + */ +var inlineHtml = co.wrap(function * (html, options) { options = Ru.defaults({ filename: null, less: {}, @@ -19,39 +27,37 @@ var inline = co.wrap(function * (html, options) { html = yield fs.readFile(filename, 'utf8'); } catch (error) { - if (error.code === 'ENOENT') { - filename = options.filename; - } - else { - throw error; - } + if (error.code === 'ENOENT') filename = options.filename; + else throw error; } + + // Embed assets var files = [filename]; + try { + var lessResult = yield inlineLess(html, filename, options.less); + html = lessResult.html; + files = R.concat(files, lessResult.files); - // Inline links - var lessResult = yield inlineLinkLess(html, filename, options.less); - html = lessResult.html; - files.push(lessResult.files); + var styleResult = inlineStyle(html, filename); + html = styleResult.html; + files = R.concat(files, styleResult.files); - // TODO inline links: css - - // TODO inline scripts - // browserify js? => scriptify - - // Inline paths -> datauris - var styleResult = inlineStyle(html, filename); // Inline styles - html = styleResult.html; - files.push(styleResult.files); - - var imgResult = inlineImg(html, filename); // Inline images - html = imgResult.html; - files.push(imgResult.files); + var imgResult = inlineImg(html, filename); + html = imgResult.html; + files = R.concat(files, imgResult.files); + } + catch (error) { + if (!error.filename) error.filename = filename; + error.files = R.uniq(R.concat(files, error.files || [])); + throw error; + } + files = R.uniq(files); var result = { - html: html, - files: R.uniq(R.flatten(files, true)) + html, + files }; return (options.verbose ? result : result.html); }); -module.exports = inline; +module.exports = inlineHtml; diff --git a/lib/inline-css-url.js b/lib/inline-css-url.js index 5e71a82..0dad98f 100644 --- a/lib/inline-css-url.js +++ b/lib/inline-css-url.js @@ -28,26 +28,34 @@ var clean = function (path) { * @param filename * @returns {{css: (css|any), files: Array}} */ -var inline = function (css, filename) { +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)) { - urlPath = clean(urlPath); - urlPath = path.resolve(basePath, urlPath); - files.push(urlPath); - urlPath = datauri(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: files + files }; }; -module.exports = inline; +module.exports = inlineUrl; diff --git a/lib/inline-img.js b/lib/inline-img.js index f68c1f6..419ce47 100644 --- a/lib/inline-img.js +++ b/lib/inline-img.js @@ -3,25 +3,34 @@ 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(function (index, element) { + var $images = $('img').filter((index, element) => { var path = $(element).attr('src'); return isLocalPath(path) && !isTemplateExpression(path); }); - images.each(function (index, element) { - var src = $(element).attr('src'); - var filename = path.resolve(basedir, src); - files.push(filename); - src = datauri(filename); - $(element).attr('src', src); - }); + 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: files + files }; }; diff --git a/lib/inline-less.js b/lib/inline-less.js new file mode 100644 index 0000000..cad44d9 --- /dev/null +++ b/lib/inline-less.js @@ -0,0 +1,67 @@ +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 = $('`; return expect(inline(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 = ``; + var resolvedSource = path.resolve(path.dirname(filename), source); + try { + yield inline(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); + } + }); + }); + // inline-style + it('throw error when html style attribute syntax invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'index.html'); + var html = `
`; + try { + yield inline(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 error when html style attribute url invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'index.html'); + var url = 'missing.png'; + var resolvedUrl = path.resolve(path.dirname(filename), url); + var html = `
`; + try { + yield inline(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('throw error when html style syntax invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'index.html'); + var html = ``; + try { + yield inline(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 error when html style url invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'index.html'); + var url = 'missing.png'; + var resolvedUrl = path.resolve(path.dirname(filename), url); + var html = ``; + try { + yield inline(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); + } + }); + }); + // inline-link-less + it('throw error when link href invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'index.html'); + var href = 'missing.less'; + var resolvedHref = path.resolve(path.dirname(filename), href); + var html = ``; + try { + yield inline(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(resolvedHref); + } + }); + }); + it('throw error when less import invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'fixtures/errors/lessImport/index.html'); + var lessFilename = path.resolve(path.dirname(filename), 'main.less'); + try { + yield inline(filename); + throw new Error('No error thrown'); + } + catch (error) { + expect(error).to.have.property('filename').that.equals(lessFilename); + expect(error).to.have.property('files').that.contains(lessFilename); + } + }); + }); + it('throw error when less syntax invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'fixtures/errors/lessSyntax/index.html'); + var lessFilename = path.resolve(path.dirname(filename), 'main.less'); + try { + yield inline(filename); + throw new Error('No error thrown'); + } + catch (error) { + expect(error).to.have.property('filename').that.equals(lessFilename); + expect(error).to.have.property('files').that.contains(lessFilename); + } + }); + }); + it('throw error when less url invalid', () => { + return co(function * () { + var filename = path.resolve(__dirname, 'fixtures/errors/lessUrl/index.html'); + var lessFilename = path.resolve(path.dirname(filename), 'main.less'); + var badUrl = path.resolve(path.dirname(filename), 'missing.png'); + try { + yield inline(filename); + throw new Error('No error thrown'); + } + catch (error) { + // expect error.filename to be html file, not less file, since images + // aren't inlined until after the compiled less has been inlined into the html. + expect(error).to.have.property('filename').that.equals(filename); + expect(error).to.have.property('files').that.contains(badUrl); + } + }); + }); });