UNPKG

htmlnano

Version:

Modular HTML minifier, built on top of the PostHTML

126 lines (122 loc) 4.49 kB
import { isListAttribute } from './collapseAttributeWhitespace.mjs'; const validOptions = new Set([ 'frequency', 'alphabetical' ]); function resolveSortType(options) { if (options === true) return 'alphabetical'; if (options === false) return false; return validOptions.has(options) ? options : false; } // class, rel, ping class ListAttributeTokenChain { addFromNodeAttrsArray(attrValuesArray) { attrValuesArray.forEach((attrValue)=>{ if (!attrValue) { return; } if (this.tokenCounts.has(attrValue)) { this.tokenCounts.set(attrValue, this.tokenCounts.get(attrValue) + 1); } else { this.tokenCounts.set(attrValue, 1); } }); } createSortOrder() { const nextSortOrder = [ ...this.tokenCounts.entries() ]; nextSortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0])); this.sortedTokens = nextSortOrder.map((i)=>i[0]); } sortFromNodeAttrsArray(attrValuesArray) { const resultArray = []; const tokenCounts = new Map(); attrValuesArray.forEach((attrValue)=>{ var _tokenCounts_get; if (!attrValue) { return; } tokenCounts.set(attrValue, ((_tokenCounts_get = tokenCounts.get(attrValue)) != null ? _tokenCounts_get : 0) + 1); }); if (!this.sortedTokens) { this.createSortOrder(); } this.sortedTokens.forEach((k)=>{ const count = tokenCounts.get(k); if (!count) { return; } for(let i = 0; i < count; i += 1){ resultArray.push(k); } }); return resultArray; } constructor(){ /** <attr, frequency> */ this.tokenCounts = new Map(); this.sortedTokens = null; } } /** Sort values inside list-like attributes (e.g. class, rel) */ const mod = { default (tree, options, moduleOptions) { const sortType = resolveSortType(moduleOptions); if (sortType === 'alphabetical') { return sortAttributesWithListsInAlphabeticalOrder(tree); } if (sortType === 'frequency') { return sortAttributesWithListsByFrequency(tree); } // Invalid configuration return tree; } }; const splitListAttributeValues = (attrValue)=>attrValue.split(/\s+/).filter(Boolean); function walkListAttributes(tree, walkFn) { tree.walk((node)=>{ if (!node.attrs) { return node; } const tagName = node.tag ? node.tag.toLowerCase() : undefined; Object.entries(node.attrs).forEach(([attrName, attrValues])=>{ const attrNameLower = attrName.toLowerCase(); if (!isListAttribute(attrNameLower, tagName) || typeof attrValues !== 'string') { return; } walkFn(node.attrs, attrName, attrValues); }); return node; }); } function sortAttributesWithListsInAlphabeticalOrder(tree) { walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{ const values = splitListAttributeValues(attrValues); if (values.length < 2) { return; } nodeAttrs[attrName] = values.sort((a, b)=>{ // @ts-expect-error -- deliberately use minus operator to sort things return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b; }).join(' '); }); return tree; } function sortAttributesWithListsByFrequency(tree) { const tokenChainObj = {}; // Traverse through tree to get frequency walkListAttributes(tree, (_nodeAttrs, attrName, attrValues)=>{ const attrNameLower = attrName.toLowerCase(); tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new ListAttributeTokenChain(); tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues)); }); // Traverse through tree again, this time sort the attribute values walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{ const attrNameLower = attrName.toLowerCase(); if (!tokenChainObj[attrNameLower]) { return; } nodeAttrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' '); }); return tree; } export { mod as default };