Skip to content

Commit ab8d40a

Browse files
authored
fix #593: support metadata on nodes (5.x edition) (#729)
* fix #593: support line numbers - add a new symbol, accessible via XMLParser.getStartIndexSymbol() which reflects the start index of a tag - copy that start index in the compressed form as well (cherry picked from commit d7601c3) * fix #593: support line numbers - correct .d.ts * fix #593: support line numbers - add a preserveStartIndex option, off by default - use an ordinary property if Symbol is not available - update tests and docs * feat #593: add metadata - add a captureMetaData which adds a metadata object - update tests and typings * feat #593: add metadata test - add test for captureMetadata && isArray && stopNodes && unpairedTags && updateTag * feat #593: update docs for metadata - update documentation - update fxp.d.ts for metadata * feat #593: update .cts and .ts for metadata - remove duplicate X2jMetadata class - update .cts file with metadata
1 parent 0272fd5 commit ab8d40a

File tree

10 files changed

+250
-14
lines changed

10 files changed

+250
-14
lines changed

docs/v4/1.GettingStarted.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ if(XMLValidator.validate()){
3333
}
3434
```
3535

36-
[> Next: XmlParser](./2.XMLparseOptions.md)
36+
[> Next: XmlParser](./2.XMLparseOptions.md)

docs/v4/2.XMLparseOptions.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,4 +1001,23 @@ const parser = new XMLParser(options);
10011001
const output = parser.parse(xmlDataStr);
10021002
```
10031003

1004+
## captureMetadata
1005+
1006+
If `captureMetadata` is true, then a [Symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol) is added to nodes with metadata about that node.
1007+
1008+
`XMLParser.getMetaDataSymbol()` will return the symbol. Being a Symbol, this property
1009+
will not normally appear in, for example, `Object.keys`.
1010+
1011+
The MetaData object is not available for nodes that resolve as strings or arrays.
1012+
1013+
```js
1014+
const parser = new XMLParser({ignoreAttributes: false, captureMetaData: true});
1015+
const jsonObj = parser.parse(`<root><thing name="zero"/><thing name="one"/></root>`);
1016+
const META_DATA_SYMBOL = XMLParser.getMetaDataSymbol();
1017+
// get the char offset of the start of the tag for <thing name="zero"/>
1018+
const thingZero = jsonObj.root.thing[0];
1019+
const thingZeroMetaData = thingZero[META_DATA_SYMBOL];
1020+
const thingZeroStartIndex = thingZeroMetaData.startIndex; // 6
1021+
```
1022+
10041023
[> Next: XmlBuilder](./3.XMLBuilder.md)

lib/fxp.d.cts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ type X2jOptions = {
210210
* Defaults to `(tagName, jPath, attrs) => tagName`
211211
*/
212212
updateTag?: (tagName: string, jPath: string, attrs: {[k: string]: string}) => string | boolean;
213+
214+
/**
215+
* If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with
216+
* metadata about each the node in the XML file.
217+
*/
218+
captureMetaData?: boolean;
213219
};
214220

