diff --git a/.gitignore b/.gitignore index 22bded3..33581c3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tmp/ *.log coverage/ bun.lockb +*.bak diff --git a/index.js b/index.js index 5294e56..95d17ff 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,13 @@ /* global hexo */ 'use strict' -hexo.config.minify = Object.assign({ +hexo.config.minify = { enable: true, - previewServer: true -}, hexo.config.minify) + previewServer: true, + ...hexo.config.minify +} -hexo.config.minify.html = Object.assign({ +hexo.config.minify.html = { enable: true, priority: 10, verbose: false, @@ -21,19 +22,21 @@ hexo.config.minify.html = Object.assign({ removeStyleLinkTypeAttributes: true, minifyJS: true, minifyCSS: true, - globOptions: { basename: true } -}, hexo.config.minify.html) + globOptions: { basename: true }, + ...hexo.config.minify.html +} -hexo.config.minify.css = Object.assign({ +hexo.config.minify.css = { enable: true, priority: 10, verbose: false, exclude: ['*.min.css'], level: 2, - globOptions: { basename: true } -}, hexo.config.minify.css) + globOptions: { basename: true }, + ...hexo.config.minify.css +} -hexo.config.minify.js = Object.assign({ +hexo.config.minify.js = { enable: true, priority: 10, verbose: false, @@ -41,68 +44,100 @@ hexo.config.minify.js = Object.assign({ compress: {}, mangle: true, output: {}, - globOptions: { basename: true } -}, hexo.config.minify.js) + globOptions: { basename: true }, + ...hexo.config.minify.js +} -hexo.config.minify.svg = Object.assign({ +hexo.config.minify.svg = { enable: true, priority: 10, verbose: false, include: ['*.svg', '!*.min.svg'], plugins: {}, - globOptions: { basename: true } -}, hexo.config.minify.svg) + globOptions: { basename: true }, + ...hexo.config.minify.svg +} -hexo.config.minify.gzip = Object.assign({ +hexo.config.minify.gzip = { enable: true, priority: 10, verbose: false, include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'], - globOptions: { basename: true } -}, hexo.config.minify.gzip) + globOptions: { basename: true }, + ...hexo.config.minify.gzip +} -hexo.config.minify.brotli = Object.assign({ +hexo.config.minify.brotli = { enable: true, priority: 10, verbose: false, include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'], - globOptions: { basename: true } -}, hexo.config.minify.brotli) + globOptions: { basename: true }, + ...hexo.config.minify.brotli +} -hexo.config.minify.zstd = Object.assign({ +hexo.config.minify.zstd = { enable: false, priority: 10, verbose: false, include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'], - globOptions: { basename: true } -}, hexo.config.minify.zstd) + globOptions: { basename: true }, + ...hexo.config.minify.zstd +} -hexo.config.minify.xml = Object.assign({ +hexo.config.minify.xml = { enable: false, priority: 10, verbose: false, include: ['*.xml', '!*.min.xml'], removeComments: true, - globOptions: { basename: true } -}, hexo.config.minify.xml) + globOptions: { basename: true }, + ...hexo.config.minify.xml +} -hexo.config.minify.json = Object.assign({ +hexo.config.minify.json = { enable: false, priority: 10, verbose: false, include: ['*.json', '!*.min.json'], - globOptions: { basename: true } -}, hexo.config.minify.json) + globOptions: { basename: true }, + ...hexo.config.minify.json +} if (hexo.config.minify.enable === true && !(hexo.config.minify.previewServer === true && ['s', 'server'].includes(hexo.env.cmd))) { - const filter = require('./lib/filter') - hexo.extend.filter.register('after_render:html', filter.minifyHtml, hexo.config.minify.html.priority) - hexo.extend.filter.register('after_render:css', filter.minifyCss, hexo.config.minify.css.priority) - hexo.extend.filter.register('after_render:js', filter.minifyJs, hexo.config.minify.js.priority) - hexo.extend.filter.register('after_generate', filter.minifySvg, hexo.config.minify.svg.priority) - hexo.extend.filter.register('after_generate', filter.gzipFn, hexo.config.minify.gzip.priority) - hexo.extend.filter.register('after_generate', filter.brotliFn, hexo.config.minify.brotli.priority) - hexo.extend.filter.register('after_generate', filter.zstdFn, hexo.config.minify.zstd.priority) - hexo.extend.filter.register('after_generate', filter.minifyXml, hexo.config.minify.xml.priority) - hexo.extend.filter.register('after_generate', filter.minifyJson, hexo.config.minify.json.priority) -} + if (hexo.config.minify.html.enable === true) { + hexo.extend.filter.register('after_render:html', require('./lib/html').minifyHtml, hexo.config.minify.html.priority) + } + if (hexo.config.minify.css.enable === true) { + hexo.extend.filter.register('after_render:css', require('./lib/css').minifyCss, hexo.config.minify.css.priority) + } + if (hexo.config.minify.js.enable === true) { + hexo.extend.filter.register('after_render:js', require('./lib/js').minifyJs, hexo.config.minify.js.priority) + } + if (hexo.config.minify.svg.enable === true) { + hexo.extend.filter.register('after_generate', require('./lib/svg').minifySvg, hexo.config.minify.svg.priority) + } + if (hexo.config.minify.gzip.enable || hexo.config.minify.brotli.enable) { + const zlib = require('./lib/zlib') + if (hexo.config.minify.gzip.enable === true) { + hexo.extend.filter.register('after_generate', zlib.gzipFn, hexo.config.minify.gzip.priority) + } + if (hexo.config.minify.brotli.enable === true) { + hexo.extend.filter.register('after_generate', zlib.brotliFn, hexo.config.minify.brotli.priority) + } + } + if (hexo.config.minify.zstd.enable === true) { + try { + hexo.extend.filter.register('after_generate', require('./lib/zstd').zstdFn, hexo.config.minify.zstd.priority) + } catch (ex) { + const log = hexo.log || console + log.warn(`ZSTD load failed. ${ex}`) + } + } + if (hexo.config.minify.xml.enable === true) { + hexo.extend.filter.register('after_generate', require('./lib/xml').minifyXml, hexo.config.minify.xml.priority) + } + if (hexo.config.minify.json.enable === true) { + hexo.extend.filter.register('after_generate', require('./lib/json').minifyJson, hexo.config.minify.json.priority) + } +} \ No newline at end of file diff --git a/lib/css.js b/lib/css.js new file mode 100644 index 0000000..1029c68 --- /dev/null +++ b/lib/css.js @@ -0,0 +1,30 @@ +'use strict' +const CleanCSS = require('clean-css') +const { isMatch, logFn } = require('./tools') + +/** + * @param {string} str + * @param {{ path: string }} data + */ +async function minifyCss(str, data) { + const hexo = this + const options = hexo.config.minify.css + if (!str) return str + + const { path } = data + const { exclude, globOptions, verbose } = options + + if (isMatch(path, exclude, globOptions)) return str + + try { + const { styles } = await new CleanCSS(options).minify(str) + if (verbose) logFn.call(this, str, styles, path, 'css') + return styles + } catch (err) { + throw new Error(`Path: ${path}\n${err}`) + } +} + +module.exports = { + minifyCss +} \ No newline at end of file diff --git a/lib/filter.js b/lib/filter.js index 0d17003..45b1c42 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -1,337 +1,11 @@ 'use strict' - -const { minify: htmlMinify } = require('html-minifier-terser') -const CleanCSS = require('clean-css') -const { minify: terserMinify } = require('terser') -const { optimize: svgOptimize } = require('svgo') -const zlib = require('zlib') -const { promisify } = require('util') -const gzip = promisify(zlib.gzip) -const br = promisify(zlib.brotliCompress) -const { minify: compressXml } = require('minify-xml') -const micromatch = require('micromatch') -const { compress: zstd } = require('@mongodb-js/zstd') - -const isMatch = (path = '', patterns = [], options = {}) => { - if (path && patterns) { - if (path.length && patterns.length) { - if (typeof patterns === 'string') patterns = [patterns] - for (const pattern of patterns) { - // disable basename if a pattern includes a slash - let { basename } = options - // only disable when basename is enabled - basename = basename && !pattern.includes('/') - if (micromatch.isMatch(path, pattern, { ...options, basename })) { - return true - } - } - } - } - return false -} - -const match = (paths = [], patterns = [], options = {}) => { - let input = paths - if (paths && patterns) { - if (paths.length && patterns.length) { - const output = [] - if (typeof patterns === 'string') patterns = [patterns] - const exclude = patterns.filter((pattern) => pattern.startsWith('!')) - const include = patterns.filter((pattern) => !pattern.startsWith('!')) - if (exclude.length) input = micromatch(paths, exclude, options) - if (include.length) { - for (const pattern of include) { - let { basename } = options - basename = basename && !pattern.includes('/') - const tmp = micromatch(input, pattern, { ...options, basename }) - if (tmp.length) output.push(...tmp) - } - return [...new Set(output)] - } - return input - } - } - return paths -} - -function logFn (original, minified, path, ext) { - const saved = ((original.length - minified.length) / original.length * 100).toFixed(2) - const log = this.log || console - log.log(`${ext}: ${path} [${saved}% saved]`) -} - -async function minifyHtml (str, data) { - const hexo = this - const options = hexo.config.minify.html - if (options.enable === false || !str) return - - const { path } = data - const { exclude, globOptions, verbose } = options - - // Return if a path matches exclusion pattern - if (isMatch(path, exclude, globOptions)) return str - - try { - const result = await htmlMinify(str, options) - if (verbose) logFn.call(this, str, result, path, 'html') - - return result - } catch (err) { - throw new Error(`Path: ${path}\n${err}`) - } -} - -async function minifyCss (str, data) { - const hexo = this - const options = hexo.config.minify.css - if (options.enable === false || !str) return - - const { path } = data - const { exclude, globOptions, verbose } = options - - if (isMatch(path, exclude, globOptions)) return str - - try { - const { styles } = await new CleanCSS(options).minify(str) - if (verbose) logFn.call(this, str, styles, path, 'css') - return styles - } catch (err) { - throw new Error(`Path: ${path}\n${err}`) - } -} - -async function minifyJs (str, data) { - const hexo = this - const options = hexo.config.minify.js - if (options.enable === false || !str) return - - const { path } = data - const { exclude, globOptions, verbose } = options - - if (isMatch(path, exclude, globOptions)) return str - - // Terser doesn't like unsupported options - const jsOptions = Object.assign({}, options) - delete jsOptions.enable - delete jsOptions.priority - delete jsOptions.verbose - // Old option, retained to avoid crash when upgrading to v4 - delete jsOptions.logger - delete jsOptions.exclude - delete jsOptions.globOptions - - try { - const { code } = await terserMinify(str, jsOptions) - if (verbose) logFn.call(this, str, code, path, 'js') - return code - } catch (err) { - throw new Error(`Path: ${path}\n${err}`) - } -} - -function minifySvg () { - const hexo = this - const options = hexo.config.minify.svg - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - // const plugins = Array.isArray(options.plugins) ? extendDefaultPlugins(options.plugins) : extendDefaultPlugins([]) - const pluginCfg = Object.prototype.toString.call(options.plugins) === '[object Object]' ? { ...options.plugins } : {} - const plugins = [{ - name: 'preset-default', - params: { - overrides: pluginCfg - } - }] - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', async () => { - if (assetTxt.length) { - try { - const { data } = svgOptimize(assetTxt, { ...options, plugins }) - if (verbose) logFn.call(this, assetTxt, data, path, 'svg') - resolve(route.set(path, data)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - -function gzipFn () { - const hexo = this - const options = hexo.config.minify.gzip - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - let { level } = options - if (typeof level !== 'number') level = zlib.constants.Z_BEST_COMPRESSION - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', async () => { - if (assetTxt.length) { - try { - const result = await gzip(assetTxt, { level }) - if (verbose) logFn.call(this, assetTxt, result, path, 'gzip') - resolve(route.set(path + '.gz', result)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - -function brotliFn () { - const hexo = this - const options = hexo.config.minify.brotli - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - let { level } = options - if (typeof level !== 'number') level = zlib.constants.BROTLI_MAX_QUALITY - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', async () => { - if (assetTxt.length) { - try { - const result = await br(assetTxt, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } }) - if (verbose) logFn.call(this, assetTxt, result, path, 'brotli') - resolve(route.set(path + '.br', result)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - -function zstdFn () { - const hexo = this - const options = hexo.config.minify.zstd - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - let { level } = options - if (typeof level !== 'number') level = undefined - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', async () => { - if (assetTxt.length) { - try { - const input = Buffer.from(assetTxt, 'utf-8') - const result = await zstd(input, level) - if (verbose) logFn.call(this, assetTxt, result, path, 'zstd') - resolve(route.set(path + '.zst', result)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - -function minifyXml () { - const hexo = this - const options = hexo.config.minify.xml - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', () => { - if (assetTxt.length) { - try { - const result = compressXml(assetTxt, { ...options }) - if (verbose) logFn.call(this, assetTxt, result, path, 'xml') - resolve(route.set(path, result)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - -function minifyJson () { - const hexo = this - const options = hexo.config.minify.json - if (options.enable === false) return - - const { route } = hexo - const routeList = route.list() - const { globOptions, include, verbose } = options - - return Promise.all((match(routeList, include, globOptions)).map((path) => { - return new Promise((resolve, reject) => { - const assetPath = route.get(path) - let assetTxt = '' - assetPath.on('data', (chunk) => (assetTxt += chunk)) - assetPath.on('end', () => { - if (assetTxt.length) { - try { - const result = JSON.stringify(JSON.parse(assetTxt)) - if (verbose) logFn.call(this, assetTxt, result, path, 'json') - resolve(route.set(path, result)) - } catch (err) { - reject(new Error(`Path: ${path}\n${err}`)) - } - } - resolve() - }) - }) - })) -} - module.exports = { - minifyHtml, - minifyCss, - minifyJs, - minifySvg, - gzipFn, - brotliFn, - zstdFn, - minifyXml, - minifyJson -} + ... require('./html'), + ... require('./css'), + ... require('./js'), + ... require('./svg'), + ... require('./zlib'), + ... require('./zstd'), + ... require('./xml'), + ... require('./json'), +} \ No newline at end of file diff --git a/lib/html.js b/lib/html.js new file mode 100644 index 0000000..7ff5e12 --- /dev/null +++ b/lib/html.js @@ -0,0 +1,31 @@ +'use strict' +const { minify: htmlMinify } = require('html-minifier-terser') +const { isMatch, logFn } = require('./tools') + +/** + * @param {string} str + * @param {{ path: string }} data + */ +async function minifyHtml(str, data) { + const hexo = this + const options = hexo.config.minify.html + if (!str) return str + + const { path } = data + const { exclude, globOptions, verbose } = options + + // Return if a path matches exclusion pattern + if (isMatch(path, exclude, globOptions)) return str + + try { + const result = await htmlMinify(str, options) + if (verbose) logFn.call(this, str, result, path, 'html') + return result + } catch (err) { + throw new Error(`Path: ${path}\n${err}`) + } +} + +module.exports = { + minifyHtml +} \ No newline at end of file diff --git a/lib/js.js b/lib/js.js new file mode 100644 index 0000000..6947b56 --- /dev/null +++ b/lib/js.js @@ -0,0 +1,40 @@ +'use strict' +const { minify: terserMinify } = require('terser') +const { isMatch, logFn } = require('./tools') + +/** + * @param {string} str + * @param {{ path: string }} data + */ +async function minifyJs(str, data) { + const hexo = this + const options = hexo.config.minify.js + if (!str) return str + + const { path } = data + const { exclude, globOptions, verbose } = options + + if (isMatch(path, exclude, globOptions)) return str + + // Terser doesn't like unsupported options + const jsOptions = { ...options } + delete jsOptions.enable + delete jsOptions.priority + delete jsOptions.verbose + // Old option, retained to avoid crash when upgrading to v4 + delete jsOptions.logger + delete jsOptions.exclude + delete jsOptions.globOptions + + try { + const { code } = await terserMinify(str, jsOptions) + if (verbose) logFn.call(this, str, code, path, 'js') + return code + } catch (err) { + throw new Error(`Path: ${path}\n${err}`) + } +} + +module.exports = { + minifyJs +} \ No newline at end of file diff --git a/lib/json.js b/lib/json.js new file mode 100644 index 0000000..3464165 --- /dev/null +++ b/lib/json.js @@ -0,0 +1,36 @@ +'use strict' +const { match, logFn } = require('./tools') + +function minifyJson() { + const hexo = this + const options = hexo.config.minify.json + + const { route } = hexo + /** @type {string[]} */ + const routeList = route.list() + const { globOptions, include, verbose } = options + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', () => { + if (assetTxt.length) { + try { + const result = JSON.stringify(JSON.parse(assetTxt)) + if (verbose) logFn.call(this, assetTxt, result, path, 'json') + resolve(route.set(path, result)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +module.exports = { + minifyJson +} \ No newline at end of file diff --git a/lib/svg.js b/lib/svg.js new file mode 100644 index 0000000..59d7814 --- /dev/null +++ b/lib/svg.js @@ -0,0 +1,45 @@ +'use strict' +const { optimize: svgOptimize } = require('svgo') +const { match, logFn } = require('./tools') + +function minifySvg() { + const hexo = this + const options = hexo.config.minify.svg + + const { route } = hexo + /** @type {string[]} */ + const routeList = route.list() + const { globOptions, include, verbose } = options + // const plugins = Array.isArray(options.plugins) ? extendDefaultPlugins(options.plugins) : extendDefaultPlugins([]) + const pluginCfg = Object.prototype.toString.call(options.plugins) === '[object Object]' ? { ...options.plugins } : {} + const plugins = [{ + name: 'preset-default', + params: { + overrides: pluginCfg + } + }] + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', async () => { + if (assetTxt.length) { + try { + const { data } = svgOptimize(assetTxt, { ...options, plugins }) + if (verbose) logFn.call(this, assetTxt, data, path, 'svg') + resolve(route.set(path, data)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +module.exports = { + minifySvg +} \ No newline at end of file diff --git a/lib/tools.js b/lib/tools.js new file mode 100644 index 0000000..d156c97 --- /dev/null +++ b/lib/tools.js @@ -0,0 +1,72 @@ +'use strict' +const micromatch = require('micromatch') + +/** + * @param {string | string[]} patterns + * @param {{ basename: string }} options + */ +function isMatch(path = '', patterns = [], options = {}) { + if (path && patterns) { + if (path.length && patterns.length) { + if (typeof patterns === 'string') patterns = [patterns] + for (const pattern of patterns) { + // disable basename if a pattern includes a slash + let { basename } = options + // only disable when basename is enabled + basename = basename && !pattern.includes('/') + if (micromatch.isMatch(path, pattern, { ...options, basename })) { + return true + } + } + } + } + return false +} + +/** + * @param {string[]} paths + * @param {string | string[]} patterns + * @param {{ basename: string }} options + */ +function match(paths = [], patterns = [], options = {}) { + let input = paths + if (paths && patterns) { + if (paths.length && patterns.length) { + /** @type {string[]} */ + const output = [] + if (typeof patterns === 'string') patterns = [patterns] + const exclude = patterns.filter(pattern => pattern.startsWith('!')) + const include = patterns.filter(pattern => !pattern.startsWith('!')) + if (exclude.length) input = micromatch(paths, exclude, options) + if (include.length) { + for (const pattern of include) { + let { basename } = options + basename = basename && !pattern.includes('/') + const tmp = micromatch(input, pattern, { ...options, basename }) + if (tmp.length) output.push(...tmp) + } + return [...new Set(output)] + } + return input + } + } + return paths +} + +/** + * @param {string} original + * @param {string} ext + * @param {string} minified + * @param {string} path + */ +function logFn(original, minified, path, ext) { + const saved = ((original.length - minified.length) / original.length * 100).toFixed(2) + const log = this.log || console + log.log(`${ext}: ${path} [${saved}% saved]`) +} + +module.exports = { + isMatch, + match, + logFn +} \ No newline at end of file diff --git a/lib/xml.js b/lib/xml.js new file mode 100644 index 0000000..3a504fc --- /dev/null +++ b/lib/xml.js @@ -0,0 +1,37 @@ +'use strict' +const { minify: compressXml } = require('minify-xml') +const { match, logFn } = require('./tools') + +function minifyXml() { + const hexo = this + const options = hexo.config.minify.xml + + const { route } = hexo + /** @type {string[]} */ + const routeList = route.list() + const { globOptions, include, verbose } = options + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', () => { + if (assetTxt.length) { + try { + const result = compressXml(assetTxt, { ...options }) + if (verbose) logFn.call(this, assetTxt, result, path, 'xml') + resolve(route.set(path, result)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +module.exports = { + minifyXml +} \ No newline at end of file diff --git a/lib/zlib.js b/lib/zlib.js new file mode 100644 index 0000000..6f5b510 --- /dev/null +++ b/lib/zlib.js @@ -0,0 +1,74 @@ +'use strict' +const zlib = require('zlib') +const { promisify } = require('util') +const gzip = promisify(zlib.gzip) +const br = promisify(zlib.brotliCompress) +const { match, logFn } = require('./tools') + +function gzipFn() { + const hexo = this + const options = hexo.config.minify.gzip + + const { route } = hexo + /** @type {string} */ + const routeList = route.list() + const { globOptions, include, verbose } = options + let { level } = options + if (typeof level !== 'number') level = zlib.constants.Z_BEST_COMPRESSION + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', async () => { + if (assetTxt.length) { + try { + const result = await gzip(assetTxt, { level }) + if (verbose) logFn.call(this, assetTxt, result, path, 'gzip') + resolve(route.set(path + '.gz', result)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +function brotliFn() { + const hexo = this + const options = hexo.config.minify.brotli + + const { route } = hexo + const routeList = route.list() + const { globOptions, include, verbose } = options + let { level } = options + if (typeof level !== 'number') level = zlib.constants.BROTLI_MAX_QUALITY + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', async () => { + if (assetTxt.length) { + try { + const result = await br(assetTxt, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: level } }) + if (verbose) logFn.call(this, assetTxt, result, path, 'brotli') + resolve(route.set(path + '.br', result)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +module.exports = { + gzipFn, + brotliFn +} \ No newline at end of file diff --git a/lib/zstd.js b/lib/zstd.js new file mode 100644 index 0000000..f5fe4f1 --- /dev/null +++ b/lib/zstd.js @@ -0,0 +1,40 @@ +'use strict' +const { compress: zstd } = require('@mongodb-js/zstd') +const { match, logFn } = require('./tools') + +function zstdFn() { + const hexo = this + const options = hexo.config.minify.zstd + + const { route } = hexo + /** @type {string[]} */ + const routeList = route.list() + const { globOptions, include, verbose } = options + let { level } = options + if (typeof level !== 'number') level = undefined + + return Promise.all((match(routeList, include, globOptions)).map(path => { + return new Promise((/** @type {(value: string) => void} */ resolve, reject) => { + const assetPath = route.get(path) + let assetTxt = '' + assetPath.on('data', chunk => (assetTxt += chunk)) + assetPath.on('end', async () => { + if (assetTxt.length) { + try { + const input = Buffer.from(assetTxt, 'utf-8') + const result = await zstd(input, level) + if (verbose) logFn.call(this, assetTxt, result, path, 'zstd') + resolve(route.set(path + '.zst', result)) + } catch (err) { + reject(new Error(`Path: ${path}\n${err}`)) + } + } + resolve(assetTxt) + }) + }) + })) +} + +module.exports = { + zstdFn +} \ No newline at end of file