Skip to content

Commit

Permalink
Merge pull request #128 from SukkaW/refactor-once
Browse files Browse the repository at this point in the history
Feat: implements #127
  • Loading branch information
maltsev committed Apr 2, 2022
2 parents e4eedd0 + 53216bf commit c4d7893
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 146 deletions.
56 changes: 53 additions & 3 deletions lib/htmlnano.es6
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ function htmlnano(optionsRun, presetRun) {
let [options, preset] = loadConfig(optionsRun, presetRun);

return function minifier(tree) {
const nodeHandlers = [];
const attrsHandlers = [];
const contentsHandlers = [];

options = { ...preset, ...options };
let promise = Promise.resolve(tree);

Expand All @@ -73,11 +77,57 @@ function htmlnano(optionsRun, presetRun) {
}
});

let module = require('./modules/' + moduleName);
promise = promise.then(tree => module.default(tree, options, moduleOptions));
const module = require('./modules/' + moduleName);

if (module.onAttrs) {
attrsHandlers.push(module.onAttrs(options, moduleOptions));
}
if (module.onContent) {
contentsHandlers.push(module.onContent(options, moduleOptions));
}
if (module.onNode) {
nodeHandlers.push(module.onNode(options, moduleOptions));
}
if (module.default) {
promise = promise.then(tree => module.default(tree, options, moduleOptions));
}
}

if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
return promise;
}

return promise;
return promise.then(tree => {
tree.walk(node => {
if (node.attrs) {
// Convert all attrs' key to lower case
let newAttrsObj = {};
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
newAttrsObj[attrName.toLowerCase()] = attrValue;
});

for (const handler of attrsHandlers) {
newAttrsObj = handler(newAttrsObj, node);
}

node.attrs = newAttrsObj;
}

if (node.content) {
for (const handler of contentsHandlers) {
node.content = handler(node.content, node);
}
}

for (const handler of nodeHandlers) {
node = handler(node, node);
}

return node;
});

return tree;
});
};
}

Expand Down
37 changes: 14 additions & 23 deletions lib/modules/collapseAttributeWhitespace.es6
Original file line number Diff line number Diff line change
Expand Up @@ -68,42 +68,33 @@ const attributesWithSingleValue = {
};

