mirror of
				https://github.com/KevinMidboe/inline-html.git
				synced 2025-10-29 17:40: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