@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
208 lines (178 loc) • 5.5 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as htmlparser2 from 'htmlparser2';
// TODO https://relaxng.org/relaxng.rng
// TODO namespaces
interface XmlClassType {
_xmlTextField?: string;
_elems: { [xmlElementTag: string]: {
fieldName: string,
classTypeName: string
}};
onOpenTag?(param: UnMarshaller, tagname: string, attributes: {[p: string]: string}): void;
onText?(param: UnMarshaller, text: string): void;
onCloseTag?(param: UnMarshaller, tagname: string): void;
}
interface StackPos<ValType> {
val: ValType;
classType: XmlClassType;
closeTag: string;
}
interface XmlElementDef {
fieldName: string;
classTypeName: string;
parentClassType?: any
isArray: boolean;
}
function createObj(classType, attributes: {[xmlAttrName: string]: string}) {
const obj = new classType();
if (classType._attrs) {
for (const xmlAttrName in attributes) {
const attr = classType._attrs[xmlAttrName];
if (attr) {
const value = attributes[xmlAttrName];
obj[attr.fieldName] = value;
}
}
}
return obj;
}
function addXmlHandlers(classType) {
classType.onOpenTag = (context: UnMarshaller, xmlElementTag: string, attributes: {[xmlAttrName: string]: string}) => {
if (!classType._elems) return;
const elemDef: XmlElementDef = classType._elems[xmlElementTag];
if (elemDef) {
if (!elemDef.classTypeName || typeof elemDef.classTypeName !== 'string') {
throw new Error('noClassType for tag: ' + xmlElementTag + ', fieldName: ' + elemDef.fieldName);
}
const subObj = createObj(context.getClass(elemDef.classTypeName), attributes);
const obj = context.top.val;
if (elemDef.isArray) {
if (!Array.isArray(obj[elemDef.fieldName])) {
obj[elemDef.fieldName] = [];
}
obj[elemDef.fieldName].push(subObj);
} else {
obj[elemDef.fieldName] = subObj;
}
context.push({
val: subObj,
classType: context.getClass(elemDef.classTypeName),
closeTag: xmlElementTag
});
}
};
classType.onCloseTag = (context: UnMarshaller, xmlElementTag: string) => {
if (context.top.closeTag === xmlElementTag) {
context.pop();
}
};
classType.onText = (context: UnMarshaller, text: string) => {
const fieldName = context.top.classType._xmlTextField;
if (fieldName) {
const obj = context.top.val;
const props = classType._xmlTextProps;
if (props.isArray) {
if (!obj[fieldName]) {
obj[fieldName] = [];
}
obj[fieldName].push(text);
} else {
if (!obj[fieldName]) {
obj[fieldName] = '';
}
obj[fieldName] += text;
}
}
};
}
export function XmlRootElement(xmlElementTag) {
return (classType) => {
classType._xmlElementTag = xmlElementTag;
addXmlHandlers(classType);
};
}
export function XmlElement() {
return (classType) => {
addXmlHandlers(classType);
};
}
export function XmlAttribute(xmlAttrName, fieldName) {
return (classType) => {
if (!classType._attrs) classType._attrs = {};
classType._attrs[xmlAttrName] = {
fieldName
};
};
}
export function XmlElementChild(xmlElementTag: string, fieldName: string, subClassTypeName: string, props: {isArray: boolean} = {isArray: false}) {
if (!subClassTypeName) {
throw new Error('No subClassType');
}
return (classType) => {
if (!classType._elems) classType._elems = {};
const xmlElemDef: XmlElementDef = {
fieldName,
classTypeName: subClassTypeName,
...props
};
classType._elems[xmlElementTag] = xmlElemDef;
};
}
export function XmlText(fieldName, props: {isArray: boolean} = {isArray: false}) {
return (classType) => {
classType._xmlTextField = fieldName;
classType._xmlTextProps = props;
};
}
function createRootStackPos(rootClassTypeName: string, rootClassType): StackPos<any> {
const val = { retVal: null };
const classType: XmlClassType = {
_elems: {
[rootClassType._xmlElementTag]: {
fieldName: 'retVal',
classTypeName: rootClassTypeName,
}
}
};
addXmlHandlers(classType);
return {
val,
classType,
closeTag: ''
};
}
export class UnMarshaller {
public readonly parser: htmlparser2.Parser;
private stack: StackPos<any>[] = [];
constructor(private classes: {[name: string]: any}, private rootClassTypeName: string) {
this.stack.push(createRootStackPos(this.rootClassTypeName, this.getClass(this.rootClassTypeName)));
this.parser = new htmlparser2.Parser({
onopentag: (tagname, attributes) => this.top.classType.onOpenTag(this, tagname, attributes),
ontext: (text) => this.top.classType.onText(this, text),
onclosetag: (tagname) => this.top.classType.onCloseTag(this, tagname),
}, { xmlMode: true });
}
get top(): StackPos<any> {
return this.stack[this.stack.length - 1];
}
public push(val: StackPos<any>) {
this.stack.push(val);
}
public pop() {
return this.stack.pop();
}
public unmarshal(content) {
if ('string' !== typeof content) {
content = new TextDecoder().decode(content);
}
this.parser.write(content);
this.parser.end();
return this.top.val['retVal'];
}
getClass(classTypeName: string) {
if (!this.classes[classTypeName]) {
throw new Error('No class registered: ' + classTypeName);
}
return this.classes[classTypeName];
}
}