Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ tmp/
.idea/
.nyc_output/
coverage/
package-lock.json
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,25 @@ You can set options of HTMLMinifier in the main `_config.yml` file:

``` yaml
html_minifier:
exclude:
exclude:
```

- **exclude**: Exclude files from being minified. Support [globbing patterns](https://github.com/micromatch/micromatch#extended-globbing).

Default options:

``` yaml
html_minifier:
html_minifier:
collapseBooleanAttributes: true
collapseWhitespace: true
# Ignore '<!-- more -->' https://hexo.io/docs/tag-plugins#Post-Excerpt
ignoreCustomComments: [ !!js/regexp /^\s*more/]
removeComments: true
removeEmptyAttributes: true
removeScriptTypeAttributes: true
removeStyleLinkTypeAttributes: true
minifyJS: true
minifyCSS: true
removeRedundantAttributes: true
removeAttributeQuotes: true
minifyJs: true
minifyCss: true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO:

We should provide a migration guide since the configuration has changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should provide a migration guide since the configuration has changed.

```

- **ignoreCustomComments**: Array of regex'es that allow to ignore certain comments, when matched. Need to prepend [`!!js/regexp`](https://github.com/nodeca/js-yaml#supported-yaml-types) to support regex.
Expand Down
9 changes: 4 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ hexo.config.html_minifier = Object.assign({
exclude: [],
collapseBooleanAttributes: true,
collapseWhitespace: true,
// Ignore '<!-- more -->' https://hexo.io/docs/tag-plugins#Post-Excerpt
ignoreCustomComments: [/^\s*more/],
removeComments: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
minifyCSS: true
removeRedundantAttributes: true,
removeAttributeQuotes: true,
minifyJs: true,
minifyCss: true
}, hexo.config.html_minifier);

hexo.extend.filter.register('after_render:html', require('./lib/filter'));
28 changes: 23 additions & 5 deletions lib/filter.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
'use strict';
const minify = require('html-minifier').minify;
const htmlnano = require('htmlnano');
const micromatch = require('micromatch');

module.exports = function(str, data) {
const options = this.config.html_minifier;
module.exports = async function(str, data) {
const options = { ...this.config.html_minifier };
const path = data.path;
const exclude = options.exclude;
const { exclude, ignoreCustomComments, removeComments } = options;

if (path && exclude && exclude.length) {
if (micromatch.isMatch(path, exclude)) return str;
}

delete options.exclude;
delete options.ignoreCustomComments;
delete options.removeComments;
Comment on lines +14 to +16
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating the config options object is dangerous. Since options is a reference to this.config.html_minifier, these deletions will permanently modify the configuration for subsequent calls. This will cause issues if the filter is invoked multiple times. Create a shallow copy of options before mutation: const options = { ...this.config.html_minifier };

Copilot uses AI. Check for mistakes.
if (removeComments) {
if (Array.isArray(ignoreCustomComments) && ignoreCustomComments.length) {
options.removeComments = comment => {
comment = comment.replace(/^<!--|-->$/g, '');
for (const regex of ignoreCustomComments) {
if (regex.test(comment)) return false;
}
return true;
};
} else {
options.removeComments = 'safe';
}
}

try {
return minify(str, options);
const result = await htmlnano.process(str, options);
return result.html;
} catch (err) {
throw new Error(`Path: ${path}\n${err}`);
}
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hexo-html-minifier",
"version": "1.0.0",
"description": "Minify HTML files with HTMLMinifier.",
"description": "Minify HTML files with htmlnano.",
"main": "index.js",
"scripts": {
"eslint": "eslint .",
Expand Down Expand Up @@ -31,8 +31,12 @@
],
"license": "MIT",
"dependencies": {
"html-minifier": "^4.0.0",
"micromatch": "^4.0.4"
"cssnano": "^7.1.1",
"htmlnano": "^2.1.5",
"micromatch": "^4.0.4",
"postcss": "^8.5.6",
"svgo": "^3.3.2",
"terser": "^5.44.0"
},
"devDependencies": {
"chai": "^6.0.1",
Expand Down
54 changes: 25 additions & 29 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,61 @@
'use strict';

const should = require('chai').should(); // eslint-disable-line
const { minify } = require('html-minifier');

describe('hexo-html-minifier', () => {
const ctx = {
config: {
html_minifier: {
exclude: [],
collapseBooleanAttributes: true,
collapseWhitespace: true,
ignoreCustomComments: [/^\s*more/],
removeComments: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
minifyCSS: true
removeRedundantAttributes: true,
removeAttributeQuotes: true,
minifyJs: true,
minifyCss: true
}
}
};
const h = require('../lib/filter').bind(ctx);
const defaultCfg = JSON.parse(JSON.stringify(ctx.config));
const defaultCfg = {...ctx.config.html_minifier};
const input = '<p id="">foo</p>';
const path = 'index.html';

beforeEach(() => {
ctx.config = JSON.parse(JSON.stringify(defaultCfg));
ctx.config.html_minifier = {...defaultCfg};
});

it('default', () => {
const result = h(input, { path });
it('default', async () => {
const result = await h(input, { path });
result.should.eql('<p>foo</p>');
});

it('option', () => {
it('option', async () => {
ctx.config.html_minifier.removeEmptyAttributes = false;
const result = h(input, { path });
result.should.eql(input);
const result = await h(input, { path });
// htmlnano still removes the empty attribute value, leaving just the attribute name
result.should.eql('<p id>foo</p>');
});

it('exclude', () => {
it('exclude', async () => {
ctx.config.html_minifier.exclude = '**/*.min.html';
const result = h(input, { path: 'foo/bar.min.html' });
const result = await h(input, { path: 'foo/bar.min.html' });
result.should.eql(input);
});

it('invalid input', () => {
it('invalid input', async () => {
// htmlnano handles malformed HTML gracefully without throwing errors
const invalid = '<html><>?:"{}|_+</html>';
let expected;

try {
minify(invalid);
} catch (err) {
expected = err;
}
const result = await h(invalid, { path });
// htmlnano processes the content as-is
result.should.eql(invalid);
});

try {
h(invalid, { path });
should.fail();
} catch (err) {
err.message.should.eql(`Path: ${path}\n${expected}`);
}
it('ignoreCustomComments', async () => {
const content = '<!-- more -->\n<p>Content</p>';
const result = await h(content, { path });
result.should.include('<!-- more -->');
});
});