@ooxml-tools/xml 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/dist/npm/index.js +101 -0
- package/dist/npm/types.d.ts +19 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://ooxml-tools.github.io/design/images/xml-dark.png">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://ooxml-tools.github.io/design/images/xml-light.png">
|
|
5
|
+
<img alt="@ooxml-tools/xml" height="56" src="https://ooxml-tools.github.io/design/images/xml-light.png">
|
|
6
|
+
</picture>
|
|
7
|
+
</h1>
|
|
8
|
+
|
|
9
|
+
Some XML helpers to help with OOXML development
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> These are all based around [`xml-js`](https://www.npmjs.com/package/xml-js) and output that modules internal format
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
17
|
+
### `asXmlElement`
|
|
18
|
+
When working with `xml-js` it's really handy to not have to worry about when you're passing an XML string our the `xml-js` JSON type called `Element`
|
|
19
|
+
|
|
20
|
+
This helper function just outputs an `Element` for either input
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
asXmlElement("<name>foo</name>") // => {name: "name", text: "foo"}
|
|
24
|
+
asXmlElement({name: "name", text: "foo"}) // => {name: "name", text: "foo"}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> All the other functions in this document that accept XML, also accept this `Element` (using this function)
|
|
29
|
+
|
|
30
|
+
### `safeXml`
|
|
31
|
+
An XML tagged template literal with the following features
|
|
32
|
+
|
|
33
|
+
- error if XML is incorrect
|
|
34
|
+
- array support in substitution
|
|
35
|
+
|
|
36
|
+
The following will error in development, because of mismatched start/end tags
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
safeXml`<foo>hello</bar>`
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Substitution of arrays "just works" so you can map values in the tagged template literals
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const outXml = safeXml`<test>${[1,2,3].map(n => safeXml`<name>item ${1}<name>`)}</test>`
|
|
46
|
+
assert.equal(outXml, `<test><name>item 1</name><name>item 2</name><name>item 3</name></test>`);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This also makes it easy to support <https://prettier.io/docs/en/options#embedded-language-formatting>
|
|
50
|
+
|
|
51
|
+
### `compact`
|
|
52
|
+
Removes non required whitespace from the XML
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const outXml = compact(`
|
|
56
|
+
<test>
|
|
57
|
+
something
|
|
58
|
+
</test>
|
|
59
|
+
`);
|
|
60
|
+
assert.equal(outXml, "<test>something</test>");
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### `removeFragments`
|
|
64
|
+
For XML to be valid, there must be a single root node. When composing apps it's handy for that not to be true.
|
|
65
|
+
|
|
66
|
+
For example the following would error
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const xml = safeXml`
|
|
70
|
+
<name>foo</name>
|
|
71
|
+
<name>bar</name>
|
|
72
|
+
`
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
So instead we'd like something like a HTML fragment
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const xml = safeXml`
|
|
79
|
+
<XML_FRAGMENT>
|
|
80
|
+
<name>foo</name>
|
|
81
|
+
<name>bar</name>
|
|
82
|
+
</XML_FRAGMENT>
|
|
83
|
+
`
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
So we did just that, `removeFragments` walks over a tree removing `<XML_FRAGMENT>...</XML_FRAGMENT>` nodes.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const innerBit = safeXml`
|
|
90
|
+
<XML_FRAGMENT>
|
|
91
|
+
<name>foo</name>
|
|
92
|
+
<name>bar</name>
|
|
93
|
+
</XML_FRAGMENT>
|
|
94
|
+
`
|
|
95
|
+
const newXml = removeFragments(
|
|
96
|
+
safeXml`
|
|
97
|
+
<doc>
|
|
98
|
+
${innerBit}
|
|
99
|
+
</doc>
|
|
100
|
+
`
|
|
101
|
+
)
|
|
102
|
+
assert.equal(newXml, `
|
|
103
|
+
<doc>
|
|
104
|
+
<name>foo</name>
|
|
105
|
+
<name>bar</name>
|
|
106
|
+
</doc>
|
|
107
|
+
`)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `cdata`
|
|
111
|
+
From [the wikipedia page](https://en.wikipedia.org/wiki/CDATA)
|
|
112
|
+
|
|
113
|
+
> CDATA section is a piece of element content that is marked up to be interpreted literally, as textual data, not as marked-up content.
|
|
114
|
+
|
|
115
|
+
But `<name><![CDATA[one < two]]><name>` is ugly and hard to read, so instead
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
safeXml`<name>${cdata("one < two")}</name>`
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { xml2js, js2xml } from 'xml-js';
|
|
2
|
+
|
|
3
|
+
function asXmlElement(xml) {
|
|
4
|
+
if (typeof xml === "string") {
|
|
5
|
+
return xml2js(xml, {
|
|
6
|
+
compact: false,
|
|
7
|
+
captureSpacesBetweenElements: false,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
return xml;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param templateStrings template strings
|
|
17
|
+
* @param templateValues tamplate values
|
|
18
|
+
* @returns xml string
|
|
19
|
+
*/
|
|
20
|
+
function safeXml(templateStrings, ...templateValues) {
|
|
21
|
+
let xml = "";
|
|
22
|
+
for (let i = 0; i < templateStrings.length; i++) {
|
|
23
|
+
if (templateValues[i] !== undefined) {
|
|
24
|
+
const value = templateValues[i];
|
|
25
|
+
const xmlPart = Array.isArray(value) ? value.join("") : value;
|
|
26
|
+
xml += `${templateStrings[i]}${xmlPart}`;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
xml += `${templateStrings[i]}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (process.env.NODE_ENV === "development") {
|
|
33
|
+
// This will "throw" if the XML is invalid
|
|
34
|
+
xml2js(xml, {
|
|
35
|
+
compact: false,
|
|
36
|
+
captureSpacesBetweenElements: false,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return xml;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cdata(input) {
|
|
43
|
+
return `<![CDATA[${input}]]>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compact(xml) {
|
|
47
|
+
const parsed = xml2js(xml, {
|
|
48
|
+
compact: false,
|
|
49
|
+
trim: true,
|
|
50
|
+
});
|
|
51
|
+
return js2xml(parsed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createFragment(elements) {
|
|
55
|
+
return {
|
|
56
|
+
type: "element",
|
|
57
|
+
name: "XML_FRAGMENT",
|
|
58
|
+
elements: elements,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function _collapseFragments(orig) {
|
|
62
|
+
var _a;
|
|
63
|
+
let node = orig;
|
|
64
|
+
const cloneIfRequired = () => {
|
|
65
|
+
if (node === orig) {
|
|
66
|
+
return Object.assign(Object.assign({}, node), { elements: node.elements ? [...node.elements] : undefined });
|
|
67
|
+
}
|
|
68
|
+
return node;
|
|
69
|
+
};
|
|
70
|
+
if (node.elements) {
|
|
71
|
+
for (let i = node.elements.length - 1; i >= 0; i--) {
|
|
72
|
+
if (node.elements) {
|
|
73
|
+
const child = node.elements[i];
|
|
74
|
+
const out = _collapseFragments(child);
|
|
75
|
+
if (Array.isArray(out)) {
|
|
76
|
+
node = cloneIfRequired();
|
|
77
|
+
node.elements.splice(i, 1, ...out);
|
|
78
|
+
}
|
|
79
|
+
else if (out !== child) {
|
|
80
|
+
node = cloneIfRequired();
|
|
81
|
+
if (node.elements) {
|
|
82
|
+
node.elements[i] = out;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.name === "XML_FRAGMENT") {
|
|
89
|
+
return (_a = node.elements) !== null && _a !== void 0 ? _a : [];
|
|
90
|
+
}
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
93
|
+
function collapseFragments(root) {
|
|
94
|
+
const out = _collapseFragments(asXmlElement(root));
|
|
95
|
+
if (Array.isArray(out)) {
|
|
96
|
+
return createFragment(out);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { asXmlElement, cdata, collapseFragments, compact, createFragment, safeXml };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Element } from 'xml-js';
|
|
2
|
+
|
|
3
|
+
declare function asXmlElement(xml: string | Element): Element;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param templateStrings template strings
|
|
7
|
+
* @param templateValues tamplate values
|
|
8
|
+
* @returns xml string
|
|
9
|
+
*/
|
|
10
|
+
declare function safeXml(templateStrings: TemplateStringsArray, ...templateValues: (string | number | (string | number)[])[]): string;
|
|
11
|
+
|
|
12
|
+
declare function cdata(input: string | number): string;
|
|
13
|
+
|
|
14
|
+
declare function compact(xml: string): string;
|
|
15
|
+
|
|
16
|
+
declare function createFragment(elements: Element[]): Element;
|
|
17
|
+
declare function collapseFragments(root: string | Element): Element;
|
|
18
|
+
|
|
19
|
+
export { asXmlElement, cdata, collapseFragments, compact, createFragment, safeXml };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ooxml-tools/xml",
|
|
3
|
+
"description": "Some XML helpers to help with OOXML development",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/npm/index.js",
|
|
7
|
+
"types": "./dist/npm/types.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint": "npx prettier . --check",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"lint:format": "npx prettier . --write",
|
|
13
|
+
"build": "rollup -c rollup.config.ts --configPlugin typescript"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/npm/index.js",
|
|
17
|
+
"./commands": "./dist/npm/commands.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"./dist/npm",
|
|
21
|
+
"./package.json",
|
|
22
|
+
"./README.md"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
26
|
+
"@rollup/plugin-typescript": "^12.1.2",
|
|
27
|
+
"@rollup/plugin-virtual": "^3.0.2",
|
|
28
|
+
"@tsconfig/node22": "^22.0.0",
|
|
29
|
+
"@types/yargs": "^17.0.32",
|
|
30
|
+
"prettier": "^3.4.2",
|
|
31
|
+
"rollup": "^4.18.1",
|
|
32
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
33
|
+
"rollup-plugin-dts": "^6.1.1",
|
|
34
|
+
"rollup-plugin-preserve-shebang": "^1.0.1",
|
|
35
|
+
"rollup-plugin-typescript-paths": "^1.5.0",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"tslib": "^2.6.3",
|
|
38
|
+
"tsx": "^4.17.0",
|
|
39
|
+
"typescript": "^5.5.4",
|
|
40
|
+
"vitest": "^2.1.6"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.x"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"xml-js": "^1.6.11"
|
|
47
|
+
}
|
|
48
|
+
}
|