mirror of
https://github.com/KevinMidboe/inline-html.git
synced 2025-10-29 09:30:29 +00:00
Add support for inlining CSS stylesheets. Add tests. Update docs.
This commit is contained in:
18
README.md
18
README.md
@@ -11,9 +11,11 @@ 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.
|
||||
- Linked CSS stylesheets - The stylesheet is read and inlined within a `<style>` element. Note that nested `@import`'s are also inlined.
|
||||
|
||||
- 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 output is inlined within a `<style>` element. Note that `@import`'s are also inlined.
|
||||
|
||||
- Images - The source path is replaced with a datauri.
|
||||
|
||||
- CSS url data types - The reference path is replaced with a datauri. These can be used in linked stylesheets, style elements, and element style attributes.
|
||||
|
||||
@@ -32,7 +34,6 @@ Assuming ...
|
||||
- `main.less`
|
||||
|
||||
```css
|
||||
@import (less) 'main.css';
|
||||
div { background-image: url('path/to/file'); }
|
||||
```
|
||||
|
||||
@@ -51,6 +52,7 @@ var inline = require('inline-html');
|
||||
co(function * () {
|
||||
var html = `
|
||||
<script src="main.js"></script>
|
||||
<link rel="stylesheet" href="main.css"/>
|
||||
<link rel="stylesheet/less" href="main.less"/>
|
||||
<style> div { background-image: url('path/to/file'); } </style>
|
||||
<div style="background-image: url('path/to/file');"></div>
|
||||
@@ -59,13 +61,9 @@ co(function * () {
|
||||
html = yield inline.html(html);
|
||||
console.log(html);
|
||||
/**
|
||||
<script>
|
||||
var a = 1;
|
||||
</script>
|
||||
<style>
|
||||
@font-face { src: url('data:...'); }
|
||||
div { background-image: url('data:...'); }
|
||||
</style>
|
||||
<script> var a = 1; </script>
|
||||
<style> @font-face { src: url('data:...'); } </style>
|
||||
<style> div { background-image: url('data:...'); } </style>
|
||||
<style> div { background-image: url('data:...'); } </style>
|
||||
<div style="background-image: url('data:...');"></div>
|
||||
<img src="data:..."/>
|
||||
|
||||
@@ -4,6 +4,7 @@ const fs = require('mz/fs');
|
||||
const inlineCssUrl = require('./css-url');
|
||||
const inlineImg = require('./img');
|
||||
const inlineLess = require('./link-less');
|
||||
const inlineLinkCss = require('./link-css');
|
||||
const inlineScript = require('./script');
|
||||
const R = require('ramda');
|
||||
const Ru = require('@panosoft/ramda-utils');
|
||||
@@ -19,7 +20,7 @@ var inline = {};
|
||||
*/
|
||||
inline.html = co.wrap(function * (html, options) {
|
||||
options = Ru.defaults({
|
||||
filename: null,
|
||||
filename: '.',
|
||||
less: {},
|
||||
verbose: false
|
||||
}, options || {});
|
||||
@@ -34,6 +35,10 @@ inline.html = co.wrap(function * (html, options) {
|
||||
$ = result.$;
|
||||
files = R.concat(files, result.files);
|
||||
|
||||
result = yield inlineLinkCss($, filename, options);
|
||||
$ = result.$;
|
||||
files = R.concat(files, result.files);
|
||||
|
||||
result = inlineCssUrl($, filename, options);
|
||||
$ = result.$;
|
||||
files = R.concat(files, result.files);
|
||||
|
||||
70
lib/link-css.js
Normal file
70
lib/link-css.js
Normal file
@@ -0,0 +1,70 @@
|
||||
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 postcss = require('postcss');
|
||||
const postcssImport = require('postcss-import');
|
||||
const postcssUrl = require('postcss-url');
|
||||
const R = require('ramda');
|
||||
|
||||
const forEachIndexed = R.addIndex(R.forEach);
|
||||
const render = R.curryN(2, co.wrap(function * (filename, href) {
|
||||
var imports;
|
||||
const processor = postcss()
|
||||
.use(postcssImport({ async: true, onImport: files => imports = files }))
|
||||
.use(postcssUrl({ url: 'rebase' }));
|
||||
try {
|
||||
const css = yield fs.readFile(href, 'utf8');
|
||||
const result = yield processor.process(css, { from: href, to: filename });
|
||||
const output = {css: result.css, imports};
|
||||
return output;
|
||||
}
|
||||
catch (error) {
|
||||
// process uses error.file = href
|
||||
// import uses error.fileName
|
||||
// readFile uses nothing => use filename
|
||||
error.filename = error.file || error.fileName || filename;
|
||||
throw error;
|
||||
}
|
||||
}));
|
||||
/**
|
||||
* Inline liked CSS stylesheets by replacing link elements with
|
||||
* style elements that contain the css file contents.
|
||||
* @param {Object} $
|
||||
* Parsed HTML source to inline
|
||||
* @param {String} [filename='.']
|
||||
* Filename of the HTML document contained within $
|
||||
* @return {Promise}
|
||||
*/
|
||||
const inlineLinkCss = co.wrap(function * ($, filename) {
|
||||
// TODO consider: explicitly default filename = '.'?
|
||||
// path.dirname(null || undefined) -> '.'
|
||||
const basedir = path.dirname(filename);
|
||||
var files = [];
|
||||
try {
|
||||
const links = $('link[rel="stylesheet"]')
|
||||
.filter((index, link) => {
|
||||
const href = $(link).attr('href');
|
||||
return isLocalPath(href) && !isTemplateExpression(href);
|
||||
})
|
||||
.toArray();
|
||||
const getHref = element => path.resolve(basedir, $(element).attr('href'));
|
||||
const hrefs = R.map(getHref, links);
|
||||
files = R.concat(files, hrefs);
|
||||
const outputs = yield R.map(render(filename), hrefs);
|
||||
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);
|
||||
const replaceLink = (link, index) => $(link).replaceWith(styles[index]);
|
||||
forEachIndexed(replaceLink, links);
|
||||
files = R.uniq(files);
|
||||
return { $, files };
|
||||
}
|
||||
catch (error) {
|
||||
if (!error.filename) error.filename = filename;
|
||||
error.files = R.uniq(files);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
module.exports = inlineLinkCss;
|
||||
@@ -3,8 +3,8 @@
|
||||
"version": "0.2.2",
|
||||
"description": "Inline local assets referenced in an HTML document.",
|
||||
"repository": "panosoft/inline-html",
|
||||
"engines" : {
|
||||
"node" : ">=4.0.0"
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
@@ -22,6 +22,7 @@
|
||||
"less": "^2.5.1",
|
||||
"mz": "^2.0.0",
|
||||
"postcss": "^5.0.12",
|
||||
"postcss-import": "^7.1.3",
|
||||
"postcss-url": "^5.0.0",
|
||||
"ramda": "^0.18.0",
|
||||
"string": "^3.3.0"
|
||||
|
||||
1
test/fixtures/basic.css
vendored
1
test/fixtures/basic.css
vendored
@@ -1 +1,2 @@
|
||||
/* basic.css */
|
||||
div { color: blue; }
|
||||
|
||||
3
test/fixtures/basic.less
vendored
3
test/fixtures/basic.less
vendored
@@ -1 +1,2 @@
|
||||
div { background-image: url('file.txt'); }
|
||||
/* basic.less */
|
||||
div { color: blue; }
|
||||
|
||||
1
test/fixtures/import.css
vendored
Normal file
1
test/fixtures/import.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import 'basic.css';
|
||||
1
test/fixtures/invalid-import.css
vendored
Normal file
1
test/fixtures/invalid-import.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import 'missing.css';
|
||||
2
test/fixtures/invalid-import.less
vendored
2
test/fixtures/invalid-import.less
vendored
@@ -1 +1 @@
|
||||
@import (less) 'missing.css';
|
||||
@import 'missing.less';
|
||||
|
||||
1
test/fixtures/invalid-syntax.css
vendored
Normal file
1
test/fixtures/invalid-syntax.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
div {
|
||||
1
test/fixtures/invalid-url.css
vendored
Normal file
1
test/fixtures/invalid-url.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
div { background-image: url('missing.png'); }
|
||||
4
test/fixtures/invalid-url.less
vendored
4
test/fixtures/invalid-url.less
vendored
@@ -1,3 +1 @@
|
||||
div {
|
||||
background-image: url('missing.png');
|
||||
}
|
||||
div { background-image: url('missing.png'); }
|
||||
|
||||
1
test/fixtures/nested-import.css
vendored
Normal file
1
test/fixtures/nested-import.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
@import 'import.css';
|
||||
1
test/fixtures/url.css
vendored
Normal file
1
test/fixtures/url.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
div { background-image: url('file.txt'); }
|
||||
1
test/fixtures/url.less
vendored
Normal file
1
test/fixtures/url.less
vendored
Normal file
@@ -0,0 +1 @@
|
||||
div { background-image: url('file.txt'); }
|
||||
117
test/index.js
117
test/index.js
@@ -324,11 +324,100 @@ describe('inline-html', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('link-css', () => {
|
||||
const filename = path.resolve(__dirname, 'index.html');
|
||||
const link = (href) => `<link rel="stylesheet" href="${href}"/>`;
|
||||
it('inline local href', () => co(function * () {
|
||||
const href = path.resolve(__dirname, 'fixtures/basic.css');
|
||||
const html = link(href);
|
||||
const result = yield inline.html(html);
|
||||
expect(result).to.match(/<style>[^]*basic.css[^]*<\/style>/);
|
||||
}));
|
||||
it('ignore remote href', () => co(function * () {
|
||||
const html = link('http://test.com/main.less');
|
||||
const result = yield inline.html(html);
|
||||
expect(result).to.equal(html);
|
||||
}));
|
||||
it('ignore template expression href', () => co(function * () {
|
||||
const html = link('{{href}}');
|
||||
const result = yield inline.html(html);
|
||||
expect(result).to.equal(html);
|
||||
}));
|
||||
it('inline local nested imports', () => co(function * () {
|
||||
const href = path.resolve(__dirname, 'fixtures/nested-import.css');
|
||||
const html = link(href);
|
||||
const result = yield inline.html(html);
|
||||
return expect(result).to.match(/<style>[^]*basic.css[^]*<\/style>/)
|
||||
.and.not.match(/@import/);
|
||||
}));
|
||||
it('rebase urls relative to html filename', () => co(function * () {
|
||||
const href = 'fixtures/url.css';
|
||||
const html = link(href);
|
||||
const result = yield inline.html(html, { filename });
|
||||
expect(result).to.match(/<style>[^]*url\('data:.*,.*'\)[^]*<\/style>/);
|
||||
}));
|
||||
it('throw error when href invalid', () => co(function * () {
|
||||
const invalid = 'fixtures/missing.css';
|
||||
const invalidResolved = path.resolve(path.dirname(filename), invalid);
|
||||
const html = link(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(invalidResolved);
|
||||
}
|
||||
}));
|
||||
it('throw error when import path invalid', () => co(function * () {
|
||||
const invalid = 'fixtures/invalid-import.css';
|
||||
const invalidResolved = path.resolve(path.dirname(filename), invalid);
|
||||
const html = link(invalid);
|
||||
try {
|
||||
yield inline.html(html, { filename });
|
||||
throw new Error('No error thrown');
|
||||
}
|
||||
catch (error) {
|
||||
expect(error).to.have.property('filename').that.equals(invalidResolved);
|
||||
expect(error).to.have.property('files').that.contains(invalidResolved);
|
||||
}
|
||||
}));
|
||||
it('throw error when css syntax invalid', () => co(function * () {
|
||||
const invalid = 'fixtures/invalid-syntax.css';
|
||||
const invalidResolved = path.resolve(path.dirname(filename), invalid);
|
||||
const html = link(invalid);
|
||||
try {
|
||||
yield inline.html(html, { filename });
|
||||
throw new Error('No error thrown');
|
||||
}
|
||||
catch (error) {
|
||||
expect(error).to.have.property('filename').that.equals(invalidResolved);
|
||||
expect(error).to.have.property('files').that.contains(invalidResolved);
|
||||
}
|
||||
}));
|
||||
it('include all local hrefs in error.files when error encountered', () => co(function * () {
|
||||
const valid = 'fixtures/basic.css';
|
||||
const invalid = 'fixtures/missing.css';
|
||||
const validResolved = path.resolve(path.dirname(filename), valid);
|
||||
const invalidResolved = path.resolve(path.dirname(filename), invalid);
|
||||
const html = `${link(invalid)}${link(valid)}`;
|
||||
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(validResolved);
|
||||
expect(error).to.have.property('files').that.contains(invalidResolved);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
describe('link-less', () => {
|
||||
it('inline local href', () => {
|
||||
const filename = path.resolve(__dirname, 'fixtures/basic.less');
|
||||
const html = `<link rel="stylesheet/less" href="${filename}"/>`;
|
||||
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/);
|
||||
return expect(inline.html(html)).to.eventually.match(/<style>[^]*basic.less[^]*<\/style>/);
|
||||
});
|
||||
it('ignore remote href', () => {
|
||||
const html = `<link rel="stylesheet/less" href="http://test.com/main.less"/>`;
|
||||
@@ -341,14 +430,14 @@ describe('inline-html', () => {
|
||||
it('inline local nested imports', () => {
|
||||
const filename = path.resolve(__dirname, 'fixtures/nested-import.less');
|
||||
const html = `<link rel="stylesheet/less" href="${filename}"/>`;
|
||||
return expect(inline.html(html)).to.eventually.match(/<style>[^]*<\/style>/)
|
||||
return expect(inline.html(html)).to.eventually.match(/<style>[^]*basic.less[^]*basic.css[^]*<\/style>/)
|
||||
.and.not.match(/@import/);
|
||||
});
|
||||
it('rebase urls relative to html filename', () => {
|
||||
const filename = path.resolve(__dirname, 'index.html');
|
||||
const href = 'fixtures/basic.less';
|
||||
const href = 'fixtures/url.less';
|
||||
const html = `<link rel="stylesheet/less" href="${href}"/>`;
|
||||
return expect(inline.html(html, { filename })).to.eventually.match(/<style>[^]*<\/style>/);
|
||||
return expect(inline.html(html, { filename })).to.eventually.match(/<style>[^]*url\('data:.*,.*'\)[^]*<\/style>/);
|
||||
});
|
||||
it('throw error when link href invalid', () => co(function * () {
|
||||
const filename = path.resolve(__dirname, 'index.html');
|
||||
@@ -392,20 +481,24 @@ describe('inline-html', () => {
|
||||
expect(error).to.have.property('files').that.contains(lessFilename);
|
||||
}
|
||||
}));
|
||||
it('throw error when less url invalid', () => co(function * () {
|
||||
const filename = path.resolve(__dirname, 'fixtures/index.html');
|
||||
const lessBasename = 'invalid-url.less';
|
||||
const badUrl = path.resolve(path.dirname(filename), 'missing.png');
|
||||
const html = `<link rel="stylesheet/less" href="${lessBasename}">`;
|
||||
it('include all local hrefs in error.files when error encountered', () => co(function * () {
|
||||
const filename = path.resolve(__dirname, 'index.html');
|
||||
const valid = 'fixtures/basic.css';
|
||||
const invalid = 'fixtures/missing.css';
|
||||
const validResolved = path.resolve(path.dirname(filename), valid);
|
||||
const invalidResolved = path.resolve(path.dirname(filename), invalid);
|
||||
const html = `
|
||||
<link rel="stylesheet/less" href="${invalid}">
|
||||
<link rel="stylesheet/less" href="${valid}">
|
||||
`;
|
||||
try {
|
||||
yield inline.html(html, {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);
|
||||
expect(error).to.have.property('files').that.contains(validResolved);
|
||||
expect(error).to.have.property('files').that.contains(invalidResolved);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user