215221
type strnumOptions = {
@@ -407,6 +413,18 @@ declare class XMLParser {
407413
* @param entityValue {string} Eg: '\r'
408414
*/
409415
addEntity(entityIdentifier: string, entityValue: string): void;
416+
417+
/**
418+
* Returns a Symbol that can be used to access the {@link XMLMetaData}
419+
* property on a node.
420+
*
421+
* If Symbol is not available in the environment, an ordinary property is used
422+
* and the name of the property is here returned.
423+
*
424+
* The XMLMetaData property is only present when {@link X2jOptions.captureMetaData}
425+
* is true in the options.
426+
*/
427+
static getMetaDataSymbol() : Symbol;
410428
}
411429

412430
declare class XMLValidator{
@@ -418,12 +436,23 @@ declare class XMLBuilder {
418436
build(jObj: any): string;
419437
}
420438

439+
440+
/**
441+
* This object is available on nodes via the symbol {@link XMLParser.getMetaDataSymbol}
442+
* when {@link X2jOptions.captureMetaData} is true.
443+
*/
444+
declare interface XMLMetaData {
445+
/** The index, if available, of the character where the XML node began in the input stream. */
446+
startIndex?: number;
447+
}
448+
421449
declare namespace fxp {
422450
export {
423451
XMLParser,
424452
XMLValidator,
425-
XMLBuilder
453+
XMLBuilder,
454+
XMLMetaData
426455
}
427456
}
428457

429-
export = fxp;
458+
export = fxp;

spec/startIndex_spec.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
2+
import { XMLParser } from "../src/fxp.js";
3+
4+
describe("XMLParser", function () {
5+
6+
const xmlData = `<root><foo/><bar type="quux"/><bar type="bat"/><baz type="foo">FOO</baz></root>`;
7+
const XML_METADATA = XMLParser.getMetaDataSymbol();
8+
9+
it("should support captureMetadata && !preserveOrder", function () {
10+
const expected = {
11+
root: {
12+
[XML_METADATA]: { startIndex: 0 },
13+
foo: '',
14+
bar: [
15+
{
16+
[XML_METADATA]: { startIndex: 12 },
17+
"@_type": 'quux'
18+
},
19+
{
20+
[XML_METADATA]: { startIndex: 30 },
21+
"@_type": 'bat'
22+
},
23+
],
24+
baz: {
25+
'@_type': 'foo',
26+
'#text': 'FOO',
27+
[XML_METADATA]: {startIndex: 47},
28+
}
29+
}
30+
};
31+
32+
const parser = new XMLParser({ preserveOrder: false, ignoreAttributes: false, captureMetaData: true });
33+
const result = parser.parse(xmlData);
34+
// console.dir({ result, expected }, { depth: Infinity });
35+
expect(result).toEqual(expected);
36+
});
37+
it("should support captureMetadata && preserveOrder", function () {
38+
const expected = [
39+
{
40+
root: [
41+
{ foo: [], [XML_METADATA]: { startIndex: 6 } },
42+
{
43+
bar: [],
44+
':@': { "@_type": 'quux' },
45+
[XML_METADATA]: { startIndex: 12 },
46+
},
47+
{
48+
bar: [],
49+
':@': { "@_type": 'bat' },
50+
[XML_METADATA]: { startIndex: 30 },
51+
},
52+
{
53+
baz: [{ '#text': 'FOO' }],
54+
':@': { '@_type': 'foo' },
55+
[XML_METADATA]: {startIndex: 47},
56+
},
57+
],
58+
[XML_METADATA]: { startIndex: 0 },
59+
}
60+
];
61+
62+
const parser = new XMLParser({ preserveOrder: true, ignoreAttributes: false, captureMetaData: true });
63+
const result = parser.parse(xmlData);
64+
// console.dir({ result, expected }, { depth: Infinity });
65+
expect(result).toEqual(expected);
66+
});
67+
const xmlData2 = `<root><foo/><bar type="quux"/><bar type="bat"/><baz type="foo">FOO</baz><stop>This is a <b>stop</b> node.</stop><unpaired attr="1"></root>`;
68+
69+
it("should support captureMetadata && isArray && stopNodes && unpairedTags && updateTag", function () {
70+
const expected = {
71+
ROOT: {
72+
[XML_METADATA]: { startIndex: 0 },
73+
foo: [''],
74+
bar: [
75+
{
76+
[XML_METADATA]: { startIndex: 12 },
77+
"@_type": 'quux'
78+
},
79+
{
80+
[XML_METADATA]: { startIndex: 30 },
81+
"@_type": 'bat'
82+
},
83+
],
84+
baz: {
85+
'#text': 'FOO',
86+
'@_type': 'foo',
87+
[XML_METADATA]: { startIndex: 47 },
88+
},
89+
// no metadata on stop nodes.
90+
stop: 'This is a <b>stop</b> node.',
91+
unpaired: {
92+
'@_attr': '1',
93+
[XML_METADATA]: { startIndex: 112 },
94+
}
95+
}
96+
};
97+
98+
const parser = new XMLParser({
99+
preserveOrder: false, ignoreAttributes: false, captureMetaData: true,
100+
isArray(tagName) {
101+
return (tagName == 'foo');
102+
},
103+
stopNodes: [ 'root.stop' ], unpairedTags: ['unpaired'],
104+
updateTag(tagName) {
105+
if (tagName === 'root') {
106+
tagName = 'ROOT';
107+
}
108+
return tagName;
109+
},
110+
});
111+
const result = parser.parse(xmlData2);
112+
// console.dir({ result, expected }, { depth: Infinity });
113+
expect(result).toEqual(expected);
114+
});
115+
116+
117+
});

src/fxp.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ type X2jOptions = {
210210
* Defaults to `(tagName, jPath, attrs) => tagName`
211211
*/
212212
updateTag?: (tagName: string, jPath: string, attrs: {[k: string]: string}) => string | boolean;
213+
214+
/**
215+
* If true, adds a Symbol to all object nodes, accessible by {@link XMLParser.getMetaDataSymbol} with
216+
* metadata about each the node in the XML file.
217+
*/
218+
captureMetaData?: boolean;
213219
};
214220

215221
type strnumOptions = {
@@ -407,6 +413,18 @@ export class XMLParser {
407413
* @param entityValue {string} Eg: '\r'
408414
*/
409415
addEntity(entityIdentifier: string, entityValue: string): void;
416+
417+
/**
418+
* Returns a Symbol that can be used to access the {@link XMLMetaData}
419+
* property on a node.
420+
*
421+
* If Symbol is not available in the environment, an ordinary property is used
422+
* and the name of the property is here returned.
423+
*
424+
* The XMLMetaData property is only present when {@link X2jOptions.captureMetaData}
425+
* is true in the options.
426+
*/
427+
static getMetaDataSymbol() : Symbol;
410428
}
411429

412430
export class XMLValidator{
@@ -416,3 +434,12 @@ export class XMLBuilder {
416434
constructor(options?: XmlBuilderOptions);
417435
build(jObj: any): string;
418436
}
437+
438+
/**
439+
* This object is available on nodes via the symbol {@link XMLParser.getMetaDataSymbol}
440+
* when {@link X2jOptions.captureMetaData} is true.
441+
*/
442+
export interface XMLMetaData {
443+
/** The index, if available, of the character where the XML node began in the input stream. */
444+
startIndex?: number;
445+
}

src/xmlparser/OptionsBuilder.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const defaultOptions = {
3838
return tagName
3939
},
4040
// skipEmptyListItem: false
41+
captureMetaData: false,
4142
};
4243

4344
export const buildOptions = function(options) {

src/xmlparser/OrderedObjParser.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,7 @@ const parseXml = function(xmlData) {
246246
if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){
247247
childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName);
248248
}
249-
this.addChild(currentNode, childNode, jPath)
250-
249+
this.addChild(currentNode, childNode, jPath, i);
251250
}
252251

253252

@@ -312,6 +311,7 @@ const parseXml = function(xmlData) {
312311
if(tagName !== xmlObj.tagname){
313312
jPath += jPath ? "." + tagName : tagName;
314313
}
314+
const startIndex = i;
315315
if (this.isItStopNode(this.options.stopNodes, jPath, tagName)) {
316316
let tagContent = "";
317317
//self-closing tag
@@ -340,6 +340,7 @@ const parseXml = function(xmlData) {
340340
}
341341

342342
const childNode = new xmlNode(tagName);
343+
343344
if(tagName !== tagExp && attrExpPresent){
344345
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
345346
}
@@ -350,7 +351,7 @@ const parseXml = function(xmlData) {
350351
jPath = jPath.substr(0, jPath.lastIndexOf("."));
351352
childNode.add(this.options.textNodeName, tagContent);
352353

353-
this.addChild(currentNode, childNode, jPath)
354+
this.addChild(currentNode, childNode, jPath, startIndex);
354355
}else{
355356
//selfClosing tag
356357
if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){
@@ -370,7 +371,7 @@ const parseXml = function(xmlData) {
370371
if(tagName !== tagExp && attrExpPresent){
371372
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
372373
}
373-
this.addChild(currentNode, childNode, jPath)
374+
this.addChild(currentNode, childNode, jPath, startIndex);
374375
jPath = jPath.substr(0, jPath.lastIndexOf("."));
375376
}
376377
//opening tag
@@ -381,7 +382,7 @@ const parseXml = function(xmlData) {
381382
if(tagName !== tagExp && attrExpPresent){
382383
childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName);
383384
}
384-
this.addChild(currentNode, childNode, jPath)
385+
this.addChild(currentNode, childNode, jPath, startIndex);
385386
currentNode = childNode;
386387
}
387388
textData = "";
@@ -395,14 +396,16 @@ const parseXml = function(xmlData) {
395396
return xmlObj.child;
396397
}
397398

398-
function addChild(currentNode, childNode, jPath){
399+
function addChild(currentNode, childNode, jPath, startIndex){
400+
// unset startIndex if not requested
401+
if (!this.options.captureMetaData) startIndex = undefined;
399402
const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"])
400403
if(result === false){
401-
}else if(typeof result === "string"){
404+
} else if(typeof result === "string"){
402405
childNode.tagname = result
403-
currentNode.addChild(childNode);
406+
currentNode.addChild(childNode, startIndex);
404407
}else{
405-
currentNode.addChild(childNode);
408+
currentNode.addChild(childNode, startIndex);
406409
}
407410
}
408411

src/xmlparser/XMLParser.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { buildOptions} from './OptionsBuilder.js';
22
import OrderedObjParser from './OrderedObjParser.js';
33
import prettify from './node2json.js';
44
import {validate} from "../validator.js";
5+
import XmlNode from './xmlNode.js';
56

67
export default class XMLParser{
78

@@ -53,4 +54,18 @@ export default class XMLParser{
5354
this.externalEntities[key] = value;
5455
}
5556
}
56-
}
57+
58+
/**
59+
* Returns a Symbol that can be used to access the metadata
60+
* property on a node.
61+
*
62+
* If Symbol is not available in the environment, an ordinary property is used
63+
* and the name of the property is here returned.
64+
*
65+
* The XMLMetaData property is only present when `captureMetaData`
66+
* is true in the options.
67+
*/
68+
static getMetaDataSymbol() {
69+
return XmlNode.getMetaDataSymbol();
70+
}
71+
}

src/xmlparser/node2json.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict';
22

3+
import XmlNode from './xmlNode.js';
4+
5+
const METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
6+
37
/**
48
*
59
* @param {array} node
@@ -36,6 +40,9 @@ function compress(arr, options, jPath){
3640

3741
let val = compress(tagObj[property], options, newJpath);
3842
const isLeaf = isLeafTag(val, options);
43+
if (tagObj[METADATA_SYMBOL] !== undefined) {
44+
val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
45+
}
3946

4047
if(tagObj[":@"]){
4148
assignAttributes( val, tagObj[":@"], newJpath, options);

0 commit comments

Comments
 (0)