const U2 = require("uglify-js");
const htmlMinify = require('html-minifier').minify;
const CleanCSS = require('clean-css');
const svgo = require('svgo');
const convert = require('xml-js');
defaultOptions = {
collapseWhitespace: true,
minifyCSS: true,
minifySVG: true,
svgOptions: {
plugins: svgo.extendDefaultPlugins([
{
name: 'removeViewBox',
active: false
}
]),
}
}
/**
* Extract element containing token to Cdata element
* @param {string} token
* @param {XMLElement} elt
* @returns {XMLElement} tranform XML tree by stringifying element containing the token
*/
const toCData = function (elt, token) {
let transform = elt.name == token;
if (elt.type == "text" && elt.text.includes(token)) {
transform = true
} else if ("attributes" in elt) {
for (const k of Object.keys(elt.attributes)) {
if (k.includes(token)) {
transform = true;
} else if (elt.attributes[k].includes(token)) {
transform = true
}
if (transform) {
break
}
}
}
if (transform) {
const newElt = {
type: "cdata",
cdata: token + JSON.stringify(elt)
}
return newElt
} else {
if ('elements' in elt) {
elt.elements = elt.elements.map(e => toCData(e, token))
}
return elt
}
}
/**
* Recover extracted element containing token from Cdata element
* @param {string} token
* @param {XMLElement} elt
* @returns {XMLElement} restored XML tree
*/
const fromCData = function (elt, token) {
if (elt.type == "cdata" && elt.cdata.startsWith(token)) {
return JSON.parse(elt.cdata.substr(token.length))
} else {
if ('elements' in elt) {
elt.elements = elt.elements.map(e => fromCData(e, token))
}
return elt
}
}
const processASTNode = function(node,htmlOptions,cssOptions,svgOptions){
if (node instanceof U2.AST_Template
&& node.tag
&& ['html', 'css', 'svg'].includes(node.tag.name)) {
const templateContent = node.strings.map(s => s.replace(/[\n|\r]/gm, ' '))
if (node.tag.name == "html") {
const input = templateContent.join('${}');
const output = htmlMinify(input, htmlOptions)
node.strings = output.split("${}");
} else if (node.tag.name == "css" && cssOptions) {
if (templateContent.length > 1) {
throw new Error("css literal cannot use template.\r\nMore info https://lit-element.polymer-project.org/api/modules/_lit_element_.html#css ")
}
const input = templateContent[0];
const output = new CleanCSS(cssOptions).minify(input).styles
node.strings = [output];
} else if (node.tag.name == "svg" && svgOptions) {
const token = 'validTokenMlet'
const input = templateContent.join(token);
// Parse XML
let obj = convert.xml2js(input)
// Transform elements containing template to CData elt
obj = toCData(obj, token)
// Optimise svg (it does not affect Cdata elements)
let xml = convert.js2xml(obj);
xml = svgo.optimize(xml, svgOptions).data
// Revert CData elt to their original form
obj = convert.xml2js(xml)
obj = fromCData(obj, token)
// Reconstruct template
node.strings = convert.js2xml(obj).split(token);
}
}
}
module.exports = function (code, options) {
let mergedOptions = { ...defaultOptions, ...options }
let cssOptions = false;
if (mergedOptions.minifyCSS) {
cssOptions = typeof mergedOptions.minifyCSS == 'object' ? mergedOptions.minifyCSS : {}
}
let svgOptions = false;
if (mergedOptions.minifySVG) {
svgOptions = mergedOptions.svgOptions ? mergedOptions.svgOptions : {};
}
const walker = new U2.TreeWalker((node)=>processASTNode(node,mergedOptions,cssOptions,svgOptions));
const ast = U2.parse(code)
ast.walk(walker)
return ast.print_to_string({ beautify: false });;
}