262
src/libs/FilesFormats/XML.ts
Normal file
262
src/libs/FilesFormats/XML.ts
Normal file
@ -0,0 +1,262 @@
|
||||
import { objectMap } from '@dzeio/object-util'
|
||||
import xml2js from 'xml2js'
|
||||
|
||||
export interface Attribute {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
|
||||
/**
|
||||
* the name of the tag
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* The tag attributes
|
||||
*/
|
||||
attrs?: Array<Attribute | undefined>
|
||||
|
||||
/**
|
||||
* The tag childs
|
||||
*/
|
||||
childs?: Array<Tag | string | undefined>
|
||||
}
|
||||
|
||||
/**
|
||||
* xml2js tag interface not published for our options
|
||||
*/
|
||||
interface xml2jsTag {
|
||||
/**
|
||||
* the name of the tag (it is a private value because of our integration, but we still want to get it)
|
||||
*/
|
||||
// @ts-ignore see above
|
||||
// #name: string
|
||||
/**
|
||||
* the attributes record
|
||||
*/
|
||||
$?: Record<string, string>
|
||||
|
||||
/**
|
||||
* the possible text (only when #name === __text__)
|
||||
*/
|
||||
_?: string
|
||||
|
||||
/**
|
||||
* the tag Childs
|
||||
*/
|
||||
$$?: Array<xml2jsTag>
|
||||
}
|
||||
|
||||
export interface XMLOptions {
|
||||
|
||||
/**
|
||||
* if set (min: `true`)
|
||||
* it will render the content in a user readable form
|
||||
* (should not break compatibility (ODT excluded))
|
||||
*/
|
||||
pretty?: boolean | {
|
||||
|
||||
/**
|
||||
* the whitespace character(s)
|
||||
* default: '\t'
|
||||
*/
|
||||
whitespaceCharacter?: string
|
||||
|
||||
/**
|
||||
* the base number of whitespace character to use
|
||||
* default: 0
|
||||
*/
|
||||
baseCount?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* XML, the XML parser/stringifier that keep everything in order !
|
||||
*/
|
||||
export default class XML {
|
||||
|
||||
/**
|
||||
* Parse XML content to xml Tags
|
||||
* @param str the XML string
|
||||
*/
|
||||
public static async parse(str: string): Promise<Tag> {
|
||||
const xml: xml2jsTag = await xml2js.parseStringPromise(str, {
|
||||
charsAsChildren: true,
|
||||
explicitChildren: true,
|
||||
explicitRoot: false,
|
||||
|
||||
preserveChildrenOrder: true
|
||||
})
|
||||
return this.convert(xml)
|
||||
}
|
||||
|
||||
public static getAttribute(tag: Tag, key: string): string | undefined {
|
||||
if (!tag.attrs || tag.attrs.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return (tag.attrs.find((it) => it?.key === key))?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the Parsed XML back to XML
|
||||
* @param tag the root tag
|
||||
* @param options the options used
|
||||
*/
|
||||
public static async stringify(tag: Tag, options?: XMLOptions): Promise<string> {
|
||||
return this.stringifySync(tag, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the Parsed XML back to XML
|
||||
* @param tag the root tag
|
||||
* @param options the options used
|
||||
*/
|
||||
public static stringifySync(tag: Tag, options?: XMLOptions): string {
|
||||
const pretty = !!options?.pretty
|
||||
const baseCount = (typeof options?.pretty === 'object' && options?.pretty?.baseCount) || 0
|
||||
const whitespaceCharacter = (typeof options?.pretty === 'object' && options?.pretty?.whitespaceCharacter) || '\t'
|
||||
const hasChilds = Array.isArray(tag.childs) && tag.childs.length > 0
|
||||
let base = options?.pretty ? ''.padEnd(baseCount, whitespaceCharacter) : ''
|
||||
|
||||
if (!tag.name) {
|
||||
throw new Error('Tag name MUST be set')
|
||||
} else if (tag.name.includes(' ')) {
|
||||
throw new Error(`The tag name MUST not include spaces (${tag.name})`)
|
||||
}
|
||||
|
||||
// start of tag
|
||||
base += `<${tag.name}`
|
||||
|
||||
// add every attributes
|
||||
if (tag.attrs) {
|
||||
for (const attr of tag.attrs) {
|
||||
if (typeof attr === 'undefined') {
|
||||
continue
|
||||
}
|
||||
base += ` ${attr.key}`
|
||||
if (typeof attr.value === 'string') {
|
||||
base += `="${this.escape(attr.value)}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end of tag
|
||||
base += hasChilds ? '>' : '/>'
|
||||
|
||||
if (options?.pretty) {
|
||||
base += '\n'
|
||||
}
|
||||
|
||||
// process childs
|
||||
if (hasChilds) {
|
||||
for (const subTag of tag.childs!) {
|
||||
if (typeof subTag === 'undefined') {
|
||||
continue
|
||||
}
|
||||
if (typeof subTag === 'string') {
|
||||
if (pretty) {
|
||||
base += ''.padEnd(baseCount + 1, whitespaceCharacter)
|
||||
}
|
||||
base += this.escape(subTag)
|
||||
if (pretty) {
|
||||
base += '\n'
|
||||
}
|
||||
} else {
|
||||
base += this.stringifySync(subTag, pretty ? { pretty: { baseCount: baseCount + 1, whitespaceCharacter } } : undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end of tag
|
||||
if (hasChilds) {
|
||||
if (pretty) {
|
||||
base += ''.padEnd(baseCount, whitespaceCharacter)
|
||||
}
|
||||
base += `</${tag.name}>`
|
||||
if (pretty) {
|
||||
base += '\n'
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param it the element to validate
|
||||
* @returns {boolean} if {it} is of type {@link Tag}
|
||||
*/
|
||||
public static isTag(it: any): it is Tag {
|
||||
if (typeof it === 'object') {
|
||||
return 'name' in it
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public static findChild(tag: Tag, name: string): Tag | null {
|
||||
if (tag.name === name) {
|
||||
return tag
|
||||
} else if (tag.childs) {
|
||||
for (const child of tag.childs) {
|
||||
if (typeof child !== 'object') {
|
||||
continue
|
||||
}
|
||||
const found = this.findChild(child, name)
|
||||
if (found) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a xml2js tag to a XML.Tag
|
||||
* @param {xml2jsTag} tag the xml2js tag
|
||||
* @returns {Tag} the XML Tag
|
||||
*/
|
||||
private static convert(tag: xml2jsTag): Tag {
|
||||
const baseTag: Tag = {
|
||||
name: tag['#name']
|
||||
}
|
||||
|
||||
// convert XML2JS attributes to our attribut format
|
||||
// (Allowing users to keep order and to add items not only at the bottom)
|
||||
if (tag.$) {
|
||||
baseTag.attrs = objectMap(tag.$, (v, k) => ({ key: k, value: v }))
|
||||
}
|
||||
|
||||
// convert childs
|
||||
if (tag.$$) {
|
||||
baseTag.childs = tag.$$
|
||||
.map((subTag) => {
|
||||
// if child is a string
|
||||
if (subTag['#name'] === '__text__') {
|
||||
return subTag._
|
||||
}
|
||||
|
||||
// convert child
|
||||
return this.convert(subTag)
|
||||
})
|
||||
// filter empty items
|
||||
.filter((v) => !!v)
|
||||
}
|
||||
return baseTag
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape every XML characters
|
||||
* @param str the base string
|
||||
* @returns {string} the escaped string
|
||||
*/
|
||||
private static escape(str: string): string {
|
||||
return str
|
||||
.replace(/&/gu, '&')
|
||||
.replace(/"/gu, '"')
|
||||
.replace(/'/gu, ''')
|
||||
.replace(/</gu, '<')
|
||||
.replace(/>/gu, '>')
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user