UNPKG

htmlnano

Version:

Modular HTML minifier, built on top of the PostHTML

147 lines (145 loc) 5.41 kB
function normalizeAttrValue(value) { if (typeof value !== 'string') return null; const trimmed = value.trim(); return trimmed.length === 0 ? '' : trimmed.toLowerCase(); } function findAttrEntry(attrs, name) { const targetName = name.toLowerCase(); for (const [attrName, attrValue] of Object.entries(attrs)){ if (attrName.toLowerCase() === targetName) { return { name: attrName, value: attrValue }; } } return null; } function getNormalizedAttrValue(attrs, name) { const entry = findAttrEntry(attrs, name); return entry ? normalizeAttrValue(entry.value) : null; } function hasAttr(attrs, name) { return findAttrEntry(attrs, name) !== null; } // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#JavaScript_types const redundantScriptTypes = new Set([ 'application/javascript', 'application/ecmascript', 'application/x-ecmascript', 'application/x-javascript', 'text/javascript', 'text/ecmascript', 'text/javascript1.0', 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4', 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript' ]); // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default const missingValueDefaultAttributes = { form: { method: 'get' }, input: { type: 'text' }, button: { // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type type: 'submit' }, script: { language: 'javascript', // https://html.spec.whatwg.org/#fetch-priority-attributes fetchpriority: 'auto', type: (attrs)=>{ const typeValue = getNormalizedAttrValue(attrs, 'type'); return typeValue !== null && redundantScriptTypes.has(typeValue); }, // Remove attribute if the function returns false charset: (attrs)=>{ // The charset attribute only really makes sense on “external” SCRIPT elements: // http://perfectionkills.com/optimizing-html/#8_script_charset return !hasAttr(attrs, 'src'); } }, style: { media: 'all', type: 'text/css' }, link: { media: 'all', // https://html.spec.whatwg.org/#fetch-priority-attributes fetchpriority: 'auto', type: (attrs)=>{ // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet const relValue = getNormalizedAttrValue(attrs, 'rel'); const typeValue = getNormalizedAttrValue(attrs, 'type'); if (!relValue || !typeValue) { return false; } const relTokens = relValue.split(/\s+/); const isRelStyleSheet = relTokens.includes('stylesheet'); const isTypeTextCSS = typeValue === 'text/css'; // Only "text/css" is redundant for link[rel=stylesheet]. Otherwise "type" shouldn't be removed return isRelStyleSheet && isTypeTextCSS; } }, // See: https://html.spec.whatwg.org/#lazy-loading-attributes img: { loading: 'eager', // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decoding decoding: 'auto', // https://html.spec.whatwg.org/#fetch-priority-attributes fetchpriority: 'auto' }, iframe: { loading: 'eager' }, // https://html.spec.whatwg.org/multipage/media.html#htmltrackelement track: { kind: 'subtitles' }, textarea: { // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-wrap wrap: 'soft' }, area: { // https://html.spec.whatwg.org/multipage/image-maps.html#attr-area-shape shape: 'rect' } }; const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(missingValueDefaultAttributes)); /** Removes redundant attributes */ const mod = { onAttrs () { return (attrs, node)=>{ if (!node.tag) return attrs; const newAttrs = attrs; const attrsRecord = attrs; if (tagsHaveMissingValueDefaultAttributes.has(node.tag)) { const tagRedundantAttributes = missingValueDefaultAttributes[node.tag]; for (const redundantAttributeName of Object.keys(tagRedundantAttributes)){ const tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName]; let isRemove = false; const attrEntry = findAttrEntry(attrsRecord, redundantAttributeName); if (typeof tagRedundantAttributeValue === 'function') { isRemove = tagRedundantAttributeValue(attrsRecord); } else if (attrEntry) { const normalizedValue = normalizeAttrValue(attrEntry.value); isRemove = normalizedValue !== null && normalizedValue === tagRedundantAttributeValue; } if (isRemove && attrEntry) { delete newAttrs[attrEntry.name]; } } } return newAttrs; }; } }; export { mod as default, redundantScriptTypes };