Skip to content

Commit

Permalink
Refactor: rewrite most of the module into "once" module
Browse files Browse the repository at this point in the history
  • Loading branch information
SukkaW committed Dec 9, 2020
1 parent 2da8104 commit db5967f
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 139 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
lib/modules/example.es6
lib/modules/example_once.es6
test.js
31 changes: 26 additions & 5 deletions lib/htmlnano.es6
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import maxPreset from './presets/max';

function htmlnano(options = {}, preset = safePreset) {
return function minifier(tree) {
const nodeHandlers = [];
const attrsHandlers = [];
const contentsHandlers = [];

Expand All @@ -32,20 +33,40 @@ function htmlnano(options = {}, preset = safePreset) {
if (module.oncontent) {
contentsHandlers.push(module.oncontent(options, moduleOptions));
}
if (module.onnode) {
nodeHandlers.push(module.onnode(options, moduleOptions));
}
} else {
// It is a traditional htmlnano module
promise = promise.then(tree => module.default(tree, options, moduleOptions));
}
}

if (attrsHandlers.length + contentsHandlers.length > 0) {
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length > 0) {
promise = promise.then(tree => {
tree.walk(node => {
for (const handler of attrsHandlers) {
node.attrs = handler(node.attrs, 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;
}
for (const handler of contentsHandlers) {
node.content = handler(node.content, node);

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;
Expand Down
29 changes: 14 additions & 15 deletions lib/modules/collapseAttributeWhitespace.es6
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@ export const attributesWithLists = new Set([
'ping',
]);

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

Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
const attrNameLower = attrName.toLowerCase();
if (! attributesWithLists.has(attrNameLower)) {
function onattrs() {
return (attrs) => {
Object.entries(attrs).forEach(([attrName, attrValue]) => {
if (! attributesWithLists.has(attrName)) {
return;
}

const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
node.attrs[attrName] = newAttrValue;
attrs[attrName] = attrValue.replace(/\s+/g, ' ').trim();
});

return node;
});

return tree;
return attrs;
};
}

/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
export {
once, // It is a "once" module
onattrs
};
34 changes: 17 additions & 17 deletions lib/modules/collapseBooleanAttributes.es6
Original file line number Diff line number Diff line change
Expand Up @@ -102,43 +102,43 @@ const amphtmlBooleanAttributes = new Set([
'subscriptions-dialog'
]);

const once = true;

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

function onattrs(options, moduleOptions) {
return (attrs, node) => {
if (! node.tag) {
return node;
return 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;
attrs[attrName] = true;
}
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && node.attrs[attrName] === '') {
node.attrs[attrName] = true;
attrs[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;
attrs[attrName] = true;
}
}

return node;
});

return tree;
return attrs;
};
}

export {
once, // It is a "once" module
onattrs
};
30 changes: 15 additions & 15 deletions lib/modules/deduplicateAttributeValues.es6
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
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)) {
const once = true;

function onattrs() {
return (attrs, node) => {
Object.keys(attrs).forEach(attrName => {
if (! attributesWithLists.has(attrName)) {
return;
}

Expand All @@ -31,11 +27,15 @@ export default function collapseAttributeWhitespace(tree) {
uniqeAttrValues.add(attrValue);
});

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

return node;
});

return tree;
return attrs;
};
}

/** Deduplicate values inside list-like attributes (e.g. class, rel) */
export {
once,
onattrs
};
53 changes: 53 additions & 0 deletions lib/modules/example_once.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Example "once" module
*/

// It is a "once" module
const once = true;

/**
* 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)
*/
function onattrs(options, moduleOptions) {
return (attrs, node) => {
// You can modify "attrs" based on "node"

return attrs; // ... 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)
*/
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)
*/
function onnode(options, moduleOptions) {
return (node) => {
return node; // ... return new node here
};
}

export {
// This marks the module is a "once" module
once,
onattrs,
oncontent,
onnode
}
36 changes: 19 additions & 17 deletions lib/modules/minifyJson.es6
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/* 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;
}
const once = true;

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

return tree;
return content;
};
}

/* Minify JSON inside <script> tags */
export {
once,
oncontent
};
65 changes: 32 additions & 33 deletions lib/modules/minifyUrls.es6
Original file line number Diff line number Diff line change
Expand Up @@ -92,53 +92,49 @@ const isLinkRelCanonical = ({ tag, attrs }) => {
let relateUrlInstance;
let STORED_URL_BASE;

/** Convert absolute url into relative url */
export default function minifyUrls(tree, options, moduleOptions) {
const urlBase = processModuleOptions(moduleOptions);
const once = true;

// Invalid configuration, return tree directly
if (!urlBase) return tree;

/** Bring up a reusable RelateUrl instances (only once)
*
* STORED_URL_BASE is used to invalidate RelateUrl instances,
* avoiding require.cache acrossing multiple htmlnano instance with different configuration,
* e.g. unit tests cases.
*/
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
relateUrlInstance = new RelateUrl(urlBase);
STORED_URL_BASE = urlBase;
}
function onattrs(options, moduleOptions) {
const urlBase = processModuleOptions(moduleOptions);

tree.walk(node => {
if (!node.attrs) return node;
return (attrs, node) => {
// Invalid configuration, return directly
if (!urlBase) return attrs;

if (!node.tag) return node;
/** Bring up a reusable RelateUrl instances (only once)
*
* STORED_URL_BASE is used to invalidate RelateUrl instances,
* avoiding require.cache acrossing multiple htmlnano instance with different configuration,
* e.g. unit tests cases.
*/
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
relateUrlInstance = new RelateUrl(urlBase);
STORED_URL_BASE = urlBase;
}

if (!tagsHaveUriValuesForAttributes.has(node.tag)) return node;
if (!node.tag) return attrs;
if (!tagsHaveUriValuesForAttributes.has(node.tag)) return attrs;

// Prevent link[rel=canonical] being processed
// Can't be excluded by isUriTypeAttribute()
if (isLinkRelCanonical(node)) return node;
if (isLinkRelCanonical(node)) return attrs;

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

if (isUriTypeAttribute(node.tag, attrNameLower)) {
for (const [attrName, attrValue] of Object.entries(attrs)) {
if (isUriTypeAttribute(node.tag, attrName)) {
// FIXME!
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
// the WHATWG URL API is very strict while attrValue might not be a valid URL
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
attrs[attrName] = relateUrlInstance.relate(attrValue);

continue;
}

if (isSrcsetAttribute(node.tag, attrNameLower)) {
if (isSrcsetAttribute(node.tag, attrName)) {
try {
const parsedSrcset = srcset.parse(attrValue);

node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
srcset.url = relateUrlInstance.relate(srcset.url);

return srcset;
Expand All @@ -147,13 +143,16 @@ export default function minifyUrls(tree, options, moduleOptions) {
// srcset will throw an Error for invalid srcset.
}


continue;
}
}

return node;
});

return tree;
return attrs;
};
}

/** Convert absolute url into relative url */
export {
once,
onattrs
};
Loading

0 comments on commit db5967f

Please sign in to comment.