htmlnano
Version:
Modular HTML minifier, built on top of the PostHTML
134 lines (130 loc) • 4.31 kB
JavaScript
import { extractTextContentFromNode } from '../helpers.mjs';
function normalizeAttrsForKey(attrs, config) {
const normalized = {
...config.baseAttrs
};
for (const [key, value] of Object.entries(attrs || {})){
if (config.skippedAttrs.has(key) || value === undefined) {
continue;
}
if (config.booleanAttrs.has(key)) {
normalized[key] = true;
continue;
}
normalized[key] = value;
}
return normalized;
}
function normalizeAsyncAttr(attrs) {
if (!attrs) {
return;
}
if (attrs.async === '') {
attrs.async = true;
}
if (attrs.nomodule === '') {
attrs.nomodule = true;
}
}
function getScriptType(attrs) {
const type = attrs.type || 'text/javascript';
return typeof type === 'string' ? type.toLowerCase() : 'text/javascript';
}
function isMergeableScriptType(type) {
return type === 'text/javascript' || type === 'application/javascript';
}
const booleanAttrs = new Set([
'async',
'defer',
'nomodule'
]);
const skippedAttrs = new Set([
'src',
'integrity',
'type'
]);
function normalizeScriptAttrsForKey(attrs, scriptType) {
return normalizeAttrsForKey(attrs, {
baseAttrs: {
type: scriptType
},
booleanAttrs,
skippedAttrs
});
}
function buildScriptKey(attrs, scriptType, scriptSrcIndex) {
const normalizedAttrs = normalizeScriptAttrsForKey(attrs, scriptType);
const keyObject = {
index: scriptSrcIndex,
...normalizedAttrs
};
return JSON.stringify(Object.fromEntries(Object.entries(keyObject).sort()));
}
function endsWithLineComment(scriptContent) {
const lastNewlineIndex = Math.max(scriptContent.lastIndexOf('\n'), scriptContent.lastIndexOf('\r'));
const lastLine = lastNewlineIndex === -1 ? scriptContent : scriptContent.slice(lastNewlineIndex + 1);
return /\/\/.*$/.test(lastLine);
}
function mergeScriptNodes(scriptNodesIndex, tracking) {
for (const scriptNodes of Object.values(scriptNodesIndex)){
if (scriptNodes.length < 2) {
continue;
}
const lastScriptNode = scriptNodes.pop();
tracking.mergedScriptNodes.add(lastScriptNode);
scriptNodes.reverse().forEach((scriptNode)=>{
let scriptContent = extractTextContentFromNode(scriptNode).trim();
if (!scriptContent) {
tracking.removedScriptNodes.add(scriptNode);
// @ts-expect-error -- remove node
scriptNode.tag = false;
scriptNode.content = [];
return;
}
if (endsWithLineComment(scriptContent)) {
scriptContent += '\n;';
} else if (scriptContent.slice(-1) !== ';') {
scriptContent += ';';
}
lastScriptNode.content = lastScriptNode.content || [];
lastScriptNode.content.unshift(scriptContent);
tracking.removedScriptNodes.add(scriptNode);
// @ts-expect-error -- remove node
scriptNode.tag = false;
scriptNode.content = [];
});
}
}
/* Merge multiple <script> into one */ const mod = {
default (tree) {
const scriptNodesIndex = {};
const tracking = {
mergedScriptNodes: new WeakSet(),
removedScriptNodes: new WeakSet()
};
let scriptSrcIndex = 1;
tree.match({
tag: 'script'
}, (node)=>{
const nodeAttrs = node.attrs || {};
normalizeAsyncAttr(nodeAttrs);
if ('src' in nodeAttrs || 'integrity' in nodeAttrs) {
scriptSrcIndex++;
return node;
}
const scriptType = getScriptType(nodeAttrs);
if (!isMergeableScriptType(scriptType)) {
return node;
}
const scriptKey = buildScriptKey(nodeAttrs, scriptType, scriptSrcIndex);
if (!scriptNodesIndex[scriptKey]) {
scriptNodesIndex[scriptKey] = [];
}
scriptNodesIndex[scriptKey].push(node);
return node;
});
mergeScriptNodes(scriptNodesIndex, tracking);
return tree;
}
};
export { mod as default };