mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(xml): new lib to parse/format XML
This commit is contained in:
committed by
Pierre Donias
parent
db3c51d55e
commit
798049c20e
56
@vates/xml/.USAGE.md
Normal file
56
@vates/xml/.USAGE.md
Normal 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 & 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 & entities
|
||||
// <tag2 />
|
||||
// <tag2 />
|
||||
// <ns:tag3 />
|
||||
// </tag1>
|
||||
```
|
||||
1
@vates/xml/.npmignore
Symbolic link
1
@vates/xml/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
89
@vates/xml/README.md
Normal file
89
@vates/xml/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/xml
|
||||
|
||||
[](https://npmjs.org/package/@vates/xml)  [](https://bundlephobia.com/result?p=@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 & 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 & 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
30
@vates/xml/cli.js
Executable 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
64
@vates/xml/format.js
Normal file
@@ -0,0 +1,64 @@
|
||||
'use strict'
|
||||
|
||||
const ENTITIES = {
|
||||
'"': '"',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
}
|
||||
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
35
@vates/xml/package.json
Normal 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
23
@vates/xml/parse.js
Normal 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
25
@vates/xml/test/data.json
Normal 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
7
@vates/xml/test/data.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tag1 attr1="value1" attr2="value2">
|
||||
Text & entities
|
||||
<tag2 />
|
||||
<tag2 />
|
||||
<ns:tag3 />
|
||||
</tag1>
|
||||
18
@vates/xml/test/format.js
Normal file
18
@vates/xml/test/format.js
Normal 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
14
@vates/xml/test/parse.js
Normal 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)
|
||||
})
|
||||
@@ -27,4 +27,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/xml major
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user