inline-assets
Version:
Inline External Assets of HTML/CSS Files
304 lines (286 loc) • 12.2 kB
JavaScript
/*
** node-inline-assets -- Inline External Assets of HTML/CSS Files
** Copyright (c) 2014-2023 Dr. Ralf S. Engelschall <http://engelschall.com>
**
** Permission is hereby granted, free of charge, to any person obtaining
** a copy of this software and associated documentation files (the
** "Software"), to deal in the Software without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Software, and to
** permit persons to whom the Software is furnished to do so, subject to
** the following conditions:
**
** The above copyright notice and this permission notice shall be included
** in all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* standard requirements */
var fs = require("fs")
var path = require("path")
/* extra requirements */
var _ = require("lodash")
var datauri = require("datauri/parser")
var htmlmin = require("html-minifier").minify
var CSSO = require("csso")
var uglifyjs = require("uglify-js")
/* generate regular expression for an XML simple tag */
var reXMLTagSimple = function (name) {
return new RegExp(
"<(" + name + ")" +
"((?:\\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:\"[^\"]*\"|'[^']*'|\\S+))?)*)" +
"\\s*(?:\\/>|>\\s*<\\/" + name + ">|>)", "g"
)
}
/* generate regular expression for an XML complex tag */
var reXMLTagComplex = function (name) {
return new RegExp(
"<(" + name + ")" +
"((?:\\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:\"[^\"]*\"|'[^']*'|\\S+))?)*)" +
"\\s*>(.*?)<\\/" + name + ">", "g"
)
}
/* parse attributes of a XML tag */
var attrOfTag = function (tag) {
var attr = {}
tag.replace(/\s+([a-zA-Z][a-zA-Z0-9-]*)(?:=(?:"([^"]*)"|'([^']*)'|(\S+)))?/g,
function (all, key, v1, v2, v3) {
var val = (v1 !== undefined ? v1 : (v2 !== undefined ? v2 : (v3 !== undefined ? v3 : true)))
attr[key] = (typeof val === "string" ? val.replace(/"/, "\"").replace(/&/g, "&") : val)
}
)
return attr
}
/* generate an XML tag */
var genXMLTag = function (name, attrs, body) {
var tag = "<" + name
_.forEach(attrs, function (val, key) {
if (val === true)
tag += " " + key
else
tag += " " + key + "=\"" + val.replace(/&/g, "&").replace(/"/g, """) + "\""
})
if (typeof body !== "undefined")
tag += ">" + body + "</" + name + ">"
else
tag += "/>"
return tag
}
/* cleanup an URL for use as a filename */
var url2fn = function (url) {
return url.replace(/\?.*$/, "").replace(/#.*$/, "")
}
/* determine whether asset is processed in inlining process */
var processAsset = function (options, filename) {
var pattern = options.pattern
var processIt = (pattern[0].substr(0, 1) === "!")
for (var i = 0; i < pattern.length; i++) {
var negate = false
var regex = pattern[i]
if (regex.substr(0, 1) === "!") {
negate = true
regex = regex.substr(1)
}
regex = new RegExp(regex)
if (filename.match(regex))
processIt = !negate
}
return processIt
}
/* send caller verbose information */
var verbose = function (options, action, type, filename, lenBefore, lenAfter) {
if (typeof options.verbose === "function")
options.verbose(action, type, filename, lenBefore, lenAfter)
}
/* inline assets of a piece of arbitrary data */
var inlineAssetsForData = function (basename, filename, content, options) {
var len = content.length
verbose(options, "expanding", "DATA", filename, len, -1)
content = (new datauri()).format(filename, content).content
verbose(options, "expanded", "DATA", filename, len, content.length)
return content
}
/* inline assets of a piece of JavaScript */
var inlineAssetsForJS = function (basename, filename, content, options) {
var len = content.length
verbose(options, "expanding", "JS", filename, len, -1)
if (options.jsmin) {
var len2 = content.length
verbose(options, "minifying", "JS", filename, len2, -1)
content = uglifyjs.minify(content, { fromString: true, mangle: false })
verbose(options, "minifying", "JS", filename, len2, content.length)
}
verbose(options, "expanded", "JS", filename, len, content.length)
return content
}
/* inline assets of a piece of CSS */
var inlineAssetsForCSS = function (basename, filename, content, options) {
var len = content.length
verbose(options, "expanding", "CSS", filename, len, -1)
var reURL1 = "@import\\s+" +
"(?:" +
"url\\((?:\"([^\"]*)\"|'([^']*)'|(\\S+?))\\)|" +
"(?:\"([^\"]*)\"|'([^']*)')" +
")" +
"(\\s+[^;\\r\\n]+)?" +
"\\s*;"
var reURL2 = "\\b" +
"url\\((?:\"([^\"]*)\"|'([^']*)'|(\\S+?))\\)" +
"(?:\\s*format\\((?:\"([^\"]*)\"|'([^']*)'|(\\S+?))\\))?" +
"(\\s*,)?"
content = content.replace(new RegExp(reURL1, "g"),
function (stmt, v1, v2, v3, v4, v5, media) {
var fn = (v1 !== undefined ? v1 : (v2 !== undefined ? v2 : (v3 !== undefined ? v3 : (v4 !== undefined ? v4 : v5))))
if (!fn.match(/^(?:data|https?):/)) {
fn = path.resolve(path.dirname(filename), url2fn(fn))
if (processAsset(options, fn)) {
var css = fs.readFileSync(fn, { encoding: "utf8" })
css = inlineAssetsForCSS(path.dirname(filename), fn, css, options) /* RECURSION! */
if (typeof media === "string")
css = "@media" + media + " {" + css + "}"
stmt = css
}
else if (options.purge)
stmt = ""
}
return stmt
}
)
content = content.replace(new RegExp(reURL2, "g"), function (stmt, v1, v2, v3, f1, f2, f3, epilog) {
var fn = (v1 !== undefined ? v1 : (v2 !== undefined ? v2 : v3))
var fm = (f1 !== undefined ? f1 : (f2 !== undefined ? f2 : f3))
if (!fn.match(/^(?:data|https?):/)) {
fn = path.resolve(path.dirname(filename), url2fn(fn))
if (processAsset(options, fn)) {
var data = fs.readFileSync(fn, { encoding: null })
data = inlineAssetsForData(filename, fn, data, options)
stmt = "url(\"" + data + "\")"
if (fm)
stmt += " format(\"" + fm + "\")"
if (epilog)
stmt += epilog
}
else if (options.purge)
stmt = ""
}
return stmt
})
if (options.cssmin) {
var len2 = content.length
verbose(options, "minifying", "CSS", filename, len2, -1)
content = CSSO.minify(content, {
restructure: false,
sourceMap: false
}).css
verbose(options, "minifyed", "CSS", filename, len2, content.length)
}
verbose(options, "expanded", "CSS", filename, len, content.length)
return content
}
/* inline assets of a piece of HTML */
var inlineAssetsForHTML = function (basename, filename, content, options) {
var len = content.length
verbose(options, "expanding", "HTML", filename, len, -1)
content = content.replace(reXMLTagSimple("script"), function (tag, name, attrs) {
var attr = attrOfTag(attrs)
if ( typeof attr.type === "undefined"
|| attr.type === "text/javascript"
|| (typeof attr.src === "string" && attr.src.match(/\.js/i))) {
if (typeof attr.src === "string" && !attr.src.match(/^(?:data|https?):/)) {
var fn = path.resolve(path.dirname(filename), url2fn(attr.src))
if (processAsset(options, fn)) {
var js = fs.readFileSync(fn, { encoding: "utf8" })
js = inlineAssetsForJS(path.dirname(filename), fn, js, options)
delete attr.src
tag = genXMLTag("script", attr, js)
}
}
}
return tag
})
content = content.replace(reXMLTagSimple("link"), function (tag, name, attrs) {
var attr = attrOfTag(attrs)
if ( attr.rel === "stylesheet"
|| attr.type === "text/css"
|| (typeof attr.href === "string" && attr.href.match(/\.css$/i))) {
if (typeof attr.href === "string" && !attr.href.match(/^(?:data|https?):/)) {
var fn = path.resolve(path.dirname(filename), url2fn(attr.href))
if (processAsset(options, fn)) {
var css = fs.readFileSync(fn, { encoding: "utf8" })
css = inlineAssetsForCSS(path.dirname(filename), fn, css, options)
if (typeof attr.media !== "undefined")
css = "@media " + attr.media + " {" + css + " }"
tag = genXMLTag("style", { type: "text/css" }, css)
}
}
}
return tag
})
content = content.replace(reXMLTagComplex("style"), function (tag, name, attrs, body) {
var attr = attrOfTag(attrs)
if ( typeof attr.type === "undefined"
|| attr.type === "text/css" ) {
body = inlineAssetsForCSS(basename, filename, body, options)
tag = genXMLTag("style", { type: "text/css" }, body)
}
return tag
})
content = content.replace(reXMLTagSimple("img"), function (tag, name, attrs) {
var attr = attrOfTag(attrs)
var fn
if (typeof attr.src === "string" && !attr.src.match(/^(?:data|https?):/)) {
if (attr.src.match(/\.(?:png|gif|jpe?g|svg)$/i)) {
fn = path.resolve(path.dirname(filename), url2fn(attr.src))
if (processAsset(options, fn)) {
var data = fs.readFileSync(fn)
data = inlineAssetsForData(filename, fn, data, options)
attr.src = data
tag = genXMLTag("img", attr)
}
}
}
return tag
})
if (options.htmlmin) {
var len2 = content.length
verbose(options, "minifying", "HTML", filename, len2, -1)
content = htmlmin(content, {
removeComments: true,
collapseWhitespace: true
})
verbose(options, "minifyed", "HTML", filename, len2, content.length)
}
verbose(options, "expanded", "HTML", filename, len, content.length)
return content
}
/* inline assets of an arbitrary piece */
var inlineAssets = function (basename, filename, content, options) {
/* provide option defaults */
if (typeof options.cssmin === "undefined")
options.cssmin = false
if (typeof options.htmlmin === "undefined")
options.htmlmin = false
if (typeof options.jsmin === "undefined")
options.jsmin = false
if (typeof options.pattern === "undefined")
options.pattern = [ ".+" ]
if (typeof options.purge === "undefined")
options.purge = false
/* dispatch according to file extension */
if (filename.match(/\.html?$/i))
return inlineAssetsForHTML(basename, filename, content, options)
else if (filename.match(/\.css$/i))
return inlineAssetsForCSS(basename, filename, content, options)
else if (filename.match(/\.js$/i))
return inlineAssetsForJS(basename, filename, content, options)
else
return inlineAssetsForData(basename, filename, content, options)
}
/* export the packing API function */
module.exports = inlineAssets