/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
export default function collapseAttributeWhitespace(tree) {
tree.walk(node => {
if (!node.attrs) {
return node;
}
export function onAttrs() {
return (attrs, node) => {
const newAttrs = attrs;

Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
const attrNameLower = attrName.toLowerCase();

if (attributesWithLists.has(attrNameLower)) {
Object.entries(attrs).forEach(([attrName, attrValue]) => {
if (attributesWithLists.has(attrName)) {
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
node.attrs[attrName] = newAttrValue;

return node;
newAttrs[attrName] = newAttrValue;
return;
}

if (
isEventHandler(attrNameLower)
isEventHandler(attrName)
|| (
Object.hasOwnProperty.call(attributesWithSingleValue, attrNameLower)
Object.hasOwnProperty.call(attributesWithSingleValue, attrName)
&& (
attributesWithSingleValue[attrNameLower] === null
|| attributesWithSingleValue[attrNameLower].includes(node.tag)
attributesWithSingleValue[attrName] === null
|| attributesWithSingleValue[attrName].includes(node.tag)
)
)
) {
node.attrs[attrName] = minifySingleAttributeValue(attrValue);

return node;
newAttrs[attrName] = minifySingleAttributeValue(attrValue);
}
});

return node;
});

return tree;
return newAttrs;
};
}

function minifySingleAttributeValue(value) {
Expand Down
33 changes: 13 additions & 20 deletions lib/modules/collapseBooleanAttributes.es6
Original file line number Diff line number Diff line change
Expand Up @@ -102,43 +102,36 @@ const amphtmlBooleanAttributes = new Set([
'subscriptions-dialog'
]);

export function onAttrs(options, moduleOptions) {
return (attrs, node) => {
if (!node.tag) return attrs;

export default function collapseBooleanAttributes(tree, options, moduleOptions) {
tree.walk(node => {
if (! node.attrs) {
return node;
}

if (! node.tag) {
return node;
}
const newAttrs = attrs;

for (const attrName of Object.keys(node.attrs)) {
for (const attrName of Object.keys(attrs)) {
if (attrName === 'visible' && node.tag.startsWith('a-')) {
continue;
}

if (htmlBooleanAttributes.has(attrName)) {
node.attrs[attrName] = true;
newAttrs[attrName] = true;
}
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && node.attrs[attrName] === '') {
node.attrs[attrName] = true;
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
newAttrs[attrName] = true;
}

// collapse crossorigin attributes
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
if (
attrName.toLowerCase() === 'crossorigin' && (
node.attrs[attrName] === 'anonymous' ||
node.attrs[attrName] === ''
attrs[attrName] === 'anonymous' ||
attrs[attrName] === ''
)
) {
node.attrs[attrName] = true;
newAttrs[attrName] = true;
}
}

return node;
});

return tree;
return newAttrs;
};
}
25 changes: 10 additions & 15 deletions lib/modules/deduplicateAttributeValues.es6
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { attributesWithLists } from './collapseAttributeWhitespace';

/** Deduplicate values inside list-like attributes (e.g. class, rel) */
export default function collapseAttributeWhitespace(tree) {
tree.walk(node => {
if (! node.attrs) {
return node;
}

Object.keys(node.attrs).forEach(attrName => {
const attrNameLower = attrName.toLowerCase();
if (! attributesWithLists.has(attrNameLower)) {
export function onAttrs() {
return (attrs) => {
const newAttrs = attrs;
Object.keys(attrs).forEach(attrName => {
if (! attributesWithLists.has(attrName)) {
return;
}

const attrValues = node.attrs[attrName].split(/\s/);
const attrValues = attrs[attrName].split(/\s/);
const uniqeAttrValues = new Set();
const deduplicatedAttrValues = [];

attrValues.forEach((attrValue) => {
if (! attrValue) {
// Keep whitespaces
Expand All @@ -31,11 +28,9 @@ export default function collapseAttributeWhitespace(tree) {
uniqeAttrValues.add(attrValue);
});

node.attrs[attrName] = deduplicatedAttrValues.join(' ');
newAttrs[attrName] = deduplicatedAttrValues.join(' ');
});

return node;
});

return tree;
return newAttrs;
};
}
55 changes: 54 additions & 1 deletion lib/modules/example.es6
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
/**
* Example module
* It is an example htmlnano module.
*
* A htmlnano module can be modify the attributes of every node (through a "onAttrs" named export),
* modify the content of every node (through an optional "onContent" named export), modify the node
* itself (through an optional "onNode" named export), or modify the entire tree (through an optional
* default export).
*/

/**
* Modify attributes of node. Optional.
*
* @param {object} options - Options that were passed to htmlnano
* @param moduleOptions — Module options. For most modules this is just "true" (indication that the module was enabled)
* @return {Function} - Return a function that takes attribute object and the node (for the context), and returns the modified attribute object
*/
export function onAttrs(options, moduleOptions) {
return (attrs, node) => {
// You can modify "attrs" based on "node"
const newAttrs = { ...attrs };

return newAttrs; // ... then return the modified attrs
};
}

/**
* Modify content of node. Optional.
*
* @param {object} options - Options that were passed to htmlnano
* @param moduleOptions — Module options. For most modules this is just "true" (indication that the module was enabled)
* @return {Function} - Return a function that takes contents (an array of node and string) and the node (for the context), and returns the modified content array.
*/
export function onContent(options, moduleOptions) {
return (content, node) => {
// Same goes the "content"

return content; // ... return modified content here
};
}

/**
* It is possible to modify entire ndde as well. Optional.
* @param {object} options - Options that were passed to htmlnano
* @param moduleOptions — Module options. For most modules this is just "true" (indication that the module was enabled)
* @return {Function} - Return a function that takes the node, and returns the new, modified node.
*/
export function onNode(options, moduleOptions) {
return (node) => {
return node; // ... return new node here
};
}

/**
* Modify the entire tree. Optional.
*
* @param {object} tree - PostHTML tree (https://github.com/posthtml/posthtml/blob/master/README.md)
* @param {object} options - Options that were passed to htmlnano
* @param moduleOptions — Module options. For most modules this is just "true" (indication that the module was enabled)
* @return {object | Proimse} - Return the modified tree.
*/
export default function example(tree, options, moduleOptions) {
// Module filename (example.es6), exported default function name (example),
Expand Down
29 changes: 12 additions & 17 deletions lib/modules/minifyJson.es6
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
/* Minify JSON inside <script> tags */
export default function minifyJson(tree) {
// Match all <script> tags which have JSON mime type
tree.match({tag: 'script', attrs: {type: /(\/|\+)json/}}, node => {
let content = (node.content || []).join('');
if (! content) {
return node;
}
const rNodeAttrsTypeJson = /(\/|\+)json/;

try {
content = JSON.stringify(JSON.parse(content));
} catch (error) {
return node;
export function onContent() {
return (content, node) => {
let newContent = content;
if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
try {
newContent = JSON.stringify(JSON.parse((content || []).join('')));
} catch (error) {
// Invalid JSON
}
}

node.content = [content];
return node;
});

return tree;
return newContent;
};
}
25 changes: 10 additions & 15 deletions lib/modules/normalizeAttributeValues.es6
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,22 @@ const caseInsensitiveAttributes = {
wrap: ['textarea']
};

export default function normalizeAttributeValues(tree) {
tree.walk(node => {
if (!node.attrs) {
return node;
}
export function onAttrs() {
return (attrs, node) => {
const newAttrs = attrs;

Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
const attrNameLower = attrName.toLowerCase();
Object.entries(attrs).forEach(([attrName, attrValue]) => {
if (
Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower)
Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName)
&& (
caseInsensitiveAttributes[attrNameLower] === null
|| caseInsensitiveAttributes[attrNameLower].includes(node.tag)
caseInsensitiveAttributes[attrName] === null
|| caseInsensitiveAttributes[attrName].includes(node.tag)
)
) {
node.attrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
}
});

return node;
});

return tree;
return newAttrs;
};
}
Loading

0 comments on commit c4d7893

Please sign in to comment.