2025-02-21 14:54:50 -05:00
|
|
|
import xml2js from 'xml2js';
|
|
|
|
|
|
|
|
|
|
class Data {
|
|
|
|
|
#obj;
|
|
|
|
|
#expression;
|
|
|
|
|
|
|
|
|
|
constructor(obj, expression) {
|
2025-02-23 13:22:32 -05:00
|
|
|
// xml2js returns leaf nodes as strings, turn those into consistent objects
|
|
|
|
|
// I found this to be safer and more efficient than using the explicitCharkey option
|
2025-02-21 14:54:50 -05:00
|
|
|
this.#obj = typeof obj === 'string' ? { _: obj } : obj;
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
// this identifies how the object was referenced, helps a ton with debugging
|
|
|
|
|
this.#expression = expression;
|
2025-02-21 14:54:50 -05:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
#buildExpression(propName, index = undefined) {
|
2025-02-21 14:54:50 -05:00
|
|
|
let expression = `${this.#expression}.${propName}`;
|
|
|
|
|
if (index !== undefined) {
|
|
|
|
|
expression += `[${index}]`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return expression;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
// used by "optional" functions to return undefined instead of throwing an error
|
|
|
|
|
#optional(func) {
|
|
|
|
|
try {
|
|
|
|
|
return func();
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// will not throw an error if property doesn't exist, defaults to empty array
|
2025-02-21 18:54:19 -05:00
|
|
|
children(propName) {
|
|
|
|
|
const nodes = this.#obj[propName] ?? [];
|
|
|
|
|
return nodes.map((value, index) => new Data(value, this.#buildExpression(propName, index)));
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
// throws an error if property (or index on property) doesn't exist
|
2025-02-21 18:54:19 -05:00
|
|
|
child(propName, index = 0) {
|
|
|
|
|
const nodes = this.#obj[propName];
|
|
|
|
|
if (nodes === undefined) {
|
2025-02-21 14:54:50 -05:00
|
|
|
throw new Error(`Could not find ${this.#buildExpression(propName)}.`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-21 18:54:19 -05:00
|
|
|
const node = nodes[index];
|
|
|
|
|
if (node === undefined) {
|
|
|
|
|
throw new Error(`Could not find ${this.#buildExpression(propName, index)}.`);
|
|
|
|
|
}
|
2025-02-21 14:54:50 -05:00
|
|
|
|
2025-02-21 18:54:19 -05:00
|
|
|
return new Data(node, this.#buildExpression(propName, index));
|
2025-02-21 14:54:50 -05:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
// convenience function, since it's very common to want the value of a child
|
2025-02-21 18:54:19 -05:00
|
|
|
childValue(propName, index = 0) {
|
2025-02-23 13:22:32 -05:00
|
|
|
return this.child(propName, index).value();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// throws an error if this object doesn't have a value string
|
|
|
|
|
value() {
|
|
|
|
|
const value = this.#obj._;
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
throw new Error(`Could not get value from ${this.#expression}.`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
2025-02-21 14:54:50 -05:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 13:22:32 -05:00
|
|
|
// throws an error if attribute does not exist
|
2025-02-21 18:54:19 -05:00
|
|
|
attribute(attrName) {
|
2025-02-21 14:54:50 -05:00
|
|
|
const attribute = this.#obj.$?.[attrName];
|
|
|
|
|
if (attribute === undefined) {
|
|
|
|
|
throw new Error(`Could not get attribute ${attrName} from ${this.#expression}.`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return attribute;
|
|
|
|
|
}
|
2025-02-23 13:22:32 -05:00
|
|
|
|
|
|
|
|
optionalChild(propName, index = 0) {
|
|
|
|
|
return this.#optional(() => this.child(propName, index));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
optionalChildValue(propName, index = 0) {
|
|
|
|
|
return this.#optional(() => this.childValue(propName, index));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
optionalValue() {
|
|
|
|
|
return this.#optional(() => this.value());
|
|
|
|
|
}
|
2025-02-21 14:54:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function load(content) {
|
|
|
|
|
const rootData = await xml2js.parseStringPromise(content, {
|
|
|
|
|
tagNameProcessors: [xml2js.processors.stripPrefix],
|
|
|
|
|
trim: true
|
|
|
|
|
}).catch((ex) => {
|
|
|
|
|
ex.message = 'Could not parse XML. This likely means your import file is malformed.\n\n' + ex.message;
|
|
|
|
|
throw ex;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rssData = rootData.rss;
|
|
|
|
|
if (rssData === undefined) {
|
|
|
|
|
throw new Error('Could not find <rss> root node. This likely means your import file is malformed.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Data(rssData, 'rss');
|
|
|
|
|
}
|