feat(xml): new lib to parse/format XML

This commit is contained in:
Julien Fontanet
2024-04-24 15:28:22 +02:00
committed by Pierre Donias
parent db3c51d55e
commit 798049c20e
13 changed files with 365 additions and 1 deletions

56
@vates/xml/.USAGE.md Normal file
View File

@@ -0,0 +1,56 @@
### `parseXml(xml, [opts])`
> Based on [`sax`](https://www.npmjs.com/package/sax)
`opts` is an optional object which can contain the following options:
- `normalize = true`: if true, then turn any whitespace into a single space
- `strict = true`: whether or not to be a jerk
- `trim = true`: whether or not to trim text nodes
```js
import { parseXml } from '@xen-orchestra/xml/parse'
const tree = parseXml(`<?xml version="1.0" encoding="UTF-8"?>
<tag1 attr1="value1" attr2="value2">
Text &amp; entities
<tag2 />
<tag2 />
<ns:tag3 />
</tag1>
`)
// → {
// name: 'tag1',
// attributes: { attr1: 'value1', attr2: 'value2' },
// children: [
// 'Text & entities',
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'ns:tag3', attributes: {}, children: [] }
// ]
// }
```
### `formatXml(tree, [opts])`
`opts` is an optional object which can contain the following options:
- `includeDeclaration = true`: whether to include an XML declaration
- `indent = 2`: string or number of spaces to use to indent; if an empty string or `0`, no indent or new lines will be used
```js
import { formatXml } from '@xen-orchestra/xml/format'
formatXml({
name: 'tag1',
attributes: { attr1: 'value1', attr2: 'value2' },
children: ['Text & entities', { name: 'tag2' }, { name: 'tag2' }, { name: 'ns:tag3' }],
})
// → <?xml version="1.0" encoding="UTF-8"?>
// <tag1 attr1="value1" attr2="value2">
// Text &amp; entities
// <tag2 />
// <tag2 />
// <ns:tag3 />
// </tag1>
```

1
@vates/xml/.npmignore Symbolic link
View File

@@ -0,0 +1 @@
../../scripts/npmignore

89
@vates/xml/README.md Normal file
View File

@@ -0,0 +1,89 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/xml
[![Package Version](https://badgen.net/npm/v/@vates/xml)](https://npmjs.org/package/@vates/xml) ![License](https://badgen.net/npm/license/@vates/xml) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/xml)](https://bundlephobia.com/result?p=@vates/xml) [![Node compatibility](https://badgen.net/npm/node/@vates/xml)](https://npmjs.org/package/@vates/xml)
> Simple XML formatting/parsing
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/xml):
```sh
npm install --save @vates/xml
```
## Usage
### `parseXml(xml, [opts])`
> Based on [`sax`](https://www.npmjs.com/package/sax)
`opts` is an optional object which can contain the following options:
- `normalize = true`: if true, then turn any whitespace into a single space
- `strict = true`: whether or not to be a jerk
- `trim = true`: whether or not to trim text nodes
```js
import { parseXml } from '@xen-orchestra/xml/parse'
const tree = parseXml(`<?xml version="1.0" encoding="UTF-8"?>
<tag1 attr1="value1" attr2="value2">
Text &amp; entities
<tag2 />
<tag2 />
<ns:tag3 />
</tag1>
`)
// → {
// name: 'tag1',
// attributes: { attr1: 'value1', attr2: 'value2' },
// children: [
// 'Text & entities',
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'tag2', attributes: {}, children: [] },
// { name: 'ns:tag3', attributes: {}, children: [] }
// ]
// }
```
### `formatXml(tree, [opts])`
`opts` is an optional object which can contain the following options:
- `includeDeclaration = true`: whether to include an XML declaration
- `indent = 2`: string or number of spaces to use to indent; if an empty string or `0`, no indent or new lines will be used
```js
import { formatXml } from '@xen-orchestra/xml/format'
formatXml({
name: 'tag1',
attributes: { attr1: 'value1', attr2: 'value2' },
children: ['Text & entities', { name: 'tag2' }, { name: 'tag2' }, { name: 'ns:tag3' }],
})
// → <?xml version="1.0" encoding="UTF-8"?>
// <tag1 attr1="value1" attr2="value2">
// Text &amp; entities
// <tag2 />
// <tag2 />
// <ns:tag3 />
// </tag1>
```
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

30
@vates/xml/cli.js Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env node
'use strict'
const { inspect } = require('node:util')
const { formatXml } = require('@vates/xml/format')
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
function log(val) {
process.stdout.write(inspect(val, false, null, true))
process.stdout.write('\n')
}
function main([inputPath = 0]) {
const input = readFileSync(inputPath)
// attempt to parse from JSON
let data
try {
data = JSON.parse(input)
} catch (error) {
// fallback to XML
log(parseXml(input))
return
}
process.stdout.write(formatXml(data))
}
main(process.argv.slice(2))

64
@vates/xml/format.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict'
const ENTITIES = {
'"': '&quot;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&apos;',
}
const RE = new RegExp(Object.keys(ENTITIES).join('|'), 'g')
function replace(match) {
return ENTITIES[match]
}
function escape(str) {
return str.replace(RE, replace)
}
function formatNode(node, depth) {
const indent = this.indent.repeat(depth)
if (typeof node === 'object') {
const line = [indent, '<', node.name]
const { attributes } = node
if (attributes !== undefined) {
for (const name of Object.keys(attributes)) {
line.push(' ', name, '="', escape(attributes[name]), '"')
}
}
const { children } = node
if (children === undefined || children.length === 0) {
line.push(' />')
this.lines.push(line.join(''))
} else {
line.push('>')
this.lines.push(line.join(''))
for (const child of children) {
formatNode.call(this, child, depth + 1)
}
this.lines.push(indent + '</' + node.name + '>')
}
} else {
// string (or scalar)
this.lines.push(indent + escape(String(node)))
}
}
exports.formatXml = function formatXml(tree, { includeDeclaration = true, indent = 2 } = {}) {
const lines = []
if (includeDeclaration) {
lines.push('<?xml version="1.0" encoding="UTF-8"?>')
}
const ctx = {
lines,
indent: typeof indent === 'number' ? ' '.repeat(indent) : indent,
}
formatNode.call(ctx, tree, 0)
return lines.join(indent !== 0 && indent !== '' ? '\n' : '')
}

35
@vates/xml/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"private": false,
"name": "@vates/xml",
"description": "Simple XML formatting/parsing",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/xml",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@vates/xml",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.0.0",
"engines": {
"node": ">=14"
},
"dependencies": {
"sax": "^1.3.0"
},
"devDependencies": {
"test": "^3.3.0"
},
"exports": {
"./format": "./format.js",
"./parse": "./parse.js"
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
}
}

23
@vates/xml/parse.js Normal file
View File

@@ -0,0 +1,23 @@
'use strict'
const sax = require('sax')
exports.parseXml = function parseXml(xml, { normalize = true, strict = true, trim = true } = {}) {
const stack = [{ children: [] }]
const parser = sax.parser(strict, { normalize, trim })
parser.ontext = text => {
stack[stack.length - 1].children.push(text)
}
parser.onopentag = ({ name, attributes }) => {
stack.push({ name, attributes, children: [] })
}
parser.onclosetag = () => {
const node = stack.pop()
stack[stack.length - 1].children.push(node)
}
parser.write(xml).close()
return stack[0].children[0]
}

25
@vates/xml/test/data.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "tag1",
"attributes": {
"attr1": "value1",
"attr2": "value2"
},
"children": [
"Text & entities",
{
"name": "tag2",
"attributes": {},
"children": []
},
{
"name": "tag2",
"attributes": {},
"children": []
},
{
"name": "ns:tag3",
"attributes": {},
"children": []
}
]
}

7
@vates/xml/test/data.xml Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<tag1 attr1="value1" attr2="value2">
Text &amp; entities
<tag2 />
<tag2 />
<ns:tag3 />
</tag1>

18
@vates/xml/test/format.js Normal file
View File

@@ -0,0 +1,18 @@
'use strict'
const { formatXml } = require('@vates/xml/format')
const { join } = require('node:path')
const { readFileSync } = require('node:fs')
const assert = require('node:assert/strict')
const test = require('test')
test('format()', function () {
const xml = String(readFileSync(join(__dirname, 'data.xml')))
const tree = JSON.parse(readFileSync(join(__dirname, 'data.json')))
assert.equal(formatXml(tree) + '\n', xml)
})
test('supports missing attributes and children', function () {
assert.equal(formatXml({ name: 'foo' }, { includeDeclaration: false }), '<foo />')
})

14
@vates/xml/test/parse.js Normal file
View File

@@ -0,0 +1,14 @@
'use strict'
const { parseXml } = require('@vates/xml/parse')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
const assert = require('node:assert/strict')
const test = require('test')
test('parse()', function () {
const xml = String(readFileSync(join(__dirname, 'data.xml')))
const tree = JSON.parse(readFileSync(join(__dirname, 'data.json')))
assert.deepEqual(parseXml(xml), tree)
})

View File

@@ -27,4 +27,6 @@
<!--packages-start-->
- @vates/xml major
<!--packages-end-->

View File

@@ -19811,7 +19811,7 @@ sax-parser@^2.0.2:
resolved "https://registry.yarnpkg.com/sax-parser/-/sax-parser-2.0.2.tgz#7b3b4a25fc69bf4e729ad5f0f98430205d461689"
integrity sha512-EjLxlFjZdmv/cpOwV+klYEeOYjR2Dc9C495d2Ruk+N6xknrOnIfjSum2a63hfi9Vox2fCsjYc3NuDVo0YkGpjg==
sax@>=0.6, sax@>=0.6.0:
sax@>=0.6, sax@>=0.6.0, sax@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==