feat: minify xml

This commit is contained in:
curbengh 2020-01-03 00:35:25 +00:00
parent d75b10b64b
commit 317b5e43d3
No known key found for this signature in database
GPG Key ID: 21EA847C35D6E034
4 changed files with 258 additions and 3 deletions

View File

@ -21,6 +21,7 @@ Yet Another Minifier for Hexo. Minify and compress HTML, JS, CSS and SVG. XML, J
- [SVG](#svg) - [SVG](#svg)
- [Gzip](#gzip) - [Gzip](#gzip)
- [Brotli](#brotli) - [Brotli](#brotli)
- [XML](#xml)
- [Globbing](#globbing) - [Globbing](#globbing)
- [HTTP Compression](#http-compression) - [HTTP Compression](#http-compression)
@ -191,6 +192,24 @@ minify:
- **globOptions** - See [globbing](#globbing) section. - **globOptions** - See [globbing](#globbing) section.
- **level** - Compression level. Range `1-11`. Defaults to `11`, or the value of [`zlib.constants.BROTLI_MAX_QUALITY`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_brotli_constants) - **level** - Compression level. Range `1-11`. Defaults to `11`, or the value of [`zlib.constants.BROTLI_MAX_QUALITY`](https://nodejs.org/docs/latest-v12.x/api/zlib.html#zlib_brotli_constants)
## XML
``` yaml
minify:
xml:
enable: false
include:
- '*.xml'
- '!*.min.xml'
```
- **enable** - Enable the plugin. Defaults to `false`.
- **priority** - Plugin's priority. Defaults to `10`.
- **verbose** - Verbose output. Defaults to `false`.
- **include** - Include files. Support [wildcard](http://www.globtester.com/) pattern(s) in a string or array.
- Exclude `*.min.xml` by default.
- **removeComments** - Remove [comments](https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction) in xml. Defaults to `true`.
- **globOptions** - See [globbing](#globbing) section.
## Globbing ## Globbing
Use "globOptions" to customise how glob patterns match files. Refer to [micromatch](https://github.com/micromatch/micromatch#options) for available options. Use "globOptions" to customise how glob patterns match files. Refer to [micromatch](https://github.com/micromatch/micromatch#options) for available options.

View File

@ -59,6 +59,14 @@ const brotliDefault = {
include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'], include: ['*.html', '*.css', '*.js', '*.txt', '*.ttf', '*.atom', '*.stl', '*.xml', '*.svg', '*.eot', '*.json'],
globOptions: { basename: true } globOptions: { basename: true }
} }
const xmlDefault = {
enable: false,
priority: 10,
verbose: false,
include: ['*.xml', '!*.min.xml'],
removeComments: true,
globOptions: { basename: true }
}
hexo.config.minify = Object.assign(minifyDefault, hexo.config.minify) hexo.config.minify = Object.assign(minifyDefault, hexo.config.minify)
hexo.config.minify.html = Object.assign(htmlDefault, hexo.config.minify.html) hexo.config.minify.html = Object.assign(htmlDefault, hexo.config.minify.html)
@ -67,6 +75,7 @@ hexo.config.minify.js = Object.assign(jsDefault, hexo.config.minify.js)
hexo.config.minify.svg = Object.assign(svgDefault, hexo.config.minify.svg) hexo.config.minify.svg = Object.assign(svgDefault, hexo.config.minify.svg)
hexo.config.minify.gzip = Object.assign(gzipDefault, hexo.config.minify.gzip) hexo.config.minify.gzip = Object.assign(gzipDefault, hexo.config.minify.gzip)
hexo.config.minify.brotli = Object.assign(brotliDefault, hexo.config.minify.brotli) hexo.config.minify.brotli = Object.assign(brotliDefault, hexo.config.minify.brotli)
hexo.config.minify.xml = Object.assign(xmlDefault, hexo.config.minify.xml)
if (hexo.config.minify.enable === true) { if (hexo.config.minify.enable === true) {
const filter = require('./lib/filter') const filter = require('./lib/filter')
@ -76,6 +85,7 @@ if (hexo.config.minify.enable === true) {
hexo.extend.filter.register('after_generate', filter.minifySvg, hexo.config.minify.svg.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.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.brotliFn, hexo.config.minify.brotli.priority)
hexo.extend.filter.register('after_generate', filter.minifyXml, hexo.config.minify.xml.priority)
} }
module.exports = { module.exports = {
@ -85,5 +95,6 @@ module.exports = {
jsDefault, jsDefault,
svgDefault, svgDefault,
gzipDefault, gzipDefault,
brotliDefault brotliDefault,
xmlDefault
} }

View File

@ -215,11 +215,50 @@ function brotliFn () {
})) }))
} }
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, removeComments, 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 {
/* !
* Regex patterns are adapted from pretty-data 0.50.0
* Licensed MIT (c) 2012-2017 Vadim Kiryukhin ( vkiryukhin @ gmail.com )
* https://github.com/vkiryukhin/pretty-data
*/
const text = removeComments
? assetTxt.replace(/<![ \r\n\t]*(--([^-]|[\r\n]|-[^-])*--[ \r\n\t]*)>/g, '')
: assetTxt
const result = text.replace(/>\s{0,}</g, '><')
if (verbose) logFn.call(this, assetTxt, result, path, 'xml')
resolve(route.set(path, result))
} catch (err) {
reject(new Error(err))
}
}
resolve()
})
})
}))
}
module.exports = { module.exports = {
minifyHtml, minifyHtml,
minifyCss, minifyCss,
minifyJs, minifyJs,
minifySvg, minifySvg,
gzipFn, gzipFn,
brotliFn brotliFn,
minifyXml
} }

