From 0632d4ee44a576eb1e3a60555b5d05d1db5ab33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zg=C3=BCr=20=C3=96z=C3=A7=C4=B1tak?= Date: Fri, 17 Jul 2020 18:00:53 +0300 Subject: [PATCH] Add custom JS object parser. See #25 --- src/builder/XMLBuilderImpl.ts | 6 +-- src/interfaces.ts | 96 ++++++++++++++++++++++++++++++++--- src/readers/BaseReader.ts | 81 ++++++++++++++++++++--------- src/readers/JSONReader.ts | 9 ++-- src/readers/ObjectReader.ts | 32 ++---------- src/readers/XMLReader.ts | 9 ++-- test/readers/custom.test.ts | 35 +++++++++++++ 7 files changed, 198 insertions(+), 70 deletions(-) create mode 100644 test/readers/custom.test.ts diff --git a/src/builder/XMLBuilderImpl.ts b/src/builder/XMLBuilderImpl.ts index b63bd28..a7971ca 100644 --- a/src/builder/XMLBuilderImpl.ts +++ b/src/builder/XMLBuilderImpl.ts @@ -5,7 +5,7 @@ import { JSONWriterOptions, ObjectWriterOptions, MapWriterOptions } from "../interfaces" import { - applyDefaults, isObject, isString, isMap, isArray, isEmpty, isFunction, + applyDefaults, isObject, isString, isMap, isArray, isEmpty, getValue, forEachObject, forEachArray, isSet } from "@oozcitak/util" import { XMLWriter, MapWriter, ObjectWriter, JSONWriter } from "../writers" @@ -58,10 +58,10 @@ export class XMLBuilderImpl implements XMLBuilder { if (isString(p1) && /^\s* string) | undefined + /** + * Defines custom parser functions. + */ + parser: ParserOptions | undefined } /** @@ -131,7 +135,8 @@ export const DefaultBuilderOptions: XMLBuilderOptions = { svg: "http://www.w3.org/2000/svg", xlink: "http://www.w3.org/1999/xlink" }, - invalidCharReplacement: undefined + invalidCharReplacement: undefined, + parser: undefined } /** @@ -272,6 +277,85 @@ export interface ConvertOptions { comment: string } +/** + * Defines custom parser functions. + */ +export type ParserOptions = { + /** + * Main parser function which parses the given object and returns an XMLBuilder. + * + * @param node - node to recieve parsed content + * @param obj - object to parse + */ + parse?: (node: XMLBuilder, obj: string | ExpandObject) => XMLBuilder + + /** + * Creates a DocType node. + * The node will be skipped if the function returns `undefined`. + * + * @param name - node name + * @param publicId - public identifier + * @param systemId - system identifier + */ + docType?: (name: string, publicId: string, systemId: string) => XMLBuilder | undefined + + /** + * Creates a comment node. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param data - node data + */ + comment?: (parent: XMLBuilder, data: string) => XMLBuilder | undefined + + /** + * Creates a text node. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param data - node data + */ + text?: (parent: XMLBuilder, data: string) => XMLBuilder | undefined + + /** + * Creates a processing instruction node. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param target - instruction target + * @param data - node data + */ + instruction?: (parent: XMLBuilder, target: string, data: string) => XMLBuilder | undefined + + /** + * Creates a CData section node. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param data - node data + */ + cdata?: (parent: XMLBuilder, data: string) => XMLBuilder | undefined + + /** + * Creates an element node. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param name - node name + */ + element?: (parent: XMLBuilder, name: string) => XMLBuilder | undefined + + /** + * Creates an attribute or namespace declaration. + * The node will be skipped if the function returns `undefined`. + * + * @param parent - parent node + * @param name - node name + * @param value - node value + */ + attribute?: (parent: XMLBuilder, name: string, value: string) => XMLBuilder | undefined +} + /** * Defines the options passed to the writer. */ @@ -544,15 +628,15 @@ export interface XMLBuilder { */ ele(obj: ExpandObject): XMLBuilder - /** - * Creates new element nodes from the given JS object and appends it to the + /** + * Creates new element nodes from the given XML document string appends it to the * list of child nodes. * - * @param obj - a JS object representing nodes to insert + * @param content - an XML document string representing nodes to insert * * @returns the last top level element node created */ - ele(obj: ExpandObject, parser: ((obj: ExpandObject) => XMLBuilder)): XMLBuilder + ele(content: string): XMLBuilder /** * Removes this node from the XML document. @@ -1321,4 +1405,4 @@ type RecursivePartial = { T[P] extends (infer U)[] ? RecursivePartial[] : T[P] extends object ? RecursivePartial : T[P] -} \ No newline at end of file +} diff --git a/src/readers/BaseReader.ts b/src/readers/BaseReader.ts index a23f6b6..cb0981b 100644 --- a/src/readers/BaseReader.ts +++ b/src/readers/BaseReader.ts @@ -8,25 +8,49 @@ export abstract class BaseReader { protected _builderOptions: XMLBuilderOptions /** - * Initializes a new instance of `BaseWriter`. + * Initializes a new instance of `BaseReader`. * * @param builderOptions - XML builder options */ constructor(builderOptions: XMLBuilderOptions) { this._builderOptions = builderOptions + if (builderOptions.parser) { + Object.assign(this, builderOptions.parser) + } } abstract _parse(node: XMLBuilder, obj: U): XMLBuilder - abstract _docType(name: string, publicId: string, systemId: string): XMLBuilder | undefined - abstract _comment(parent: XMLBuilder, data: string): XMLBuilder | undefined - abstract _text(parent: XMLBuilder, data: string): XMLBuilder | undefined - abstract _instruction(parent: XMLBuilder, target: string, data: string): XMLBuilder | undefined - abstract _cdata(parent: XMLBuilder, data: string): XMLBuilder | undefined - abstract _element(parent: XMLBuilder, name: string): XMLBuilder | undefined - abstract _attribute(parent: XMLBuilder, name: string, value: string): XMLBuilder | undefined - - /** - * Produces an XML serialization of the given node. + + _docType(parent: XMLBuilder, name: string, publicId: string, systemId: string): XMLBuilder | undefined { + return parent.dtd({ name: name, pubID: publicId, sysID: systemId }) + } + + _comment(parent: XMLBuilder, data: string): XMLBuilder | undefined { + return parent.com(data) + } + + _text(parent: XMLBuilder, data: string): XMLBuilder | undefined { + return parent.txt(data) + } + + _instruction(parent: XMLBuilder, target: string, data: string): XMLBuilder | undefined { + return parent.ins(target, data) + } + + _cdata(parent: XMLBuilder, data: string): XMLBuilder | undefined { + return parent.dat(data) + } + + _element(parent: XMLBuilder, name: string): XMLBuilder | undefined { + return parent.ele(name) + } + + _attribute(parent: XMLBuilder, name: string, value: string): XMLBuilder | undefined { + return parent.att(name, value) + } + + /** + * Main parser function which parses the given object and returns an XMLBuilder. * * @param node - node to recieve parsed content * @param obj - object to parse @@ -36,18 +60,20 @@ export abstract class BaseReader { } /** - * Used by derived classes to create a DocType node. + * Creates a DocType node. + * The node will be skipped if the function returns `undefined`. * * @param name - node name * @param publicId - public identifier * @param systemId - system identifier */ - docType(name: string, publicId: string, systemId: string): XMLBuilder | undefined { - return this._docType(name, publicId, systemId) + docType(parent: XMLBuilder, name: string, publicId: string, systemId: string): XMLBuilder | undefined { + return this._docType(parent, name, publicId, systemId) } /** - * Used by derived classes to create a comment node. + * Creates a comment node. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param data - node data @@ -57,54 +83,59 @@ export abstract class BaseReader { } /** - * Used by derived classes to create a text node. + * Creates a text node. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param data - node data */ - text(parent: XMLBuilder, data: string) { + text(parent: XMLBuilder, data: string): XMLBuilder | undefined { return this._text(parent, data) } /** - * Used by derived classes to create a processing instruction node. + * Creates a processing instruction node. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param target - instruction target * @param data - node data */ - instruction(parent: XMLBuilder, target: string, data: string) { + instruction(parent: XMLBuilder, target: string, data: string): XMLBuilder | undefined { return this._instruction(parent, target, data) } /** - * Used by derived classes to create a CData section node. + * Creates a CData section node. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param data - node data */ - cdata(parent: XMLBuilder, data: string) { + cdata(parent: XMLBuilder, data: string): XMLBuilder | undefined { return this._cdata(parent, data) } /** - * Used by derived classes to create an element node. + * Creates an element node. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param name - node name */ - element(parent: XMLBuilder, name: string) { + element(parent: XMLBuilder, name: string): XMLBuilder | undefined { return this._element(parent, name) } /** - * Used by derived classes to create an attribute or namespace declaration. + * Creates an attribute or namespace declaration. + * The node will be skipped if the function returns `undefined`. * * @param parent - parent node * @param name - node name * @param value - node value */ - attribute(parent: XMLBuilder, name: string, value: string) { + attribute(parent: XMLBuilder, name: string, value: string): XMLBuilder | undefined { return this._attribute(parent, name, value) } diff --git a/src/readers/JSONReader.ts b/src/readers/JSONReader.ts index 28b42a6..de08d21 100644 --- a/src/readers/JSONReader.ts +++ b/src/readers/JSONReader.ts @@ -1,10 +1,11 @@ import { XMLBuilder } from "../interfaces" import { ObjectReader } from "./ObjectReader" +import { BaseReader } from "./BaseReader" /** - * Parses XML nodes from JSON string. + * Parses XML nodes from a JSON string. */ -export class JSONReader { +export class JSONReader extends BaseReader { /** * Parses the given document representation. @@ -12,7 +13,7 @@ export class JSONReader { * @param node - node receive parsed XML nodes * @param str - JSON string to parse */ - parse(node: XMLBuilder, str: string): XMLBuilder { - return new ObjectReader(node.options).parse(node, JSON.parse(str)) + _parse(node: XMLBuilder, str: string): XMLBuilder { + return new ObjectReader(this._builderOptions).parse(node, JSON.parse(str)) } } diff --git a/src/readers/ObjectReader.ts b/src/readers/ObjectReader.ts index f9a36d2..9a5bf96 100644 --- a/src/readers/ObjectReader.ts +++ b/src/readers/ObjectReader.ts @@ -1,46 +1,22 @@ -import { XMLBuilder, ExpandObject, XMLBuilderOptions } from "../interfaces" +import { XMLBuilder, ExpandObject } from "../interfaces" import { isArray, isString, isFunction, forEachArray, isSet, isMap, isObject, forEachObject, isEmpty } from "@oozcitak/util" -import { XMLBuilderImpl } from "../builder/XMLBuilderImpl" import { BaseReader } from "./BaseReader" /** * Parses XML nodes from objects and arrays. - * ES6 maps and sets are laso suupoted. + * ES6 maps and sets are also supoorted. */ export class ObjectReader extends BaseReader { - _docType(name: string, publicId: string, systemId: string): XMLBuilder | undefined { + /** @inheritdoc */ + docType(parent: XMLBuilder, name: string, publicId: string, systemId: string): XMLBuilder | undefined { // document type nodes cannot be represented in a JS object return undefined } - _comment(parent: XMLBuilder, data: string): XMLBuilder | undefined { - return parent.com(data) - } - - _text(parent: XMLBuilder, data: string): XMLBuilder | undefined { - return parent.txt(data) - } - - _instruction(parent: XMLBuilder, target: string, data: string): XMLBuilder | undefined { - return parent.ins(target, data) - } - - _cdata(parent: XMLBuilder, data: string): XMLBuilder | undefined { - return parent.dat(data) - } - - _element(parent: XMLBuilder, name: string): XMLBuilder | undefined { - return parent.ele(name) - } - - _attribute(parent: XMLBuilder, name: string, value: string): XMLBuilder | undefined { - return parent.att(name, value) - } - /** * Parses the given document representation. * diff --git a/src/readers/XMLReader.ts b/src/readers/XMLReader.ts index e961fd4..218044e 100644 --- a/src/readers/XMLReader.ts +++ b/src/readers/XMLReader.ts @@ -1,11 +1,12 @@ -import { XMLBuilder, XMLBuilderOptions } from "../interfaces" +import { XMLBuilder } from "../interfaces" import { createParser, sanitizeInput, throwIfParserError } from "../builder/dom" import { XMLBuilderImpl } from "../builder/XMLBuilderImpl" +import { BaseReader } from "./BaseReader" /** - * Parses XML nodes from JSON string. + * Parses XML nodes from an XML document string. */ -export class XMLReader { +export class XMLReader extends BaseReader { /** * Parses the given document representation. @@ -13,7 +14,7 @@ export class XMLReader { * @param node - node receive parsed XML nodes * @param str - XML document string to parse */ - parse(node: XMLBuilder, str: string): XMLBuilder { + _parse(node: XMLBuilder, str: string): XMLBuilder { const ele = node as any const options = node.options diff --git a/test/readers/custom.test.ts b/test/readers/custom.test.ts new file mode 100644 index 0000000..02bd92e --- /dev/null +++ b/test/readers/custom.test.ts @@ -0,0 +1,35 @@ +import $$ from '../TestHelpers' + +describe('custom parsers', () => { + + test('ObjectReader - skip comments', () => { + const obj = { + ele: 'element', + '!': 'comment' + } + + const doc = $$.create({ parser: { comment: () => undefined } }).ele('root').ele(obj).doc() + + expect($$.printTree(doc.node)).toBe($$.t` + root + ele + # element + `) + }) + + test('JSONReader - skip comments', () => { + const json = `{ + "ele": "element", + "!": "comment" + }` + + const doc = $$.create({ parser: { comment: () => undefined } }).ele('root').ele(json).doc() + + expect($$.printTree(doc.node)).toBe($$.t` + root + ele + # element + `) + }) + +})