UNPKG

htmlnano

Version:

Modular HTML minifier, built on top of the PostHTML

137 lines (133 loc) 4.99 kB
import { isAmpBoilerplate, 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; } const booleanAttrs = new Set([ 'amp-custom', 'disabled' ]); const skippedAttrs = new Set([ 'type', 'media' ]); function normalizeStyleType(attrs) { if (!attrs || typeof attrs.type !== 'string') { return 'text/css'; } const type = attrs.type.trim(); return type ? type.toLowerCase() : 'text/css'; } function normalizeStyleMedia(attrs) { if (!attrs || typeof attrs.media !== 'string') { return 'all'; } const media = attrs.media.trim(); return media ? media.replace(/\s+/g, ' ').toLowerCase() : 'all'; } function normalizeStyleAttrsForKey(attrs) { return normalizeAttrsForKey(attrs, { booleanAttrs, skippedAttrs }); } function buildStyleKey(attrs) { const keyObject = { type: normalizeStyleType(attrs), media: normalizeStyleMedia(attrs), ...normalizeStyleAttrsForKey(attrs) }; return JSON.stringify(Object.fromEntries(Object.entries(keyObject).sort())); } function extractStyleTextContent(node) { if (typeof node.content === 'string') { return node.content; } return extractTextContentFromNode(node); } // A <link rel="stylesheet"> pulls in external CSS at its position in the // document, so it participates in the cascade. Other rel values (e.g. // "preload") do not apply styles and must not break a merge group. function isStylesheetLink(node) { if (node.tag !== 'link' || !node.attrs || typeof node.attrs.rel !== 'string') { return false; } return node.attrs.rel.trim().toLowerCase().split(/\s+/).includes('stylesheet'); } /* Merge multiple <style> into one */ const mod = { default (tree) { // Only merge <style> tags that form a contiguous run (in document // order) with no intervening stylesheet source between them. CSS rules // of equal specificity resolve by source order, so merging styles // across an intervening <link rel="stylesheet"> or a <style> with // different group attributes (type/media/...) could silently change // which rules win. We therefore keep at most one open merge group at a // time and flush it whenever such a source is encountered. let activeKey = null; let activeNode = null; function flushGroup() { activeKey = null; activeNode = null; } tree.walk((node)=>{ if (typeof node !== 'object' || !node.tag) { return node; } // A stylesheet <link> contributes to the cascade at its position, // so it ends any open merge group. if (isStylesheetLink(node)) { flushGroup(); return node; } if (node.tag !== 'style' || !node.content) { return node; } const nodeAttrs = node.attrs || {}; // Skip <style scoped></style> // https://developer.mozilla.org/en/docs/Web/HTML/Element/style // // Also skip SRI, reasons are documented in "minifyJs" module // // These styles are excluded from merging, but they still apply to // the cascade, so they end any open merge group. if ('scoped' in nodeAttrs || 'integrity' in nodeAttrs) { flushGroup(); return node; } // AMP boilerplate styles are left untouched and do not participate // in the mergeable cascade, so they don't break a group. if (isAmpBoilerplate(node)) { return node; } const styleKey = buildStyleKey(nodeAttrs); if (activeNode && activeKey === styleKey) { var _activeNode, _content; const styleContent = extractStyleTextContent(node); (_content = (_activeNode = activeNode).content) != null ? _content : _activeNode.content = []; activeNode.content.push(' ' + styleContent); return ''; // Remove node } // A <style> with different group attributes is still a stylesheet // source at this position, so it ends the previous group before // starting a new one. node.content = node.content || []; activeKey = styleKey; activeNode = node; return node; }); return tree; } }; export { mod as default };