@fnlb-project/stanza
Version:
Modern XMPP in the browser, with a JSON API
402 lines (401 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Types_1 = require("./Types");
class Translator {
constructor() {
this.parents = new Set();
this.placeholder = false;
this.typeField = '';
this.versionField = '';
this.defaultType = '';
this.defaultVersion = '';
this.languageField = 'lang';
this.typeValues = new Map();
this.typeOrders = new Map();
this.importers = new Map();
this.exporters = new Map();
this.children = new Map();
this.childrenIndex = new Map();
this.implicitChildren = new Set();
this.contexts = new Map();
}
addChild(name, translator, multiple = false, selector, implicit) {
const child = {
multiple: multiple || false,
name,
selector,
translator
};
const existingChild = this.children.get(name);
if (!existingChild) {
child.translator.parents.add(this);
this.children.set(name, child);
for (const [xid] of translator.importers) {
if (!this.implicitChildren.has(xid)) {
this.childrenIndex.set(xid, name);
}
}
if (implicit) {
this.implicitChildren.add(implicit);
}
return;
}
const existing = existingChild.translator;
existingChild.multiple = multiple;
if (selector && existingChild.selector && selector !== existingChild.selector) {
existingChild.selector = undefined;
}
for (const [xid, importer] of translator.importers) {
const [type, version] = (existing.typeValues.get(xid) || '').split('__v__');
existing.updateDefinition({
contexts: translator.contexts,
element: importer.element,
exporterOrdering: new Map(),
exporters: new Map(),
importerOrdering: importer.fieldOrders,
importers: importer.fields,
namespace: importer.namespace,
optionalNamespaces: new Map(),
type,
version
});
if (!this.implicitChildren.has(xid)) {
this.childrenIndex.set(xid, name);
}
}
for (const [exportType, exporter] of translator.exporters) {
const [type, version] = exportType.split('__v__');
existing.updateDefinition({
contexts: translator.contexts,
element: exporter.element,
exporterOrdering: exporter.fieldOrders,
exporters: exporter.fields,
importerOrdering: new Map(),
importers: new Map(),
namespace: exporter.namespace,
optionalNamespaces: exporter.optionalNamespaces,
type,
version
});
}
}
addContext(path, selector, field, xid, value, implied) {
if (selector) {
path = `${path}[${selector}]`;
}
let context = this.contexts.get(path);
if (!context) {
context = {
typeField: '',
versionField: '',
typeValues: new Map()
};
}
if (implied) {
context.impliedType = value;
}
context.typeField = field || '';
context.typeValues.set(xid, value);
this.contexts.set(path, context);
}
getChild(name) {
const child = this.children.get(name);
if (!child) {
return;
}
return child.translator;
}
getImportKey(xml) {
return this.childrenIndex.get(`{${xml.getNamespace()}}${xml.getName()}`);
}
updateDefinition(opts) {
const xid = `{${opts.namespace}}${opts.element}`;
const type = opts.type || this.defaultType;
const version = opts.version || this.defaultVersion;
const versionType = version ? `${type}__v__${version}` : type;
const importer = this.importers.get(xid) ||
{
element: opts.element,
fieldOrders: new Map(),
fields: new Map(),
namespace: opts.namespace
};
for (const [fieldName, fieldImporter] of opts.importers) {
importer.fields.set(fieldName, fieldImporter);
}
for (const [fieldName, order] of opts.importerOrdering) {
importer.fieldOrders.set(fieldName, order);
}
this.importers.set(xid, importer);
const exporter = this.exporters.get(versionType) ||
{
element: opts.element,
fieldOrders: new Map(),
fields: new Map(),
namespace: opts.namespace,
optionalNamespaces: opts.optionalNamespaces
};
for (const [fieldName, fieldExporter] of opts.exporters) {
exporter.fields.set(fieldName, fieldExporter);
}
for (const [name, order] of opts.exporterOrdering) {
exporter.fieldOrders.set(name, order);
}
for (const [prefix, namespace] of opts.optionalNamespaces) {
exporter.optionalNamespaces.set(prefix, namespace);
}
this.exporters.set(versionType, exporter);
for (const [path, newContext] of opts.contexts) {
const context = this.contexts.get(path) || {
impliedType: undefined,
typeField: newContext.typeField,
versionField: newContext.versionField,
typeValues: new Map()
};
if (!context.typeField) {
context.typeField = newContext.typeField;
}
if (!context.versionField) {
context.versionField = newContext.versionField;
}
if (!context.impliedType) {
context.impliedType = newContext.impliedType;
}
for (const [xid2, type] of newContext.typeValues) {
context.typeValues.set(xid2, type);
}
this.contexts.set(path, context);
}
if (opts.type) {
this.typeValues.set(xid, versionType);
if (opts.typeOrder && opts.type) {
this.typeOrders.set(opts.type, opts.typeOrder);
}
}
else if (this.typeField && !opts.type) {
for (const [, imp] of this.importers) {
for (const [fieldName, fieldImporter] of opts.importers) {
imp.fields.set(fieldName, fieldImporter);
}
for (const [fieldName, order] of opts.importerOrdering) {
imp.fieldOrders.set(fieldName, order);
}
}
for (const [, exp] of this.exporters) {
for (const [fieldName, fieldExporter] of opts.exporters) {
exp.fields.set(fieldName, fieldExporter);
}
for (const [fieldName, order] of opts.exporterOrdering) {
exp.fieldOrders.set(fieldName, order);
}
}
}
}
replaceWith(replacement) {
for (const [a, b] of this.children) {
replacement.children.set(a, b);
}
for (const [a, b] of this.childrenIndex) {
replacement.childrenIndex.set(a, b);
}
for (const [a, b] of this.contexts) {
replacement.contexts.set(a, b);
}
for (const a of this.implicitChildren) {
replacement.implicitChildren.add(a);
}
for (const parent of this.parents) {
for (const child of parent.children.values()) {
if (child.translator === this) {
child.translator = replacement;
}
}
}
this.parents = new Set();
}
import(xml, parentContext) {
const xid = `{${xml.getNamespace()}}${xml.getName()}`;
const output = {};
const importer = this.importers.get(xid);
if (!importer) {
return;
}
const versionTypeValue = this.typeValues.get(xid) || '';
const [typeValue, versionValue] = versionTypeValue.split('__v__');
const path = parentContext.path || '';
let implied;
if (parentContext.pathSelector) {
implied = this.contexts.get(`${path}[${parentContext.pathSelector}]`);
}
if (!implied) {
implied = this.contexts.get(path);
}
if (implied) {
if (!implied.impliedType) {
const impliedTypeValue = implied.typeValues.get(xid) || '';
if (impliedTypeValue) {
output[implied.typeField] = impliedTypeValue;
}
}
}
else if (this.typeField && typeValue && typeValue !== this.defaultType) {
output[this.typeField] = typeValue;
}
if (this.versionField && versionValue && versionValue !== this.defaultVersion) {
output[this.versionField] = versionValue;
}
const context = {
...parentContext,
data: output,
importer,
lang: (xml.getAttribute('xml:lang') || parentContext.lang || '').toLowerCase(),
pathSelector: typeValue,
translator: this
};
const importFields = [...importer.fieldOrders.entries()].sort((a, b) => a[1] > b[1] ? -1 : a[1] < b[1] ? 1 : 0);
const preChildren = importFields.filter(field => field[1] >= 0);
const postChildren = importFields.filter(field => field[1] < 0);
for (const [fieldName] of preChildren) {
const importField = importer.fields.get(fieldName);
context.path = `${parentContext.path}.${fieldName}`;
const value = importField(xml, context);
if (value !== null && value !== undefined) {
output[fieldName] = value;
}
}
for (const child of xml.children) {
if (typeof child === 'string') {
continue;
}
const childName = `{${child.getNamespace()}}${child.getName()}`;
const fieldName = this.childrenIndex.get(childName);
if (!fieldName) {
continue;
}
context.path = `${parentContext.path}.${fieldName}`;
const { translator, multiple, selector } = this.children.get(fieldName);
if (!selector || selector === typeValue) {
const childOutput = translator.import(child, context);
if (childOutput !== undefined) {
if (multiple) {
if (!output[fieldName]) {
output[fieldName] = [];
}
output[fieldName].push(childOutput);
}
else if (!output[fieldName]) {
output[fieldName] = childOutput;
}
else {
output[fieldName] = translator.resolveCollision(output[fieldName], childOutput);
}
}
}
}
for (const [fieldName] of postChildren) {
const importField = importer.fields.get(fieldName);
context.path = `${parentContext.path}.${fieldName}`;
const value = importField(xml, context);
if (value !== null && value !== undefined) {
output[fieldName] = value;
}
}
return output;
}
export(data, parentContext) {
if (!data) {
return;
}
let exportType = this.defaultType;
let exportVersion = this.defaultVersion;
const path = parentContext.path || '';
let implied;
if (parentContext.pathSelector) {
implied = this.contexts.get(`${path}[${parentContext.pathSelector}]`);
}
if (!implied) {
implied = this.contexts.get(path);
}
if (implied) {
exportType = implied.impliedType || data[implied.typeField] || this.defaultType;
}
else if (this.typeField) {
exportType = data[this.typeField] || this.defaultType;
}
if (this.versionField) {
exportVersion = data[this.versionField] || this.defaultVersion;
}
const exportVersionType = exportVersion ? `${exportType}__v__${exportVersion}` : exportType;
const exporter = this.exporters.get(exportVersionType);
if (!exporter) {
return;
}
const output = (0, Types_1.createElement)(exporter.namespace, exporter.element, parentContext.namespace, parentContext.element);
if (parentContext.element) {
output.parent = parentContext.element;
}
for (const [prefix, namespace] of exporter.optionalNamespaces) {
output.addOptionalNamespace(prefix, namespace);
}
const context = {
...parentContext,
data,
element: output,
exporter,
lang: (data[this.languageField] || parentContext.lang || '').toLowerCase(),
namespace: output.getDefaultNamespace(),
pathSelector: exportType,
translator: this
};
const langExporter = exporter.fields.get(this.languageField);
if (langExporter) {
langExporter(output, data[this.languageField], parentContext);
}
const keys = Object.keys(data);
keys.sort((key1, key2) => {
const a = exporter.fieldOrders.get(key1) || 100000;
const b = exporter.fieldOrders.get(key2) || 100000;
return a - b;
});
for (const key of keys) {
if (key === this.languageField) {
// We've already processed this field
continue;
}
const value = data[key];
const fieldExporter = exporter.fields.get(key);
if (fieldExporter) {
fieldExporter(output, value, context);
continue;
}
const childTranslator = this.children.get(key);
if (!childTranslator) {
continue;
}
context.path = `${parentContext.path ? parentContext.path + '.' : ''}${key}`;
const { translator, multiple, selector } = childTranslator;
if (!selector || selector === exportType) {
let items;
if (multiple) {
items = value !== null && value !== void 0 ? value : [];
}
else {
items = [value];
}
for (const item of items) {
const childOutput = translator.export(item, context);
if (childOutput) {
output.appendChild(childOutput);
}
}
}
}
return output;
}
resolveCollision(existingData, newData) {
const existingOrder = this.typeOrders.get(existingData[this.typeField] || this.defaultType) || 0;
const newOrder = this.typeOrders.get(newData[this.typeField] || this.defaultType) || 0;
return existingOrder <= newOrder ? existingData : newData;
}
}
exports.default = Translator;