View File

@ -793,7 +793,6 @@ describe('brotli', () => {
expect(routeList).not.toContain(path.concat('.br')) expect(routeList).not.toContain(path.concat('.br'))
const result = await b() const result = await b()
// empty file resolves to an array of undefined
expect(result).toBeDefined() expect(result).toBeDefined()
expect(result[0]).toBeUndefined() expect(result[0]).toBeUndefined()
}) })
@ -966,3 +965,190 @@ describe('brotli', () => {
expect(routeList).toEqual(expect.arrayContaining(expected)) expect(routeList).toEqual(expect.arrayContaining(expected))
}) })
}) })
describe('xml', () => {
const { xmlDefault } = require('../index')
const x = require('../lib/filter').minifyXml.bind(hexo)
const path = 'foo.xml'
const input = '<?xml version="1.0" encoding="utf-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom">\n <!-- foo bar -->\n <title>foo</title>\n</feed>'
const expected = '<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>foo</title></feed>'
beforeEach(() => {
hexo.config.minify.xml = Object.assign({}, xmlDefault)
// plugin is disabled by default
hexo.config.minify.xml.enable = true
hexo.route.set(path, input)
})
afterEach(() => {
const routeList = hexo.route.list()
routeList.forEach((path) => hexo.route.remove(path))
})
test('default', async () => {
await x()
const output = hexo.route.get(path)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(expected)
})
})
test('disable', async () => {
hexo.config.minify.xml.enable = false
const result = await x()
expect(result).toBeUndefined()
})
test('option - removeComments', async () => {
hexo.config.minify.xml.removeComments = false
await x()
const output = hexo.route.get(path)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toContain('<!-- foo bar -->')
})
})
test('option - verbose', async () => {
hexo.config.minify.xml.verbose = true
hexo.log.log = jest.fn()
await x()
expect(hexo.log.log.mock.calls[0][0]).toContain(`xml: ${path}`)
})
test('empty file', async () => {
hexo.route.set(path, '')
const result = await x()
expect(result).toBeDefined()
expect(result[0]).toBeUndefined()
})
test('include - exclude *.min.xml by default', async () => {
const path = 'foo.min.xml'
hexo.route.set(path, input)
await x()
const output = hexo.route.get(path)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(input)
})
})
test('include - basename', async () => {
hexo.config.minify.xml.include = 'bar.xml'
const path = 'foo/bar.xml'
hexo.route.set(path, input)
await x()
const output = hexo.route.get(path)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(expected)
})
})
test('include - slash in pattern', async () => {
hexo.config.minify.xml.include = '**/lectus/**/*.xml'
const path = 'eleifend/lectus/nullam/dapibus/netus.xml'
hexo.route.set(path, input)
await x()
const output = hexo.route.get(path)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(expected)
})
})
test('include - basename + slash', async () => {
hexo.route.remove(path)
const paths = [
'lorem/ipsum/dolor.xml',
'gravida/sociis/erat/ante.xml',
'aptent/elementum.xml',
'felis/blandit/cursus.xml'
]
hexo.config.minify.xml.include = [
'dolor.xml',
'**/sociis/**/*.xml'
]
paths.forEach((inpath) => {
hexo.route.set(inpath, input)
})
await x()
const minPaths = paths.slice(0, 2)
const unminPaths = paths.slice(2)
minPaths.forEach((inpath) => {
const output = hexo.route.get(inpath)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(expected)
})
})
unminPaths.forEach((inpath) => {
const output = hexo.route.get(inpath)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(input)
})
})
})
test('include - reverse pattern + basename disabled', async () => {
hexo.route.remove(path)
const paths = [
'lorem/ipsum/dolor.xml',
'gravida/sociis/erat/ante.xml',
'aptent/elementum.xml',
'felis/blandit/cursus.xml'
]
hexo.config.minify.xml.include = [
'!dolor.xml'
]
hexo.config.minify.xml.globOptions = {
basename: false
}
paths.forEach((inpath) => {
hexo.route.set(inpath, input)
})
await x()
paths.forEach((inpath) => {
const output = hexo.route.get(inpath)
let result = ''
output.on('data', (chunk) => (result += chunk))
output.on('end', () => {
expect(result).toBe(expected)
})
})
})
test('include - empty route', async () => {
hexo.route.remove(path)
const result = await x()
expect(result.length).toBe(0)
})
})