├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src ├── collection.ts ├── const.ts ├── convert.ts ├── converters.ts ├── decorators.ts ├── error.ts ├── index.ts ├── namespace_manager.ts ├── types.ts ├── utils.ts ├── xml.ts ├── xml_collection.ts └── xml_object.ts ├── test ├── collection.ts ├── convert.ts ├── converters.ts ├── decorators.ts ├── error.ts ├── get_load.ts ├── namespace_manager.ts ├── object.ts ├── utils.ts └── xml_collection.ts ├── tsconfig.json ├── tsconfig.types.json ├── tslint.json └── yarn.lock /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+.[0-9]+.[0-9]+ 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: "16.x" 14 | - name: Install dependencies 15 | run: yarn 16 | - name: Build 17 | run: npm run build 18 | - name: Publish to NPM 19 | run: | 20 | npm set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 21 | npm publish --access public 22 | env: 23 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x, 16.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "::set-output name=dir::$(yarn cache dir)" 24 | 25 | - uses: actions/cache@v3 26 | id: yarn-cache 27 | with: 28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-yarn- 32 | 33 | - name: Install dependencies 34 | run: yarn 35 | 36 | - name: Run linter 37 | run: npm run lint 38 | 39 | - name: Run test with coverage 40 | run: npm run coverage 41 | 42 | # Fixes problem with incorrect SF paths. See https://github.com/coverallsapp/github-action/issues/125 43 | - name: Update lcov.info 44 | run: | 45 | sed -E "s/SF:(.+file:(.+))/SF:\2/g" ./coverage/lcov.info > coverage/lcov.new.info 46 | mv ./coverage/lcov.new.info ./coverage/lcov.info 47 | 48 | - name: Coveralls 49 | uses: coverallsapp/github-action@master 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /build 3 | /node_modules 4 | /.nyc_output 5 | /coverage 6 | /types 7 | /lib 8 | /dist 9 | /test/*.js 10 | /test/!tsconfig.js 11 | debug.log 12 | 13 | # Mac 14 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.circleci 3 | /node_modules 4 | /.nyc_output 5 | /coverage 6 | /src 7 | /test/*.ts 8 | /test/tsconfig.json 9 | /circle.yml 10 | /rollup*.js 11 | /ts*.json 12 | /**/*.js.map -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # xml-core 3 | 4 | `xml-core` is a set of classes that make it easier to work with XML within the browser and node. 5 | 6 | [![license](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/PeculiarVentures/xml-core/master/LICENSE) 7 | [![Test](https://github.com/PeculiarVentures/xml-core/actions/workflows/test.yml/badge.svg)](https://github.com/PeculiarVentures/xml-core/actions/workflows/test.yml) 8 | [![Coverage Status](https://coveralls.io/repos/github/PeculiarVentures/xml-core/badge.svg?branch=master)](https://coveralls.io/github/PeculiarVentures/xml-core?branch=master) 9 | [![NPM version](https://badge.fury.io/js/xml-core.png)](http://badge.fury.io/js/xml-core) 10 | 11 | [![NPM](https://nodei.co/npm/xml-core.png)](https://nodei.co/npm/xml-core/) 12 | 13 | 14 | ## Introduction 15 | 16 | We wanted to be able to validate [XAdES](https://en.wikipedia.org/wiki/XAdES) in the browser, specifically so we could validate the signature on the [EU Trust List](https://github.com/PeculiarVentures/tl-create). 17 | 18 | 19 | This lead us to the creation od [XMLDSIGjs](https://github.com/PeculiarVentures/xmldsigjs) which allows us to validate XML and [XAdESjs](https://github.com/PeculiarVentures/xadesjs) which extends it and enables us to validate XAdES signatures. 20 | 21 | We use `xml-core` to make the creation of these libraries easier, we hope you may find it valuable in your own projects also. 22 | 23 | Fundementally `xml-core` provides a way to transform XML to JSON and JSON to XML, which enables you to enforce a schema on the associated XML. The goal of this is to let you work naturally with XML in Javascript. 24 | 25 | It is similar to [xmljs](https://www.npmjs.com/package/xmljs) but has a few differences - 26 | - Can convert the JSON back to XML, 27 | - Uses [decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841#.61eut6wa9) to make enforcing schema in Javascript more natural. 28 | 29 | ## Install 30 | 31 | ``` 32 | npm install xml-core 33 | ``` 34 | 35 | ## Using 36 | 37 | ### ES5 38 | 39 | ```javascript 40 | var XmlCore = require("xml-core"); 41 | ``` 42 | 43 | ### ES2015 44 | 45 | ```javascript 46 | import XmlCore from "xml-core"; 47 | ``` 48 | 49 | ## Decrators 50 | 51 | Information about decorators [ES2015](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841#.qnl62mocp), [TypeScript](https://www.typescriptlang.org/docs/handbook/decorators.html) 52 | 53 | ### XmlElement 54 | 55 | Class decorator which allows to describe schema for xml element 56 | 57 | __Paramteres__ 58 | 59 | | Name | Description | 60 | |:----------------|:--------------------------------------------------------------------------------| 61 | | localName | Sets a local name for xml element. Default value is name of Class | 62 | | namespaceURI | Sets a namespace URI for xml element. Default value is `null` | 63 | | prefix | Sets a prefix for xml element. Default value is `null` | 64 | | parser | Sets a parser as `XmlObject` for each child element of `XmlCollection`. Optional| 65 | 66 | ### XmlAttribute 67 | 68 | Property decorator which allows to describe schema for attribute of xml element 69 | 70 | __Paramteres__ 71 | 72 | | Name | Description | 73 | |:----------------|:--------------------------------------------------------------------------------| 74 | | localName | Sets a local name for xml element. Default value is name of Property | 75 | | namespaceURI | Sets a namespace URI for xml element. Default value is `null` | 76 | | prefix | Sets a prefix for attribute of xml element. Default value is `null` | 77 | | defaultValue | Sets a default value for attribute of xml element. Optional | 78 | | required | Determines if attribute of xml element is required. Default value is `false` | 79 | | converter | Sets a specific converter for attribute of xml element. Default is simple text | 80 | 81 | 82 | ### XmlChildElement 83 | 84 | Property decorator which allows to describe schema for child element of xml element 85 | 86 | __Paramteres__ 87 | 88 | | Name | Description | 89 | |:----------------|:--------------------------------------------------------------------------------| 90 | | localName | Sets local name for xml element. Default value is name of Class | 91 | | namespaceURI | Sets namespace URI for xml element. Default value is `null` | 92 | | prefix | Sets prefix for xml element. Default value is `null` | 93 | | defaultValue | Sets a default value for attribute of xml element. Optional | 94 | | required | Determines if child element is required. Default value is `false` | 95 | | converter | Sets a specific converter for child element. Default is simple text | 96 | | parser | Sets parser as `XmlObject` for child element. Optional | 97 | | minOccurs | Sets a min value for child element occurs. Default value is `0` | 98 | | maxOccurs | Sets a max value for child element occurs. Default value is `MAX` | 99 | | noRoot | Determines if parser as `XmlCollection` must return it's children to parent element | 100 | 101 | ### XmlContent 102 | 103 | Property decorator which allows to describe schema for content of xml element 104 | 105 | __Paramteres__ 106 | 107 | | Name | Description | 108 | |-----------------|---------------------------------------------------------------------------------| 109 | | defaultValue | Sets a default value for content of xml element. Optional | 110 | | required | Determines if content of xml element is required. Default value is `false` | 111 | | converter | Sets a specific converter for content of xml element. Default is simple text | 112 | 113 | ## XmlObject 114 | 115 | Base class for XML elements. 116 | 117 | ### LoadXml 118 | 119 | Reads XML from string 120 | 121 | ```typescript 122 | LoadXml(node: Node | string): void; 123 | static LoadXml(node: Node | string): this; 124 | ``` 125 | 126 | ### GetXml 127 | 128 | Writes object to XML node 129 | 130 | ```typescript 131 | GetXml(): Node | null; 132 | ``` 133 | 134 | ### toString 135 | 136 | Writes object to string 137 | 138 | ``` 139 | toString(): string; 140 | ``` 141 | 142 | __Example__ 143 | 144 | Target XML [schema]() 145 | 146 | ```xml 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | ``` 158 | 159 | TypeScript implementation of XML schema 160 | 161 | ```typescript 162 | import { XmlObject, XmlBase64Converter } from "xml-core"; 163 | 164 | @XmlElement({ 165 | localName: "Signature", 166 | namespaceURI: "http://www.w3.org/2000/09/xmldsig#", 167 | prefix: "ds" 168 | }) 169 | class Signature extends XmlObject { 170 | 171 | @XmlAttribute({ 172 | localName: XmlSignature.AttributeNames.Id, 173 | defaultValue: "", 174 | }) 175 | public Id: string; 176 | 177 | @XmlChildElement({ 178 | parser: SignedInfo, 179 | required: true, 180 | }) 181 | public SignedInfo: SignedInfo; 182 | 183 | @XmlChildElement({ 184 | localName: "SignatureValue", 185 | namespaceURI: "http://www.w3.org/2000/09/xmldsig#", 186 | prefix: "ds", 187 | required: true, 188 | converter: XmlBase64Converter, 189 | defaultValue: null, 190 | }) 191 | public SignatureValue: Uint8Array | null; 192 | 193 | @XmlChildElement({ 194 | parser: KeyInfo 195 | }) 196 | public KeyInfo: KeyInfo; 197 | 198 | @XmlChildElement({ 199 | parser: DataObjects, 200 | noRoot: true 201 | }) 202 | public ObjectList: DataObjects; 203 | 204 | } 205 | ``` 206 | 207 | __Using__ 208 | 209 | ```typescript 210 | const signature = new Signature(); 211 | 212 | // Read XML 213 | signature.LoadXml(Signature.Parse('...')); 214 | console.log("Id:", signature.Id); // Id: sigId 215 | 216 | // Write XML 217 | signature.Id = "newId"; 218 | console.log(signature.toString()); // ... 219 | ``` 220 | 221 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xml-core", 3 | "version": "1.1.5", 4 | "description": "`xml-core` is a set of classes that make it easier to work with XML within the browser and node.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.es.js", 7 | "types": "dist/types/index.d.ts", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "dependencies": { 12 | "@xmldom/xmldom": "^0.8.2", 13 | "tslib": "^2.4.0", 14 | "xpath.js": "^1.1.0" 15 | }, 16 | "devDependencies": { 17 | "@types/mocha": "^9.1.1", 18 | "@types/node": "^18.6.3", 19 | "coveralls": "^3.1.1", 20 | "mocha": "^10.0.0", 21 | "nyc": "^15.1.0", 22 | "rimraf": "^3.0.2", 23 | "rollup": "^2.77.2", 24 | "rollup-plugin-typescript2": "^0.32.1", 25 | "ts-node": "^10.9.1", 26 | "tslint": "^6.1.3", 27 | "typescript": "^4.7.4" 28 | }, 29 | "scripts": { 30 | "test": "mocha", 31 | "upgrade": "yarn upgrade-interactive --latest", 32 | "prepare": "npm run build", 33 | "clear": "rimraf dist/", 34 | "build": "npm run build:module && npm run build:types", 35 | "build:module": "rollup -c", 36 | "build:types": "tsc -p tsconfig.types.json", 37 | "rebuild": "npm run clear && npm run build", 38 | "lint": "tslint 'src/**/*.ts'", 39 | "pub": "npm run pub:patch", 40 | "pub:major": "npm version major", 41 | "pub:minor": "npm version minor", 42 | "pub:patch": "npm version patch", 43 | "postpub": "git push --follow-tags", 44 | "coverage": "nyc npm test", 45 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/PeculiarVentures/xml-core.git" 50 | }, 51 | "keywords": [ 52 | "xml" 53 | ], 54 | "author": "microshine", 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/PeculiarVentures/xml-core.git/issues" 58 | }, 59 | "homepage": "https://github.com/PeculiarVentures/xml-core.git#readme", 60 | "nyc": { 61 | "extension": [ 62 | ".ts", 63 | ".tsx" 64 | ], 65 | "include": [ 66 | "src/**/*.ts" 67 | ], 68 | "exclude": [ 69 | "**/*.d.ts" 70 | ], 71 | "reporter": [ 72 | "text-summary", 73 | "lcov" 74 | ] 75 | }, 76 | "mocha": { 77 | "require": "ts-node/register", 78 | "extension": [ 79 | "ts" 80 | ], 81 | "watch-files": [ 82 | "test/**/*.ts" 83 | ] 84 | }, 85 | "resolutions": { 86 | "ansi-regex": "^5.0.1", 87 | "path-parse": "^1.0.7", 88 | "browserslist": "^4.16.5", 89 | "json-schema": "^0.4.0", 90 | "minimist": "^1.2.6" 91 | } 92 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import pkg from "./package.json"; 3 | 4 | const banner = [].join("\n"); 5 | const input = "src/index.ts"; 6 | const external = Object.keys(pkg.dependencies); 7 | 8 | export default { 9 | input, 10 | plugins: [ 11 | typescript({ 12 | check: true, 13 | clean: true, 14 | tsconfigOverride: { 15 | compilerOptions: { 16 | removeComments: true, 17 | module: "ES2015", 18 | } 19 | } 20 | }), 21 | ], 22 | external, 23 | output: [ 24 | { 25 | banner, 26 | file: pkg.main, 27 | format: "cjs", 28 | }, 29 | { 30 | banner, 31 | file: pkg.module, 32 | format: "es", 33 | }, 34 | ], 35 | }; -------------------------------------------------------------------------------- /src/collection.ts: -------------------------------------------------------------------------------- 1 | import { ICollection } from "./types"; 2 | 3 | export class Collection implements ICollection { 4 | 5 | protected items: I[] = new Array(); 6 | 7 | constructor(items?: I[]) { 8 | if (items) { 9 | this.items = items; 10 | } 11 | } 12 | 13 | public get Count() { 14 | return this.items.length; 15 | } 16 | 17 | public Item(index: number): I | null { 18 | return this.items[index] || null; 19 | } 20 | 21 | public Add(item: I) { 22 | this.items.push(item); 23 | } 24 | 25 | public Pop() { 26 | return this.items.pop(); 27 | } 28 | 29 | public RemoveAt(index: number) { 30 | this.items = this.items.filter((item, index2) => index2 !== index); 31 | } 32 | 33 | public Clear() { 34 | this.items = new Array(); 35 | } 36 | 37 | public GetIterator() { 38 | return this.items; 39 | } 40 | 41 | public ForEach(cb: (item: I, index: number, array: I[]) => void) { 42 | this.GetIterator().forEach(cb); 43 | } 44 | 45 | public Map(cb: (item: I, index: number, array: I[]) => U) { 46 | return new Collection(this.GetIterator().map(cb)); 47 | } 48 | 49 | public Filter(cb: (item: I, index: number, array: I[]) => boolean) { 50 | return new Collection(this.GetIterator().filter(cb)); 51 | } 52 | 53 | public Sort(cb: (a: I, b: I) => number) { 54 | return new Collection(this.GetIterator().sort(cb)); 55 | } 56 | 57 | public Every(cb: (value: I, index: number, array: I[]) => boolean) { 58 | return this.GetIterator().every(cb); 59 | } 60 | 61 | public Some(cb: (value: I, index: number, array: I[]) => boolean) { 62 | return this.GetIterator().some(cb); 63 | } 64 | 65 | public IsEmpty() { 66 | return this.Count === 0; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const ELEMENT = "element"; 2 | export const ATTRIBUTE = "attribute"; 3 | export const CONTENT = "content"; 4 | -------------------------------------------------------------------------------- /src/convert.ts: -------------------------------------------------------------------------------- 1 | import { XE, XmlError } from "./error"; 2 | import { XmlBufferEncoding } from "./types"; 3 | 4 | declare let unescape: any; 5 | declare let escape: any; 6 | 7 | export class Convert { 8 | 9 | public static ToString(buffer: BufferSource, enc: XmlBufferEncoding = "utf8") { 10 | const buf = new Uint8Array(buffer as ArrayBuffer); 11 | switch (enc.toLowerCase()) { 12 | case "utf8": 13 | return this.ToUtf8String(buf); 14 | case "binary": 15 | return this.ToBinary(buf); 16 | case "hex": 17 | return this.ToHex(buf); 18 | case "base64": 19 | return this.ToBase64(buf); 20 | case "base64url": 21 | return this.ToBase64Url(buf); 22 | default: 23 | throw new XmlError(XE.CONVERTER_UNSUPPORTED); 24 | } 25 | } 26 | 27 | public static FromString(str: string, enc: XmlBufferEncoding = "utf8"): Uint8Array { 28 | switch (enc.toLowerCase()) { 29 | case "utf8": 30 | return this.FromUtf8String(str); 31 | case "binary": 32 | return this.FromBinary(str); 33 | case "hex": 34 | return this.FromHex(str); 35 | case "base64": 36 | return this.FromBase64(str); 37 | case "base64url": 38 | return this.FromBase64Url(str); 39 | default: 40 | throw new XmlError(XE.CONVERTER_UNSUPPORTED); 41 | } 42 | } 43 | 44 | public static ToBase64(buf: Uint8Array): string { 45 | if (typeof btoa !== "undefined") { 46 | const binary = this.ToString(buf, "binary"); 47 | return btoa(binary); 48 | } else if (typeof Buffer !== "undefined") { 49 | return Buffer.from(buf).toString("base64"); 50 | } else { 51 | throw new XmlError(XE.CONVERTER_UNSUPPORTED); 52 | } 53 | } 54 | 55 | public static FromBase64(base64Text: string): Uint8Array { 56 | // Prepare string 57 | base64Text = base64Text.replace(/\n/g, "").replace(/\r/g, "").replace(/\t/g, "").replace(/\s/g, ""); 58 | if (typeof atob !== "undefined") { 59 | return this.FromBinary(atob(base64Text)); 60 | } else if (typeof Buffer !== "undefined") { 61 | return new Uint8Array(Buffer.from(base64Text, "base64")); 62 | } else { 63 | throw new XmlError(XE.CONVERTER_UNSUPPORTED); 64 | } 65 | } 66 | 67 | public static FromBase64Url(base64url: string): Uint8Array { 68 | return this.FromBase64(this.Base64Padding(base64url.replace(/\-/g, "+").replace(/\_/g, "/"))); 69 | } 70 | 71 | public static ToBase64Url(data: Uint8Array): string { 72 | return this.ToBase64(data).replace(/\+/g, "-").replace(/\//g, "_").replace(/\=/g, ""); 73 | } 74 | 75 | public static FromUtf8String(text: string): Uint8Array { 76 | const s = unescape(encodeURIComponent(text)); 77 | const uintArray = new Uint8Array(s.length); 78 | for (let i = 0; i < s.length; i++) { 79 | uintArray[i] = s.charCodeAt(i); 80 | } 81 | return uintArray; 82 | } 83 | public static ToUtf8String(buffer: Uint8Array): string { 84 | const encodedString = String.fromCharCode.apply(null, buffer as any); 85 | const decodedString = decodeURIComponent(escape(encodedString)); 86 | return decodedString; 87 | } 88 | 89 | public static FromBinary(text: string): Uint8Array { 90 | const stringLength = text.length; 91 | const resultView = new Uint8Array(stringLength); 92 | for (let i = 0; i < stringLength; i++) { 93 | resultView[i] = text.charCodeAt(i); 94 | } 95 | return resultView; 96 | } 97 | public static ToBinary(buffer: Uint8Array): string { 98 | let resultString = ""; 99 | for (let i = 0; i < buffer.length; i++) { 100 | resultString = resultString + String.fromCharCode(buffer[i]); 101 | } 102 | return resultString; 103 | } 104 | 105 | /** 106 | * Converts buffer to HEX string 107 | * @param {BufferSource} buffer Incoming buffer 108 | * @returns string 109 | */ 110 | public static ToHex(buffer: Uint8Array): string { 111 | const splitter = ""; 112 | const res: string[] = []; 113 | for (let i = 0; i < buffer.length; i++) { 114 | const char = buffer[i].toString(16); 115 | res.push(char.length === 1 ? "0" + char : char); 116 | } 117 | return res.join(splitter); 118 | } 119 | 120 | /** 121 | * Converts HEX string to buffer 122 | * 123 | * @static 124 | * @param {string} hexString 125 | * @returns {Uint8Array} 126 | * 127 | * @memberOf Convert 128 | */ 129 | public static FromHex(hexString: string): Uint8Array { 130 | const res = new Uint8Array(hexString.length / 2); 131 | for (let i = 0; i < hexString.length; i = i + 2) { 132 | const c = hexString.slice(i, i + 2); 133 | res[i / 2] = parseInt(c, 16); 134 | } 135 | return res; 136 | } 137 | 138 | /** 139 | * Converts string to Date 140 | * 141 | * @static 142 | * @param {string} dateTime 143 | * @returns {Date} 144 | * 145 | * @memberOf Convert 146 | */ 147 | public static ToDateTime(dateTime: string): Date { 148 | return new Date(dateTime); 149 | } 150 | 151 | /** 152 | * Converts Date to string 153 | * 154 | * @static 155 | * @param {Date} dateTime 156 | * @returns {string} 157 | * 158 | * @memberOf Convert 159 | */ 160 | public static FromDateTime(dateTime: Date): string { 161 | const str = dateTime.toISOString(); 162 | return str; 163 | } 164 | 165 | protected static Base64Padding(base64: string): string { 166 | const padCount = 4 - (base64.length % 4); 167 | if (padCount < 4) { 168 | for (let i = 0; i < padCount; i++) { 169 | base64 += "="; 170 | } 171 | } 172 | return base64; 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/converters.ts: -------------------------------------------------------------------------------- 1 | import { Convert } from "./convert"; 2 | import { IConverter } from "./types"; 3 | 4 | export const XmlBase64Converter: IConverter = { 5 | get: (value: Uint8Array) => { 6 | if (value) { 7 | return Convert.ToBase64(value); 8 | } 9 | return void 0; 10 | }, 11 | set: (value: string) => { 12 | return Convert.FromBase64(value); 13 | }, 14 | }; 15 | 16 | export const XmlNumberConverter: IConverter = { 17 | get: (value: number) => { 18 | if (value) { 19 | return value.toString(); 20 | } 21 | return "0"; 22 | }, 23 | set: (value: string) => { 24 | return Number(value); 25 | }, 26 | }; 27 | 28 | export const XmlBooleanConverter: IConverter = { 29 | get: (value: boolean) => { 30 | if (value) { 31 | return value.toString(); 32 | } 33 | return "false"; 34 | }, 35 | set: (value: string) => { 36 | if (value && value.toLowerCase() === "true") { 37 | return true; 38 | } else { 39 | return false; 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/decorators.ts: -------------------------------------------------------------------------------- 1 | import * as CONST from "./const"; 2 | import { XmlAttributeType, XmlChildElementType, XmlContentType, XmlElementType, XmlSchema } from "./types"; 3 | 4 | const MAX = 1e9; 5 | 6 | function assign(target: any, ...sources: any[]) { 7 | const res = arguments[0]; 8 | for (let i = 1; i < arguments.length; i++) { 9 | const obj = arguments[i]; 10 | for (const prop in obj) { 11 | if (!obj.hasOwnProperty(prop)) { 12 | continue; 13 | } 14 | res[prop] = obj[prop]; 15 | } 16 | } 17 | return res; 18 | } 19 | 20 | export function XmlElement(params: XmlElementType) { 21 | return (target: TFunction) => { 22 | const t = target as XmlSchema; 23 | 24 | t.localName = params.localName || (t as any).name; 25 | t.namespaceURI = params.namespaceURI || t.namespaceURI || null; 26 | t.prefix = params.prefix || t.prefix || null; 27 | t.parser = params.parser || t.parser; 28 | if (t.target !== t) { 29 | t.items = assign({}, t.items); 30 | } 31 | t.target = target; 32 | }; 33 | } 34 | 35 | export function XmlChildElement(params: XmlChildElementType = {}) { 36 | return (target: Object, propertyKey: string | symbol) => { 37 | const t = target.constructor as XmlSchema; 38 | const key = propertyKey as string; 39 | 40 | if (!t.items) { 41 | t.items = {}; 42 | } 43 | 44 | if (t.target !== t) { 45 | t.items = assign({}, t.items); 46 | } 47 | t.target = target; 48 | 49 | if (params.parser) { 50 | t.items![key] = { 51 | parser: params.parser, 52 | required: params.required || false, 53 | maxOccurs: params.maxOccurs || MAX, 54 | minOccurs: params.minOccurs === void 0 ? 0 : params.minOccurs, 55 | noRoot: params.noRoot || false, 56 | }; 57 | } else { 58 | t.items![key] = { 59 | namespaceURI: params.namespaceURI || null, 60 | required: params.required || false, 61 | prefix: params.prefix || null, 62 | defaultValue: params.defaultValue, 63 | converter: params.converter, 64 | noRoot: params.noRoot || false, 65 | }; 66 | } 67 | params.localName = params.localName || (params.parser && (params.parser as any).localName) || key; 68 | t.items![key].namespaceURI = params.namespaceURI || (params.parser && (params.parser as any).namespaceURI) || null; 69 | t.items![key].prefix = params.prefix || (params.parser && (params.parser as any).prefix) || null; 70 | t.items![key].localName = params.localName; 71 | t.items![key].type = CONST.ELEMENT; 72 | 73 | defineProperty(target, key, params); 74 | }; 75 | } 76 | 77 | export function XmlAttribute(params: XmlAttributeType = { required: false, namespaceURI: null }) { 78 | return (target: Object, propertyKey: string) => { 79 | const t = target.constructor as XmlSchema; 80 | const key = propertyKey as string; 81 | 82 | if (!params.localName) { 83 | params.localName = propertyKey as string; 84 | } 85 | 86 | if (!t.items) { 87 | t.items = {}; 88 | } 89 | 90 | if (t.target !== t) { 91 | t.items = assign({}, t.items); 92 | } 93 | t.target = target; 94 | 95 | t.items![propertyKey] = params; 96 | t.items![propertyKey].type = CONST.ATTRIBUTE; 97 | 98 | defineProperty(target, key, params); 99 | }; 100 | } 101 | 102 | export function XmlContent(params: XmlContentType = { required: false }) { 103 | return (target: Object, propertyKey: string) => { 104 | const t = target.constructor as XmlSchema; 105 | const key = propertyKey as string; 106 | 107 | if (!t.items) { 108 | t.items = {}; 109 | } 110 | 111 | if (t.target !== t) { 112 | t.items = assign({}, t.items); 113 | } 114 | t.target = target; 115 | 116 | t.items![propertyKey] = params; 117 | t.items![propertyKey].type = CONST.CONTENT; 118 | 119 | defineProperty(target, key, params); 120 | }; 121 | } 122 | 123 | function defineProperty(target: any, key: string, params: any) { 124 | const key2 = `_${key}`; 125 | 126 | const opt = { 127 | set: function (this: any, v: any) { 128 | if (this[key2] !== v) { 129 | this.element = null; 130 | this[key2] = v; 131 | } 132 | }, 133 | get: function (this: any) { 134 | if (this[key2] === void 0) { 135 | let defaultValue = params.defaultValue; 136 | if (params.parser) { 137 | defaultValue = new params.parser(); 138 | defaultValue.localName = params.localName; 139 | } 140 | this[key2] = defaultValue; 141 | } 142 | return this[key2]; 143 | }, 144 | }; 145 | 146 | // private property 147 | Object.defineProperty(target, key2, { writable: true, enumerable: false }); 148 | // public property 149 | Object.defineProperty(target, key, opt); 150 | } 151 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | interface PrintfArgs { 2 | arg: string; 3 | index: number; 4 | } 5 | 6 | function printf(text: string, ...args: any[]) { 7 | let msg: string = text; 8 | const regFind = /[^%](%\d+)/g; 9 | let match: RegExpExecArray | null = null; 10 | const matches: PrintfArgs[] = []; 11 | while (match = regFind.exec(msg)) { 12 | matches.push({ arg: match[1], index: match.index }); 13 | } 14 | 15 | // replace matches 16 | for (let i = matches.length - 1; i >= 0; i--) { 17 | const item = matches[i]; 18 | const arg = item.arg.substring(1); 19 | const index = item.index + 1; 20 | msg = msg.substring(0, index) + arguments[+arg] + msg.substring(index + 1 + arg.length); 21 | } 22 | 23 | // convert %% -> % 24 | msg = msg.replace("%%", "%"); 25 | 26 | return msg; 27 | } 28 | 29 | function padNum(num: number, size: number): string { 30 | let s = num + ""; 31 | while (s.length < size) { 32 | s = "0" + s; 33 | } 34 | return s; 35 | } 36 | 37 | export class XmlError implements Error { 38 | public stack: any; 39 | public code: number; 40 | public name: string; 41 | public message: string; 42 | protected readonly prefix = "XMLJS"; 43 | constructor(code: XE, ...args: any[]) { 44 | this.code = code; 45 | this.name = (this.constructor as any).name; 46 | arguments[0] = xes[code]; 47 | const message = printf.apply(this, arguments as any); 48 | this.message = `${this.prefix}${padNum(code, 4)}: ${message}`; 49 | this.stack = (new Error(this.message) as any).stack; 50 | } 51 | } 52 | 53 | export enum XE { 54 | NONE, 55 | NULL_REFERENCE, 56 | NULL_PARAM, 57 | DECORATOR_NULL_PARAM, 58 | COLLECTION_LIMIT, 59 | METHOD_NOT_IMPLEMENTED, 60 | METHOD_NOT_SUPPORTED, 61 | PARAM_REQUIRED, 62 | CONVERTER_UNSUPPORTED, 63 | ELEMENT_MALFORMED, 64 | ELEMENT_MISSING, 65 | ATTRIBUTE_MISSING, 66 | CONTENT_MISSING, 67 | CRYPTOGRAPHIC, 68 | CRYPTOGRAPHIC_NO_MODULE, 69 | CRYPTOGRAPHIC_UNKNOWN_TRANSFORM, 70 | ALGORITHM_NOT_SUPPORTED, 71 | ALGORITHM_WRONG_NAME, 72 | XML_EXCEPTION, 73 | } 74 | 75 | interface IXmlError { 76 | [index: number]: string; 77 | } 78 | 79 | const xes: IXmlError = {}; 80 | xes[XE.NONE] = "No description"; 81 | xes[XE.NULL_REFERENCE] = "Null reference"; 82 | xes[XE.NULL_PARAM] = "'%1' has empty '%2' object"; 83 | xes[XE.DECORATOR_NULL_PARAM] = "Decorator '%1' has empty '%2' parameter"; 84 | xes[XE.COLLECTION_LIMIT] = "Collection of '%1' in element '%2' has wrong amount of items"; 85 | xes[XE.METHOD_NOT_IMPLEMENTED] = "Method is not implemented"; 86 | xes[XE.METHOD_NOT_SUPPORTED] = "Method is not supported"; 87 | xes[XE.PARAM_REQUIRED] = "Required parameter is missing '%1'"; 88 | xes[XE.CONVERTER_UNSUPPORTED] = "Converter is not supported"; 89 | xes[XE.ELEMENT_MALFORMED] = "Malformed element '%1'"; 90 | xes[XE.ELEMENT_MISSING] = "Element '%1' is missing in '%2'"; 91 | xes[XE.ATTRIBUTE_MISSING] = "Attribute '%1' is missing in '%2'"; 92 | xes[XE.CONTENT_MISSING] = "Content is missing in '%1'"; 93 | xes[XE.CRYPTOGRAPHIC] = "Cryptographic error: %1"; 94 | xes[XE.CRYPTOGRAPHIC_NO_MODULE] = "WebCrypto module is not found"; 95 | xes[XE.CRYPTOGRAPHIC_UNKNOWN_TRANSFORM] = "Unknown transform %1"; 96 | xes[XE.ALGORITHM_NOT_SUPPORTED] = "Algorithm is not supported '%1'"; 97 | xes[XE.ALGORITHM_WRONG_NAME] = "Algorithm wrong name in use '%1'"; 98 | xes[XE.XML_EXCEPTION] = "XML exception: %1"; 99 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./decorators"; 3 | export * from "./collection"; 4 | export * from "./convert"; 5 | export * from "./utils"; 6 | export * from "./converters"; 7 | export * from "./error"; 8 | export * from "./xml"; 9 | export * from "./xml_collection"; 10 | export * from "./xml_object"; 11 | export * from "./namespace_manager"; 12 | -------------------------------------------------------------------------------- /src/namespace_manager.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "./collection"; 2 | import { XmlNamespace } from "./types"; 3 | 4 | export class NamespaceManager extends Collection { 5 | 6 | public Add(item: XmlNamespace) { 7 | item.prefix = item.prefix || ""; 8 | item.namespace = item.namespace || ""; 9 | super.Add(item); 10 | } 11 | 12 | public GetPrefix(prefix: string, start: number = this.Count - 1): XmlNamespace | null { 13 | const lim = this.Count - 1; 14 | prefix = prefix || ""; 15 | if (start > lim) { 16 | start = lim; 17 | } 18 | for (let i = start; i >= 0; i--) { 19 | const item = this.items[i]; 20 | if (item.prefix === prefix) { 21 | return item; 22 | } 23 | } 24 | return null; 25 | } 26 | 27 | public GetNamespace(namespaceUrl: string, start: number = this.Count - 1): XmlNamespace | null { 28 | const lim = this.Count - 1; 29 | namespaceUrl = namespaceUrl || ""; 30 | if (start > lim) { 31 | start = lim; 32 | } 33 | for (let i = start; i >= 0; i--) { 34 | const item = this.items[i]; 35 | if (item.namespace === namespaceUrl) { 36 | return item; 37 | } 38 | } 39 | return null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base interface for collections 3 | * 4 | * @interface ICollection 5 | * @template I 6 | */ 7 | export interface ICollection { 8 | readonly Count: number; 9 | Item(index: number): I | null; 10 | Add(item: I): void; 11 | Pop(): I | undefined; 12 | RemoveAt(index: number): void; 13 | Clear(): void; 14 | GetIterator(): I[]; 15 | ForEach(cb: (item: I, index: number, array: I[]) => void): void; 16 | Map(cb: (item: I, index: number, array: I[]) => U): ICollection; 17 | Filter(cb: (item: I, index: number, array: I[]) => boolean): ICollection; 18 | Sort(cb: (a: I, b: I) => number): ICollection; 19 | Every(cb: (value: I, index: number, array: I[]) => boolean): boolean; 20 | Some(cb: (value: I, index: number, array: I[]) => boolean): boolean; 21 | IsEmpty(): boolean; 22 | } 23 | 24 | export interface IXmlSerializable { 25 | 26 | /** 27 | * Writes object to XML node 28 | * - if class was initialized and it has no one change, GetXml returns null 29 | * @returns Node 30 | */ 31 | GetXml(): Node | null; 32 | /** 33 | * Reads XML from string 34 | * @param {Node} node 35 | * @returns void 36 | */ 37 | LoadXml(node: Node | string): void; 38 | } 39 | 40 | export type IXmlSerializableConstructor = new () => IXmlSerializable; 41 | 42 | /** 43 | * Base type for associated arrays 44 | * 45 | * @interface AssocArray 46 | * @template T 47 | */ 48 | export interface AssocArray { 49 | [index: string]: T; 50 | } 51 | 52 | export declare type XmlBufferEncoding = string | "utf8" | "binary" | "hex" | "base64" | "base64url"; 53 | 54 | export declare type ISelectResult = Node[] | Node | boolean | number | string; 55 | 56 | export interface XmlNamespace { 57 | /** 58 | * Prefix 59 | * 60 | * @type {(string |)} 61 | * @memberOf XmlNamespace 62 | */ 63 | prefix: string | null; 64 | /** 65 | * Namespace URI 66 | * 67 | * @type {(string |)} 68 | * @memberOf XmlNamespace 69 | */ 70 | namespace: string | null; 71 | } 72 | 73 | export interface XmlSchemaItemBase { 74 | /** 75 | * Local name of item 76 | * 77 | * @type {string} 78 | * @memberOf XmlSchemaItemBase 79 | */ 80 | localName?: string; 81 | /** 82 | * Namespace URI of attribute 83 | * 84 | * @type {(string |)} 85 | * @memberOf XmlSchemaItemBase 86 | */ 87 | namespaceURI?: string | null; 88 | 89 | /** 90 | * Default prefix for Xml element 91 | * 92 | * @type {(string |)} 93 | * @memberOf XmlSchemaItemBase 94 | */ 95 | prefix?: string | null; 96 | } 97 | 98 | export interface XmlSchemaItem extends XmlSchemaItemBase { 99 | /** 100 | * Default value for item 101 | * 102 | * @type {(T |)} 103 | * @memberOf XmlSchemaItem 104 | */ 105 | defaultValue?: T | null; 106 | /** 107 | * Determine where item is required 108 | * 109 | * @type {boolean} 110 | * @memberOf XmlSchemaItem 111 | */ 112 | required?: boolean; 113 | /** 114 | * Custom converter for item value 115 | * 116 | * @type {IConverter} 117 | * @memberOf XmlAttributeType 118 | */ 119 | converter?: IConverter; 120 | } 121 | 122 | export interface XmlSchemaItemParser { 123 | /** 124 | * Xml parser for item 125 | * 126 | * @type {*} 127 | * @memberOf XmlSchemaItemParser 128 | */ 129 | parser?: IXmlSerializableConstructor; 130 | } 131 | 132 | export interface XmlAttributeType extends XmlSchemaItem { 133 | } 134 | 135 | export interface XmlContentType { 136 | /** 137 | * Default value for item 138 | * 139 | * @type {(T |)} 140 | * @memberOf XmlContentType 141 | */ 142 | defaultValue?: T | null; 143 | /** 144 | * Determine where item is required 145 | * 146 | * @type {boolean} 147 | * @memberOf XmlContentType 148 | */ 149 | required?: boolean; 150 | /** 151 | * Custom converter for item value 152 | * 153 | * @type {IConverter} 154 | * @memberOf XmlContentType 155 | */ 156 | converter?: IConverter; 157 | } 158 | 159 | export interface XmlElementType extends XmlSchemaItemBase, XmlSchemaItemParser { 160 | /** 161 | * Local name for Xml element 162 | * 163 | * @type {string} 164 | * @memberOf XmlElementType 165 | */ 166 | localName: string; 167 | /** 168 | * Namespace URI fro Xml element 169 | * 170 | * @type {(string |)} 171 | * @memberOf XmlElementType 172 | */ 173 | namespaceURI?: string | null; 174 | } 175 | 176 | export interface XmlChildElementType extends XmlSchemaItem, XmlSchemaItemParser { 177 | /** 178 | * max occurs of items in collection 179 | * 180 | * @type {number} 181 | * @memberOf XmlChildElementType 182 | */ 183 | maxOccurs?: number; 184 | /** 185 | * min occurs of items in collection 186 | * 187 | * @type {number} 188 | * @memberOf XmlChildElementType 189 | */ 190 | minOccurs?: number; 191 | /** 192 | * Don't add root element of XmlCollection to compiled element 193 | * 194 | * @type {boolean} 195 | * @memberOf XmlChildElementType 196 | */ 197 | noRoot?: boolean; 198 | } 199 | 200 | export interface XmlSchema { 201 | localName?: string; 202 | namespaceURI?: string | null; 203 | prefix?: string | null; 204 | parser?: IXmlSerializableConstructor; 205 | items?: { [key: string]: (XmlChildElementType | XmlAttributeType) & { type?: string } }; 206 | target?: any; 207 | } 208 | 209 | export interface IConverter { 210 | /** 211 | * Converts value from Xml element to Object 212 | * 213 | * @memberOf IConverter 214 | */ 215 | set: (value: string) => T; 216 | /** 217 | * Converts value from Object to Xml element 218 | * 219 | * @memberOf IConverter 220 | */ 221 | get: (value: T) => string | undefined; 222 | } 223 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AssocArray } from "./types"; 2 | import { APPLICATION_XML, XmlNodeType } from "./xml"; 3 | 4 | export type SelectNodes = (node: Node, xPath: string) => Node[]; 5 | 6 | let xpath: SelectNodes = (node: Node, xPath: string) => { 7 | throw new Error("Not implemented"); 8 | }; 9 | 10 | // Fix global 11 | let sWindow: any; 12 | if (typeof self === "undefined") { 13 | sWindow = global; 14 | const xmldom = require("@xmldom/xmldom"); 15 | xpath = require("xpath.js"); 16 | sWindow.XMLSerializer = xmldom.XMLSerializer; 17 | sWindow.DOMParser = xmldom.DOMParser; 18 | sWindow.DOMImplementation = xmldom.DOMImplementation; 19 | sWindow.document = new DOMImplementation().createDocument("http://www.w3.org/1999/xhtml", "html", null!); 20 | } else { 21 | sWindow = self; 22 | } 23 | 24 | function SelectNodesEx(node: Node, xPath: string): Node[] { 25 | const doc: Document = node.ownerDocument == null ? node as Document : node.ownerDocument; 26 | const nsResolver = document.createNSResolver(node.ownerDocument == null ? (node as Document).documentElement : node.ownerDocument.documentElement); 27 | const personIterator = doc.evaluate(xPath, node, nsResolver, XPathResult.ANY_TYPE, null!); 28 | const ns: Node[] = []; 29 | let n: Node | null; 30 | while (n = personIterator.iterateNext()) { 31 | ns.push(n); 32 | } 33 | return ns; 34 | } 35 | 36 | export const Select: SelectNodes = (typeof self !== "undefined") ? SelectNodesEx : xpath; 37 | 38 | export function Parse(xmlString: string) { 39 | /** 40 | * NOTE: https://www.w3.org/TR/REC-xml/#sec-line-ends 41 | * The XML processor must behave as if it normalized all line breaks in external parsed 42 | * entities (including the document entity) on input, before parsing, by translating both 43 | * the two-character sequence #xD #xA and any #xD that is not followed by #xA to a single #xA character. 44 | */ 45 | xmlString = xmlString 46 | .replace(/\r\n/g, "\n") 47 | .replace(/\r/g, "\n"); 48 | return new DOMParser().parseFromString(xmlString, APPLICATION_XML); 49 | } 50 | 51 | export function Stringify(target: Node) { 52 | return new XMLSerializer().serializeToString(target); 53 | } 54 | 55 | /** 56 | * Returns single Node from given Node 57 | * 58 | * @export 59 | * @param {Node} node 60 | * @param {string} path 61 | * @returns 62 | */ 63 | export function SelectSingleNode(node: Node, path: string) { 64 | const ns = Select(node, path); 65 | if (ns && ns.length > 0) { 66 | return ns[0]; 67 | } 68 | return null; 69 | } 70 | 71 | function _SelectNamespaces(node: Node, selectedNodes: AssocArray = {}) { 72 | if (isElement(node)) { 73 | if (node.namespaceURI && node.namespaceURI !== "http://www.w3.org/XML/1998/namespace" && !selectedNodes[node.prefix || ""]) { 74 | selectedNodes[node.prefix ? node.prefix : ""] = node.namespaceURI!; 75 | } 76 | for (let i = 0; i < node.childNodes.length; i++) { 77 | const childNode = node.childNodes.item(i); 78 | if (childNode && childNode.nodeType === XmlNodeType.Element) { 79 | _SelectNamespaces(childNode, selectedNodes); 80 | } 81 | } 82 | } 83 | } 84 | 85 | export function SelectNamespaces(node: Element) { 86 | const attrs: AssocArray = {}; 87 | _SelectNamespaces(node, attrs); 88 | return attrs; 89 | } 90 | 91 | export function assign(target: any, ...sources: any[]) { 92 | const res = arguments[0]; 93 | for (let i = 1; i < arguments.length; i++) { 94 | const obj = arguments[i]; 95 | for (const prop in obj) { 96 | if (!obj.hasOwnProperty(prop)) { 97 | continue; 98 | } 99 | res[prop] = obj[prop]; 100 | } 101 | } 102 | return res; 103 | } 104 | 105 | /** 106 | * Returns true if object is a XML of specified type 107 | * @param obj Object to test 108 | * @param type XML Node type 109 | */ 110 | function isNodeType(obj: any, type: XmlNodeType) { 111 | return obj && obj.nodeType === type; 112 | } 113 | 114 | /** 115 | * Returns true if object is a XML Element 116 | * @param obj Object to test 117 | */ 118 | export function isElement(obj: any): obj is Element { 119 | return isNodeType(obj, XmlNodeType.Element); 120 | } 121 | 122 | /** 123 | * Returns true if object is a XML Document 124 | * @param obj Object to test 125 | */ 126 | export function isDocument(obj: any): obj is Document { 127 | return isNodeType(obj, XmlNodeType.Document); 128 | } 129 | -------------------------------------------------------------------------------- /src/xml.ts: -------------------------------------------------------------------------------- 1 | export const APPLICATION_XML = "application/xml"; 2 | export const DEFAULT_PREFIX = ""; 3 | export const DEFAULT_NAMESPACE_URI = ""; 4 | 5 | export enum XmlNodeType { 6 | None = 0, // Read method has not been 7 | Element = 1, 8 | Attribute = 2, 9 | Text = 3, // The text content of a node. 10 | CDATA = 4, // For example, 11 | EntityReference = 5, // A reference to an entity 12 | Entity = 6, // For example, 13 | ProcessingInstruction = 7, // For example, 14 | Comment = 8, 15 | Document = 9, // A document object that, as the root of the document tree, 16 | // provides access to the entire XML document. 17 | DocumentType = 10, // For example, 18 | DocumentFragment = 11, 19 | Notation = 12, // A For example, 20 | Whitespace = 13, // White space between markup. 21 | SignificantWhitespace = 14, // White space between markup in a mixed content model 22 | // or white space within the xml:space="preserve" scope. 23 | EndElement = 15, // An end element tag (for example, ). 24 | EndEntity = 16, // Returned when XmlReader gets to the end of the entity 25 | // replacement as a result of a call to XmlReader.ResolveEntity() 26 | XmlDeclaration = 17, // for example, 27 | } 28 | -------------------------------------------------------------------------------- /src/xml_collection.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "./collection"; 2 | import { XE, XmlError } from "./error"; 3 | import { ICollection } from "./types"; 4 | import { isElement } from "./utils"; 5 | import { XmlObject } from "./xml_object"; 6 | 7 | export class XmlCollection extends XmlObject implements ICollection { 8 | 9 | public static parser: any; 10 | 11 | /** 12 | * The maximum number of elements 13 | */ 14 | public MaxOccurs = Number.MAX_VALUE; 15 | 16 | /** 17 | * The minimum number of elements 18 | */ 19 | public MinOccurs = 0; 20 | 21 | // Collection 22 | protected items: I[] = []; 23 | 24 | public HasChanged() { 25 | const res = super.HasChanged(); 26 | 27 | const changed = this.Some((item) => item.HasChanged()); 28 | 29 | return res || changed; 30 | } 31 | 32 | public get Count() { 33 | return this.items.length; 34 | } 35 | 36 | public Item(index: number): I | null { 37 | return this.items[index] || null; 38 | } 39 | 40 | public Add(item: I) { 41 | this.items.push(item); 42 | this.element = null; 43 | } 44 | 45 | public Pop() { 46 | this.element = null; 47 | return this.items.pop(); 48 | } 49 | 50 | public RemoveAt(index: number) { 51 | this.items = this.items.filter((item, index2) => index2 !== index); 52 | this.element = null; 53 | } 54 | 55 | public Clear() { 56 | this.items = new Array(); 57 | this.element = null; 58 | } 59 | 60 | public GetIterator() { 61 | return this.items; 62 | } 63 | 64 | public ForEach(cb: (item: I, index: number, array: I[]) => void) { 65 | this.GetIterator().forEach(cb); 66 | } 67 | 68 | public Map(cb: (item: I, index: number, array: I[]) => U) { 69 | return new Collection(this.GetIterator().map(cb)); 70 | } 71 | 72 | public Filter(cb: (item: I, index: number, array: I[]) => boolean) { 73 | return new Collection(this.GetIterator().filter(cb)); 74 | } 75 | 76 | public Sort(cb: (a: I, b: I) => number) { 77 | return new Collection(this.GetIterator().sort(cb)); 78 | } 79 | 80 | public Every(cb: (value: I, index: number, array: I[]) => boolean) { 81 | return this.GetIterator().every(cb); 82 | } 83 | 84 | public Some(cb: (value: I, index: number, array: I[]) => boolean) { 85 | return this.GetIterator().some(cb); 86 | } 87 | 88 | public IsEmpty() { 89 | return this.Count === 0; 90 | } 91 | 92 | protected OnGetXml(element: Element) { 93 | for (const item of this.GetIterator()) { 94 | const el = item.GetXml(); 95 | if (el) { 96 | element.appendChild(el); 97 | } 98 | } 99 | } 100 | 101 | protected OnLoadXml(element: Element) { 102 | const self = this.GetStatic(); 103 | if (!self.parser) { 104 | throw new XmlError(XE.XML_EXCEPTION, `${self.localName} doesn't have required 'parser' in @XmlElement`); 105 | } 106 | for (let i = 0; i < element.childNodes.length; i++) { 107 | const node = element.childNodes.item(i); 108 | if (!(isElement(node) && 109 | node.localName === (self.parser as any).localName && 110 | // tslint:disable-next-line:triple-equals 111 | node.namespaceURI == self.namespaceURI)) { 112 | // Ignore wrong elements 113 | continue; 114 | } 115 | const el = node as Element; 116 | 117 | const item = new self.parser(); 118 | item.LoadXml(el); 119 | this.Add(item as any); 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/xml_object.ts: -------------------------------------------------------------------------------- 1 | import * as CONST from "./const"; 2 | import { XE, XmlError } from "./error"; 3 | import { AssocArray, IXmlSerializable, XmlAttributeType, XmlChildElementType, XmlContentType, XmlSchema } from "./types"; 4 | import { isDocument, isElement, Parse, SelectSingleNode } from "./utils"; 5 | import { APPLICATION_XML } from "./xml"; 6 | 7 | const DEFAULT_ROOT_NAME = "xml_root"; 8 | 9 | export class XmlObject implements IXmlSerializable { 10 | 11 | public static LoadXml(this: new () => T, param: string | Element) { 12 | const xml = new this(); 13 | xml.LoadXml(param); 14 | return xml; 15 | } 16 | 17 | public static GetElement(element: Element, name: string, required: boolean = true) { 18 | const xmlNodeList = element.getElementsByTagName(name); 19 | if (required && xmlNodeList.length === 0) { 20 | throw new XmlError(XE.ELEMENT_MISSING, name, element.localName); 21 | } 22 | return xmlNodeList[0] || null; 23 | } 24 | 25 | public static GetAttribute(element: Element, attrName: string, defaultValue: string | null, required: boolean = true) { 26 | if (element.hasAttribute(attrName)) { 27 | return element.getAttribute(attrName); 28 | } else { 29 | if (required) { 30 | throw new XmlError(XE.ATTRIBUTE_MISSING, attrName, element.localName); 31 | } 32 | return defaultValue; 33 | } 34 | } 35 | 36 | public static GetElementById(element: Document | Element, idValue: string): Element | null; 37 | public static GetElementById(node: Node, idValue: string) { 38 | if ((node == null) || (idValue == null)) { 39 | return null; 40 | } 41 | 42 | // this works only if there's a DTD or XSD available to define the ID 43 | let xel: Node | null = null; 44 | if (isDocument(node)) { 45 | xel = node.getElementById(idValue); 46 | } 47 | if (xel == null) { 48 | // search an "undefined" ID 49 | xel = SelectSingleNode(node, `//*[@*[local-name()='Id']='${idValue}']`); 50 | if (xel == null) { 51 | xel = SelectSingleNode(node, `//*[@*[local-name()='ID']='${idValue}']`); 52 | if (xel == null) { 53 | xel = SelectSingleNode(node, `//*[@*[local-name()='id']='${idValue}']`); 54 | } 55 | } 56 | } 57 | return xel as Element; 58 | } 59 | 60 | /** 61 | * Creates new instance of XmlDocument with given name of root element 62 | * @param {string} root Name of root element 63 | * @param {string} namespaceUri 64 | * @param {string} prefix 65 | * @returns Document 66 | */ 67 | public static CreateDocument(root: string = DEFAULT_ROOT_NAME, namespaceUri: string | null = null, prefix: string | null = null): Document { 68 | let namePrefix = ""; 69 | let nsPrefix = ""; 70 | let namespaceUri2 = ""; 71 | if (prefix) { 72 | namePrefix = prefix + ":"; 73 | nsPrefix = ":" + prefix; 74 | } 75 | if (namespaceUri) { 76 | namespaceUri2 = ` xmlns${nsPrefix}="${namespaceUri}"`; 77 | } 78 | const name = `${namePrefix}${root}`; 79 | const doc = new DOMParser().parseFromString(`<${name}${namespaceUri2}>`, APPLICATION_XML); 80 | return doc; 81 | } 82 | 83 | public static GetChildren(node: Node, localName: string, nameSpace?: string): Element[] { 84 | node = isDocument(node) ? node.documentElement : node; 85 | const res: Element[] = []; 86 | for (let i = 0; i < node.childNodes.length; i++) { 87 | const child = node.childNodes[i]; 88 | if (isElement(child) && child.localName === localName && (child.namespaceURI === nameSpace || !nameSpace)) { 89 | res.push(child); 90 | } 91 | } 92 | return res; 93 | } 94 | 95 | public static GetFirstChild(node: Node, localName: string, nameSpace?: string): Element | null { 96 | node = isDocument(node) ? node.documentElement : node; 97 | for (let i = 0; i < node.childNodes.length; i++) { 98 | const child = node.childNodes[i]; 99 | if (isElement(child) && child.localName === localName && (child.namespaceURI === nameSpace || !nameSpace)) { 100 | return child; 101 | } 102 | } 103 | return null; 104 | } 105 | public static GetChild(node: Element, localName: string, nameSpace?: string, required = true): Element | null { 106 | for (let i = 0; i < node.childNodes.length; i++) { 107 | const child = node.childNodes[i]; 108 | if (isElement(child) && child.localName === localName && (child.namespaceURI === nameSpace || !nameSpace)) { 109 | return child; 110 | } 111 | } 112 | if (required) { 113 | throw new XmlError(XE.ELEMENT_MISSING, localName, node.localName); 114 | } 115 | return null; 116 | } 117 | 118 | protected static attributes: AssocArray>; 119 | protected static elements: AssocArray>; 120 | protected static prefix: string | null; 121 | protected static namespaceURI: string | null; 122 | protected static localName: string; 123 | 124 | /** 125 | * XmlElement 126 | * undefined - class initialized 127 | * null - has some changes 128 | * element - has cached element 129 | * 130 | * @protected 131 | * @type {(Element | null | undefined)} 132 | * @memberOf XmlObject 133 | */ 134 | protected element?: Element | null; 135 | protected prefix = this.GetStatic().prefix || null; 136 | 137 | protected localName = this.GetStatic().localName; 138 | protected namespaceURI = this.GetStatic().namespaceURI; 139 | 140 | constructor (properties: Object = {}) { 141 | if (properties) { 142 | for (const [key, value] of Object.entries(properties)) { 143 | if (value !== undefined) { 144 | (this as any)[key] = value; 145 | } 146 | } 147 | } 148 | } 149 | 150 | public get Element() { 151 | return this.element; 152 | } 153 | 154 | public get Prefix() { 155 | return this.prefix; 156 | } 157 | public set Prefix(value: string | null) { 158 | this.prefix = value; 159 | } 160 | 161 | public get LocalName(): string { 162 | return this.localName!; 163 | } 164 | public get NamespaceURI(): string | null { 165 | return this.namespaceURI || null; 166 | } 167 | 168 | public HasChanged() { 169 | const self = this.GetStatic(); 170 | 171 | // Check changed elements 172 | if (self.items) { 173 | for (const key in self.items) { 174 | if (!self.items.hasOwnProperty(key)) { 175 | continue; 176 | } 177 | const item: XmlChildElementType = self.items[key]; 178 | const value = (this as any)[key]; 179 | 180 | if (item.parser && value && value.HasChanged()) { 181 | return true; 182 | } 183 | } 184 | } 185 | return this.element === null; 186 | } 187 | 188 | public GetXml(hard?: boolean): Element | null { 189 | if (!(hard || this.HasChanged())) { 190 | return this.element || null; 191 | } 192 | 193 | const thisAny = this as any; 194 | const doc = this.CreateDocument(); 195 | const el = this.CreateElement(); 196 | const self = this.GetStatic(); 197 | 198 | const localName: string = this.localName!; 199 | 200 | // Add attributes 201 | if (self.items) { 202 | for (const key in self.items) { 203 | if (!self.items.hasOwnProperty(key)) { 204 | continue; 205 | } 206 | const parser = thisAny[key]; 207 | const selfItem: any = self.items[key]; 208 | switch (selfItem.type) { 209 | case CONST.CONTENT: { 210 | const schema: XmlContentType = selfItem; 211 | const value = (schema.converter) ? schema.converter.get(parser) : parser; 212 | if (schema.required && (value === null || value === void 0)) { 213 | throw new XmlError(XE.CONTENT_MISSING, localName); 214 | } 215 | 216 | if (schema.defaultValue !== parser || schema.required) { 217 | el.textContent = value; 218 | } 219 | break; 220 | } 221 | case CONST.ATTRIBUTE: { 222 | const schema: XmlAttributeType = selfItem; 223 | const value = (schema.converter) ? schema.converter.get(parser) : parser; 224 | if (schema.required && (value === null || value === void 0)) { 225 | throw new XmlError(XE.ATTRIBUTE_MISSING, schema.localName, localName); 226 | } 227 | 228 | // attr value 229 | if (schema.defaultValue !== parser || schema.required) { 230 | if (!schema.namespaceURI) { 231 | el.setAttribute(schema.localName!, value); 232 | } else { 233 | el.setAttributeNS(schema.namespaceURI, schema.localName!, value); 234 | } 235 | } 236 | break; 237 | } 238 | case CONST.ELEMENT: { 239 | // Add elements 240 | const schema = selfItem as XmlChildElementType; 241 | let node: Element | null = null; 242 | 243 | if (schema.parser) { 244 | if ((schema.required && !parser) || 245 | (schema.minOccurs && !parser.Count)) { 246 | throw new XmlError(XE.ELEMENT_MISSING, parser.localName, localName); 247 | } 248 | 249 | if (parser) { 250 | node = parser.GetXml(parser.element === void 0 && (schema.required || parser.Count)); 251 | } 252 | } else { 253 | const value = (schema.converter) ? schema.converter.get(parser) : parser; 254 | if (schema.required && value === void 0) { 255 | throw new XmlError(XE.ELEMENT_MISSING, schema.localName, localName); 256 | } 257 | if (parser !== schema.defaultValue || schema.required) { 258 | if (!schema.namespaceURI) { 259 | node = doc.createElement(`${schema.prefix ? schema.prefix + ":" : ""}${schema.localName}`); 260 | } else { 261 | node = doc.createElementNS(schema.namespaceURI, `${schema.prefix ? schema.prefix + ":" : ""}${schema.localName}`); 262 | } 263 | if (Array.isArray(value)) { 264 | for (const child of value) { 265 | const val = child instanceof XmlObject ? 266 | child.GetXml(true) : 267 | child; 268 | if (val !== null) { 269 | node!.appendChild(val); 270 | } 271 | } 272 | } else if (value instanceof XmlObject) { 273 | node!.appendChild(value.GetXml(true) as Element); 274 | } else { 275 | node!.textContent = value; 276 | } 277 | } 278 | } 279 | 280 | if (node) { 281 | if (schema.noRoot) { 282 | const els: Element[] = []; 283 | // no root 284 | for (let i = 0; i < node.childNodes.length; i++) { 285 | const colNode = node.childNodes.item(i); 286 | if (isElement(colNode)) { 287 | els.push(colNode); 288 | } 289 | } 290 | if (els.length < schema.minOccurs! || els.length > schema.maxOccurs!) { 291 | throw new XmlError(XE.COLLECTION_LIMIT, parser.localName, self.localName); 292 | } 293 | els.forEach((e) => el.appendChild(e.cloneNode(true))); 294 | } else if (node.childNodes.length < schema.minOccurs! || node.childNodes.length > schema.maxOccurs!) { 295 | throw new XmlError(XE.COLLECTION_LIMIT, parser.localName, self.localName); 296 | } else { 297 | el.appendChild(node); 298 | } 299 | } 300 | break; 301 | } 302 | } 303 | } 304 | } 305 | 306 | // Set custom 307 | this.OnGetXml(el); 308 | 309 | // Cache compiled elements 310 | this.element = el; 311 | return el; 312 | } 313 | 314 | public LoadXml(param: string | Element) { 315 | let element: Element; 316 | const thisAny = this as any; 317 | if (typeof param === "string") { 318 | const doc = Parse(param); 319 | element = doc.documentElement; 320 | } else { 321 | element = param; 322 | } 323 | 324 | if (!element) { 325 | throw new XmlError(XE.PARAM_REQUIRED, "element"); 326 | } 327 | 328 | const self = this.GetStatic(); 329 | const localName = this.localName!; 330 | 331 | // tslint:disable-next-line:triple-equals 332 | if (!((element.localName === localName) && (element.namespaceURI == this.NamespaceURI))) { 333 | throw new XmlError(XE.ELEMENT_MALFORMED, localName); 334 | } 335 | 336 | // Get attributes 337 | if (self.items) { 338 | for (const key in self.items) { 339 | if (!self.items.hasOwnProperty(key)) { 340 | continue; 341 | } 342 | const selfItem: any = self.items[key]; 343 | switch (selfItem.type) { 344 | case CONST.CONTENT: { 345 | const schema: XmlContentType = selfItem; 346 | 347 | if (schema.required && !element.textContent) { 348 | throw new XmlError(XE.CONTENT_MISSING, localName); 349 | } 350 | 351 | if (!element.textContent) { 352 | thisAny[key] = schema.defaultValue; 353 | } else { 354 | const value = schema.converter ? schema.converter.set(element.textContent) : element.textContent; 355 | thisAny[key] = value; 356 | } 357 | break; 358 | } 359 | case CONST.ATTRIBUTE: { 360 | const schema: XmlAttributeType = selfItem; 361 | 362 | let hasAttribute: () => boolean; 363 | let getAttribute: () => string | null; 364 | 365 | if (!schema.localName) { 366 | throw new XmlError(XE.PARAM_REQUIRED, "localName"); 367 | } 368 | 369 | if (schema.namespaceURI) { 370 | hasAttribute = element.hasAttributeNS.bind(element, schema.namespaceURI, schema.localName); 371 | getAttribute = element.getAttributeNS.bind(element, schema.namespaceURI, schema.localName); 372 | } else { 373 | hasAttribute = element.hasAttribute.bind(element, schema.localName); 374 | getAttribute = element.getAttribute.bind(element, schema.localName); 375 | } 376 | 377 | if (schema.required && !hasAttribute()) { 378 | throw new XmlError(XE.ATTRIBUTE_MISSING, schema.localName, localName); 379 | } 380 | 381 | if (!hasAttribute()) { 382 | thisAny[key] = schema.defaultValue; 383 | } else { 384 | const value = schema.converter ? schema.converter.set(getAttribute()!) : getAttribute()!; 385 | thisAny[key] = value; 386 | } 387 | break; 388 | } 389 | case CONST.ELEMENT: { 390 | // Get element 391 | const schema: XmlChildElementType = selfItem; 392 | // noRoot 393 | if (schema.noRoot) { 394 | if (!schema.parser) { 395 | throw new XmlError(XE.XML_EXCEPTION, `Schema for '${schema.localName}' with flag noRoot must have 'parser'`); 396 | } 397 | const col: XmlCollection = new schema.parser() as any; 398 | if (!(col instanceof XmlCollection)) { 399 | throw new XmlError(XE.XML_EXCEPTION, `Schema for '${schema.localName}' with flag noRoot must have 'parser' like instance of XmlCollection`); 400 | } 401 | (col as any).OnLoadXml(element); // protected member 402 | delete col.element; // reset cache status 403 | 404 | if (col.Count < schema.minOccurs! || col.Count > schema.maxOccurs!) { 405 | throw new XmlError(XE.COLLECTION_LIMIT, (schema.parser as any).localName, localName); 406 | } 407 | thisAny[key] = col; 408 | continue; 409 | } 410 | 411 | // Get element by localName 412 | let foundElement: Element | null = null; 413 | for (let i = 0; i < element.childNodes.length; i++) { 414 | const node = element.childNodes.item(i); 415 | if (!isElement(node)) { 416 | continue; 417 | } 418 | const el = node; 419 | if (el.localName === schema.localName && 420 | // tslint:disable-next-line:triple-equals 421 | el.namespaceURI == schema.namespaceURI) { 422 | foundElement = el; 423 | break; 424 | } 425 | } 426 | 427 | // required 428 | if (schema.required && !foundElement) { 429 | throw new XmlError(XE.ELEMENT_MISSING, schema.parser ? (schema.parser as any).localName : schema.localName, localName); 430 | } 431 | 432 | if (!schema.parser) { 433 | 434 | // simple element 435 | if (!foundElement) { 436 | thisAny[key] = schema.defaultValue; 437 | } else { 438 | const value = schema.converter ? schema.converter.set(foundElement.textContent!) : foundElement.textContent; 439 | thisAny[key] = value; 440 | } 441 | } else { 442 | // element 443 | if (foundElement) { 444 | const value = new schema.parser() as IXmlSerializable; 445 | (value as any).localName = schema.localName; 446 | (value as any).namespaceURI = schema.namespaceURI; 447 | thisAny[key] = value; 448 | value.LoadXml(foundElement); 449 | } 450 | } 451 | break; 452 | } 453 | } 454 | } 455 | } 456 | 457 | // Get custom 458 | this.OnLoadXml(element); 459 | 460 | this.prefix = element.prefix || ""; 461 | this.element = element; 462 | } 463 | 464 | /** 465 | * Returns current Xml as string 466 | * - if element was initialized without changes, returns empty string 467 | */ 468 | public toString(): string { 469 | const xml = this.GetXml(); 470 | return xml ? new XMLSerializer().serializeToString(xml) : ""; 471 | } 472 | 473 | public GetElement(name: string, required: boolean = true) { 474 | if (!this.element) { 475 | throw new XmlError(XE.NULL_PARAM, this.localName); 476 | } 477 | return XmlObject.GetElement(this.element, name, required); 478 | } 479 | 480 | public GetChildren(localName: string, nameSpace?: string) { 481 | if (!this.element) { 482 | throw new XmlError(XE.NULL_PARAM, this.localName); 483 | } 484 | return XmlObject.GetChildren(this.element, localName, nameSpace || this.NamespaceURI || undefined); 485 | } 486 | 487 | public GetChild(localName: string, required = true): Element | null { 488 | if (!this.element) { 489 | throw new XmlError(XE.NULL_PARAM, this.localName); 490 | } 491 | return XmlObject.GetChild(this.element, localName, this.NamespaceURI || undefined, required); 492 | } 493 | 494 | public GetFirstChild(localName: string, namespace?: string) { 495 | if (!this.element) { 496 | throw new XmlError(XE.NULL_PARAM, this.localName); 497 | } 498 | return XmlObject.GetFirstChild(this.element, localName, namespace); 499 | } 500 | 501 | public GetAttribute(name: string, defaultValue: string | null, required: boolean = true) { 502 | if (!this.element) { 503 | throw new XmlError(XE.NULL_PARAM, this.localName); 504 | } 505 | return XmlObject.GetAttribute(this.element, name, defaultValue, required); 506 | } 507 | 508 | public IsEmpty() { 509 | return this.Element === void 0; 510 | } 511 | 512 | protected OnLoadXml(element: Element) { 513 | // Empty 514 | } 515 | 516 | protected GetStatic(): XmlSchema { 517 | return this.constructor as XmlSchema; 518 | } 519 | 520 | protected GetPrefix(): string { 521 | return (this.Prefix) ? this.prefix + ":" : ""; 522 | } 523 | 524 | protected OnGetXml(element: Element) { 525 | // Empty 526 | } 527 | 528 | protected CreateElement(document?: Document, localName?: string, namespaceUri: string | null = null, prefix: string | null = null) { 529 | if (!document) { 530 | document = this.CreateDocument()!; 531 | } 532 | localName = localName || this.localName; 533 | namespaceUri = namespaceUri || this.NamespaceURI; 534 | prefix = prefix || this.prefix; 535 | 536 | const tagName = (prefix ? `${prefix}:` : "") + localName; 537 | const xn = namespaceUri ? document!.createElementNS(namespaceUri, tagName) : document!.createElement(tagName); 538 | document!.importNode(xn, true); 539 | 540 | return xn; 541 | } 542 | 543 | protected CreateDocument() { 544 | return XmlObject.CreateDocument( 545 | this.localName, 546 | this.NamespaceURI, 547 | this.Prefix); 548 | } 549 | } 550 | 551 | import { XmlCollection } from "./xml_collection"; 552 | -------------------------------------------------------------------------------- /test/collection.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { Collection } from "../src"; 3 | 4 | describe("Collection", () => { 5 | 6 | it("New empty Collection", () => { 7 | const col = new Collection(); 8 | assert.equal(col.Count, 0); 9 | }); 10 | 11 | it("New Collection from Array", () => { 12 | const col = new Collection([1, 2, 3, 4, 5]); 13 | assert.equal(col.Count, 5); 14 | }); 15 | 16 | it("Get Item", () => { 17 | const col = new Collection([1, 2, 3, 4, 5]); 18 | assert.equal(col.Item(0), 1); 19 | assert.equal(col.Item(4), 5); 20 | assert.equal(col.Item(5), null); 21 | }); 22 | 23 | it("Add Item", () => { 24 | const col = new Collection(); 25 | assert.equal(col.Count, 0); 26 | col.Add(1); 27 | assert.equal(col.Count, 1); 28 | assert.equal(col.Item(0), 1); 29 | col.Add(2); 30 | assert.equal(col.Count, 2); 31 | assert.equal(col.Item(1), 2); 32 | }); 33 | 34 | it("Pop", () => { 35 | const col = new Collection(); 36 | col.Add(1); 37 | col.Add(2); 38 | col.Add(3); 39 | assert.equal(col.Count, 3); 40 | col.Pop(); 41 | assert.equal(col.Count, 2); 42 | assert.equal(col.Item(0), 1); 43 | assert.equal(col.Item(1), 2); 44 | }); 45 | 46 | it("RemoveAt", () => { 47 | const col = new Collection(); 48 | col.Add(1); 49 | col.Add(2); 50 | col.Add(3); 51 | assert.equal(col.Count, 3); 52 | col.RemoveAt(0); 53 | assert.equal(col.Count, 2); 54 | assert.equal(col.Item(0), 2); 55 | assert.equal(col.Item(1), 3); 56 | }); 57 | 58 | it("Clear", () => { 59 | const col = new Collection(); 60 | col.Add(1); 61 | col.Add(2); 62 | col.Add(3); 63 | assert.equal(col.Count, 3); 64 | col.Clear(); 65 | assert.equal(col.Count, 0); 66 | }); 67 | 68 | it("IsEmpty", () => { 69 | const col = new Collection(); 70 | assert.equal(col.IsEmpty(), true); 71 | col.Add(1); 72 | assert.equal(col.IsEmpty(), false); 73 | }); 74 | 75 | it("Get Iterator", () => { 76 | const col = new Collection([1, 2, 3, 4, 5]); 77 | assert.equal(col.Count, 5); 78 | const array = col.GetIterator(); 79 | assert.equal(Array.isArray(array), true); 80 | assert.equal(array.length, 5); 81 | }); 82 | 83 | it("Each", () => { 84 | const col = new Collection([1, 2, 3, 4, 5]); 85 | assert.equal(col.Count, 5); 86 | col.ForEach((item, index) => assert.equal(item, index + 1)); 87 | }); 88 | 89 | it("Map", () => { 90 | const col = new Collection([1, 2, 3, 4, 5]); 91 | assert.equal(col.Count, 5); 92 | const array = col.Map((item, index) => item === index + 1).GetIterator(); 93 | assert.equal(array.every((item) => item === true), true); 94 | }); 95 | 96 | it("Filter", () => { 97 | const col = new Collection([1, 2, 1, 2, 1, 2, 1, 2]); 98 | assert.equal(col.Count, 8); 99 | const array = col.Filter((item, index) => item === 1).GetIterator(); 100 | assert.equal(array.every((item) => item === 1), true); 101 | assert.equal(array.length, 4); 102 | }); 103 | 104 | it("Sort", () => { 105 | const col = new Collection([1, 2, 3, 4, 5]); 106 | assert.equal(col.Count, 5); 107 | const array = col.Sort((a, b) => a < b ? 1 : a > b ? -1 : 0).GetIterator(); 108 | const num = 5; 109 | array.forEach((item, index) => assert.equal(item === num - index, true)); 110 | }); 111 | 112 | it("Every", () => { 113 | const col = new Collection([1, 2, 3, 4, 5]); 114 | assert.equal(col.Count, 5); 115 | assert.equal(col.Every((item) => item < 6), true); 116 | assert.equal(col.Every((item) => item < 5), false); 117 | }); 118 | 119 | it("Some", () => { 120 | const col = new Collection([1, 2, 3, 4, 5]); 121 | assert.equal(col.Count, 5); 122 | assert.equal(col.Some((item) => item < 6), true); 123 | assert.equal(col.Some((item) => item < 5), true); 124 | assert.equal(col.Some((item) => item > 5), false); 125 | }); 126 | 127 | it("IsEmpty", () => { 128 | const col = new Collection(); 129 | assert.equal(col.IsEmpty(), true); 130 | col.Add(1); 131 | assert.equal(col.IsEmpty(), false); 132 | }); 133 | 134 | }); 135 | -------------------------------------------------------------------------------- /test/convert.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { Convert } from "../src"; 3 | 4 | describe("Convert", () => { 5 | 6 | it("DateTime", () => { 7 | const date = new Date(); 8 | const str = Convert.FromDateTime(date); 9 | assert.equal(str.length > 0, true); 10 | const newDate = Convert.ToDateTime(str); 11 | assert.equal(newDate.getTime(), date.getTime()); 12 | }); 13 | 14 | ["utf8", "binary", "hex", "base64", "base64url"].forEach((enc) => { 15 | [ 16 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]), 17 | Convert.FromString("Привет", "utf8"), 18 | Convert.FromString("123456789", "binary"), 19 | Convert.FromString("010203040506070809", "hex"), 20 | Convert.FromString("Awa=", "base64"), 21 | Convert.FromString("Aw_=", "base64url"), 22 | ].forEach((buf, index) => { 23 | it(`Encoding ${enc} buf:${Convert.ToString(buf, enc)}`, () => { 24 | const str = Convert.ToString(buf, enc); 25 | assert.equal(typeof str, "string"); 26 | const newBuf = Convert.FromString(str, enc); 27 | assert.equal(buf.every((c: number, i: number) => c === newBuf[i]), true); 28 | }); 29 | }); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/converters.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { XmlAttribute, XmlElement, XmlObject } from "../src"; 3 | import { XmlBase64Converter, XmlBooleanConverter, XmlNumberConverter } from "../src"; 4 | 5 | describe("Convertors", () => { 6 | 7 | it("String", () => { 8 | 9 | @XmlElement({ localName: "test" }) 10 | class XmlTest extends XmlObject { 11 | 12 | @XmlAttribute({}) 13 | public Value: string; 14 | 15 | } 16 | 17 | const test = new XmlTest(); 18 | test.Value = ""; 19 | const xmlTest = test.toString(); 20 | const xml = ``; 21 | assert.equal(xmlTest, xml); 22 | 23 | const test2 = XmlTest.LoadXml(xml); 24 | assert.equal(test2.Value, ""); 25 | }); 26 | 27 | it("Number", () => { 28 | 29 | @XmlElement({ localName: "test" }) 30 | class XmlTest extends XmlObject { 31 | 32 | @XmlAttribute({ converter: XmlNumberConverter }) 33 | public Value: number; 34 | 35 | } 36 | 37 | const test = new XmlTest(); 38 | test.Value = 15; 39 | const xmlTest = test.toString(); 40 | const xml = ``; 41 | assert.equal(xmlTest, xml); 42 | 43 | const test2 = XmlTest.LoadXml(xml); 44 | assert.equal(test2.Value, 15); 45 | }); 46 | 47 | it("Base64", () => { 48 | 49 | @XmlElement({ localName: "test" }) 50 | class XmlTest extends XmlObject { 51 | 52 | @XmlAttribute({ converter: XmlBase64Converter }) 53 | public Value: Uint8Array; 54 | 55 | } 56 | 57 | const test = new XmlTest(); 58 | test.Value = new Uint8Array([1, 0, 1]); 59 | const xmlTest = test.toString(); 60 | const xml = ``; 61 | assert.equal(xmlTest, xml); 62 | 63 | const test2 = XmlTest.LoadXml(xml); 64 | assert.equal(test2.Value.length, 3); 65 | assert.equal(test2.Value[0], 1); 66 | assert.equal(test2.Value[1], 0); 67 | assert.equal(test2.Value[2], 1); 68 | }); 69 | 70 | it("Boolean", () => { 71 | 72 | @XmlElement({ localName: "test" }) 73 | class XmlTest extends XmlObject { 74 | 75 | @XmlAttribute({ converter: XmlBooleanConverter }) 76 | public ValueTrue: boolean; 77 | 78 | @XmlAttribute({ converter: XmlBooleanConverter }) 79 | public ValueFalse: boolean; 80 | 81 | } 82 | 83 | const test = new XmlTest(); 84 | test.ValueTrue = true; 85 | test.ValueFalse = false; 86 | const xmlTest = test.toString(); 87 | const xml = ``; 88 | assert.equal(xmlTest, xml); 89 | 90 | const test2 = XmlTest.LoadXml(xml); 91 | assert.equal(test2.ValueTrue, true); 92 | assert.equal(test2.ValueFalse, false); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/decorators.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { Parse, XmlAttribute, XmlBase64Converter, XmlChildElement, XmlCollection, XmlContent, XmlElement, XmlNumberConverter, XmlObject } from "../src"; 3 | 4 | describe("Decorators", () => { 5 | 6 | it("malformed", () => { 7 | 8 | @XmlElement({ localName: "test" }) 9 | class XmlTest extends XmlObject { 10 | } 11 | 12 | const doc = Parse(``); 13 | 14 | const test = new XmlTest(); 15 | assert.throws(() => { 16 | test.LoadXml(doc.documentElement); 17 | }); 18 | }); 19 | 20 | context("Element", () => { 21 | 22 | context("GetXml", () => { 23 | 24 | it("simple", () => { 25 | // Need some changes for changing element to null, otherwise empty answer 26 | @XmlElement({ localName: "test" }) 27 | class Test extends XmlObject { 28 | 29 | @XmlAttribute({ localName: "id" }) 30 | public Id: string; 31 | 32 | } 33 | 34 | const test = new Test(); 35 | 36 | assert.equal(test.toString(), ""); 37 | 38 | test.Id = "123"; 39 | assert.equal(test.toString(), ``); 40 | }); 41 | 42 | it("namespace", () => { 43 | @XmlElement({ localName: "test", namespaceURI: "http://some.com" }) 44 | class Test extends XmlObject { 45 | @XmlAttribute({ localName: "id", defaultValue: "1" }) 46 | public Id: string = "test"; 47 | } 48 | 49 | const test = new Test(); 50 | 51 | assert.equal(test.toString(), ``); 52 | test.Id = "1"; 53 | assert.equal(test.toString(), ``); 54 | }); 55 | 56 | it("prefix with namespace", () => { 57 | @XmlElement({ localName: "test", prefix: "sm", namespaceURI: "http://some.com" }) 58 | class Test extends XmlObject { 59 | @XmlAttribute({ localName: "id" }) 60 | public Id: string = "test"; 61 | } 62 | 63 | const test = new Test(); 64 | 65 | assert.equal(test.toString(), ``); 66 | }); 67 | 68 | it("prefix without namespace", () => { 69 | @XmlElement({ localName: "test", prefix: "sm" }) 70 | class Test extends XmlObject { 71 | @XmlAttribute({ localName: "id", defaultValue: "" }) 72 | public Id: string = "test"; 73 | } 74 | 75 | const test = new Test(); 76 | 77 | assert.equal(test.toString(), ``); 78 | }); 79 | 80 | context("simple child", () => { 81 | it("default value", () => { 82 | @XmlElement({ localName: "test" }) 83 | class Test extends XmlObject { 84 | @XmlChildElement({ defaultValue: "" }) 85 | public Child: string; 86 | } 87 | 88 | const test = new Test(); 89 | 90 | test.Child = "Hello"; 91 | assert.equal(test.toString(), `Hello`); 92 | }); 93 | 94 | it("default value and required", () => { 95 | @XmlElement({ localName: "test" }) 96 | class Test extends XmlObject { 97 | @XmlChildElement({ required: true }) 98 | public Child: string = "1"; 99 | } 100 | 101 | const test = new Test(); 102 | 103 | assert.equal(test.toString(), `1`); 104 | }); 105 | 106 | it("changed name", () => { 107 | @XmlElement({ localName: "test" }) 108 | class Test extends XmlObject { 109 | @XmlChildElement({ localName: "ch", defaultValue: "1" }) 110 | public Child: string; 111 | } 112 | 113 | const test = new Test(); 114 | 115 | test.Child = "Hello"; 116 | assert.equal(test.toString(), `Hello`); 117 | }); 118 | 119 | it("namespace", () => { 120 | @XmlElement({ localName: "test" }) 121 | class Test extends XmlObject { 122 | @XmlChildElement({ localName: "ch", defaultValue: "1", namespaceURI: "http://some.com" }) 123 | public Child: string; 124 | } 125 | 126 | const test = new Test(); 127 | 128 | test.Child = "Hello"; 129 | assert.equal(test.toString(), `Hello`); 130 | }); 131 | 132 | it("prefix", () => { 133 | @XmlElement({ localName: "test" }) 134 | class Test extends XmlObject { 135 | @XmlChildElement({ localName: "ch", defaultValue: "1", namespaceURI: "http://some.com", prefix: "px" }) 136 | public Child: string; 137 | } 138 | 139 | const test = new Test(); 140 | 141 | test.Child = "Hello"; 142 | assert.equal(test.toString(), `Hello`); 143 | }); 144 | }); 145 | 146 | context("Child", () => { 147 | 148 | @XmlElement({ localName: "base" }) 149 | class XmlBase extends XmlObject { 150 | @XmlAttribute({ localName: "id", defaultValue: "" }) 151 | public Id: string; 152 | } 153 | 154 | @XmlElement({ localName: "child1" }) 155 | class Child1 extends XmlBase { 156 | @XmlChildElement({ localName: "text", defaultValue: 5, converter: XmlNumberConverter }) 157 | public Value: number; 158 | } 159 | @XmlElement({ localName: "child2", namespaceURI: "http://number.com" }) 160 | class Child2 extends XmlBase { 161 | @XmlChildElement({ localName: "text", defaultValue: new Uint8Array([1, 0, 1]), converter: XmlBase64Converter, required: true }) 162 | public Value: Uint8Array; 163 | } 164 | 165 | @XmlElement({ localName: "root" }) 166 | class Root extends XmlBase { 167 | @XmlChildElement({ localName: "name", required: true }) 168 | public Name: string; 169 | 170 | @XmlChildElement({ parser: Child1 }) 171 | public ChildOptional: Child1; 172 | @XmlChildElement({ parser: Child2, required: true }) 173 | public ChildRequired: Child2; 174 | } 175 | 176 | @XmlElement({ localName: "Item" }) 177 | class Item extends XmlObject { 178 | @XmlChildElement({ localName: "Id"}) 179 | public id: number; 180 | @XmlChildElement({ localName: "Name"}) 181 | public name: string; 182 | } 183 | it("default", () => { 184 | const root = new Root(); 185 | root.Name = "MyName"; 186 | 187 | root.ChildRequired = new Child2(); 188 | 189 | assert.equal(root.toString(), `MyNameAQAB`); 190 | 191 | root.ChildOptional.Id = "10"; 192 | root.ChildOptional.Value = 12; 193 | root.ChildRequired.Value = new Uint8Array([1, 1, 1]); 194 | 195 | assert.equal(root.toString(), `MyName12AQEB`); 196 | }); 197 | 198 | it("handle collection as simple array", () => { 199 | @XmlElement({ localName: "test" }) 200 | class Test extends XmlObject { 201 | @XmlChildElement({ defaultValue: [] }) 202 | public Items: Array; 203 | } 204 | 205 | const test = new Test(); 206 | 207 | test.Items = [ 208 | new Item({id: 1, name: 'Item 1'}), 209 | new Item({id: 2, name: 'Item 2'}), 210 | new Item({id: 3, name: 'Item 3'}) 211 | ]; 212 | assert.equal(test.toString(), `1Item 12Item 23Item 3`); 213 | }); 214 | }); 215 | }); 216 | 217 | context("LoadXml", () => { 218 | 219 | context("simple", () => { 220 | 221 | it("value", () => { 222 | 223 | @XmlElement({ localName: "test" }) 224 | class XmlTest extends XmlObject { 225 | 226 | @XmlChildElement({ localName: "value", defaultValue: "" }) 227 | public Value: string; 228 | 229 | } 230 | 231 | const doc = Parse(`123`); 232 | 233 | const test = new XmlTest(); 234 | test.LoadXml(doc.documentElement); 235 | 236 | assert.equal(test.Value, "123"); 237 | 238 | }); 239 | 240 | it("required with error", () => { 241 | 242 | @XmlElement({ localName: "test" }) 243 | class XmlTest extends XmlObject { 244 | 245 | @XmlChildElement({ localName: "value", defaultValue: "", required: true }) 246 | public Value: string; 247 | 248 | } 249 | 250 | const doc = Parse(``); 251 | 252 | const test = new XmlTest(); 253 | 254 | assert.throws(() => { 255 | test.LoadXml(doc.documentElement); 256 | }); 257 | 258 | }); 259 | 260 | it("required", () => { 261 | 262 | @XmlElement({ localName: "test" }) 263 | class XmlTest extends XmlObject { 264 | 265 | @XmlChildElement({ localName: "value", defaultValue: "", required: true }) 266 | public Value: string; 267 | 268 | } 269 | 270 | const doc = Parse(`123`); 271 | 272 | const test = new XmlTest(); 273 | test.LoadXml(doc.documentElement); 274 | 275 | assert.equal(test.Value, "123"); 276 | 277 | }); 278 | 279 | it("converter", () => { 280 | 281 | @XmlElement({ localName: "test" }) 282 | class XmlTest extends XmlObject { 283 | 284 | @XmlChildElement({ localName: "value", defaultValue: null, converter: XmlBase64Converter }) 285 | public Value: Uint8Array | null; 286 | 287 | } 288 | 289 | const doc = Parse(`AQAB`); 290 | 291 | const test = new XmlTest(); 292 | test.LoadXml(doc.documentElement); 293 | 294 | assert.equal(test.Value instanceof Uint8Array, true); 295 | assert.equal(test.Value!.length, 3); 296 | 297 | }); 298 | 299 | it("namespace", () => { 300 | 301 | @XmlElement({ localName: "test" }) 302 | class XmlTest extends XmlObject { 303 | 304 | @XmlChildElement({ localName: "value", defaultValue: "", namespaceURI: "http://some.com" }) 305 | public Value: string; 306 | 307 | } 308 | 309 | const doc = Parse(`Text`); 310 | 311 | const test = new XmlTest(); 312 | test.LoadXml(doc.documentElement); 313 | 314 | assert.equal(test.Value, "Text"); 315 | }); 316 | 317 | }); 318 | 319 | context("child", () => { 320 | 321 | it("simple", () => { 322 | @XmlElement({ localName: "value" }) 323 | class XmlChild extends XmlObject { 324 | 325 | @XmlAttribute({ localName: "id", defaultValue: "" }) 326 | public Id: string; 327 | 328 | } 329 | 330 | @XmlElement({ localName: "test" }) 331 | class XmlTest extends XmlObject { 332 | 333 | @XmlChildElement({ required: true, parser: XmlChild }) 334 | public Value: XmlChild; 335 | 336 | } 337 | 338 | const test = new XmlTest(); 339 | test.LoadXml(``); 340 | 341 | assert.equal(test.Value.Id, "123"); 342 | }); 343 | 344 | it("required", () => { 345 | @XmlElement({ localName: "value" }) 346 | class XmlChild extends XmlObject { 347 | 348 | @XmlAttribute({ localName: "id", defaultValue: "" }) 349 | public Id: string; 350 | 351 | } 352 | 353 | @XmlElement({ localName: "test" }) 354 | class XmlTest extends XmlObject { 355 | 356 | @XmlChildElement({ required: true, parser: XmlChild }) 357 | public Value: XmlChild; 358 | 359 | } 360 | 361 | const doc = Parse(``); 362 | 363 | const test = new XmlTest(); 364 | assert.throws(() => { 365 | test.LoadXml(doc.documentElement); 366 | }); 367 | 368 | }); 369 | 370 | it("namespace", () => { 371 | @XmlElement({ localName: "value", namespaceURI: "http://some.com" }) 372 | class XmlChild extends XmlObject { 373 | 374 | @XmlAttribute({ localName: "id", defaultValue: "" }) 375 | public Id: string; 376 | 377 | } 378 | 379 | @XmlElement({ localName: "test" }) 380 | class XmlTest extends XmlObject { 381 | 382 | @XmlChildElement({ required: true, parser: XmlChild }) 383 | public Value: XmlChild; 384 | 385 | } 386 | 387 | const doc = Parse(``); 388 | 389 | const test = new XmlTest(); 390 | test.LoadXml(doc.documentElement); 391 | 392 | assert.equal(test.Value.Id, "123"); 393 | assert.equal(test.Value.Prefix, "p"); 394 | assert.equal(test.Value.NamespaceURI, "http://some.com"); 395 | 396 | }); 397 | 398 | }); 399 | 400 | }); 401 | 402 | }); 403 | 404 | context("Attribute", () => { 405 | 406 | context("GetXml", () => { 407 | 408 | @XmlElement({ localName: "test" }) 409 | class Test extends XmlObject { 410 | 411 | @XmlAttribute() 412 | public Id?: string; 413 | 414 | @XmlAttribute({ localName: "num", defaultValue: 0, converter: XmlNumberConverter }) 415 | public ConvertNumber?: number; 416 | 417 | @XmlAttribute({ localName: "b64", converter: XmlBase64Converter }) 418 | public ConvertB64?: Uint8Array; 419 | 420 | @XmlAttribute({ defaultValue: "none" }) 421 | public Attr1?: string; 422 | 423 | @XmlAttribute({ required: true, localName: "required" }) 424 | public Required: string; 425 | 426 | protected name = "test"; 427 | } 428 | 429 | const test = new Test(); 430 | 431 | it("with required empty attribute", () => { 432 | 433 | assert.equal(test.toString(), ""); 434 | 435 | }); 436 | 437 | it("with filled empty attribute and lower case name", () => { 438 | test.Required = "some"; 439 | assert.equal(test.toString(), ``); 440 | }); 441 | 442 | it("with different default value", () => { 443 | assert.equal(test.Attr1, "none", "Doesn't have default value for decoration setting"); 444 | test.Attr1 = "wow"; 445 | assert.equal(test.Attr1, "wow"); 446 | assert.equal(test.toString(), ``); 447 | }); 448 | 449 | it("with default value", () => { 450 | test.Attr1 = "none"; 451 | assert.equal(test.Attr1, "none"); 452 | assert.equal(test.toString(), ``); 453 | }); 454 | 455 | it("with number converter value", () => { 456 | test.ConvertNumber = 1; 457 | assert.equal(test.toString(), ``); 458 | }); 459 | 460 | it("with base64 converter value", () => { 461 | test.ConvertNumber = 0; 462 | test.ConvertB64 = new Uint8Array([1, 0, 1]); 463 | assert.equal(test.toString(), ``); 464 | }); 465 | 466 | it("witd Id", () => { 467 | test.ConvertB64 = undefined; // remove value 468 | test.Id = "123"; 469 | assert.equal(test.toString(), ``); 470 | }); 471 | 472 | }); 473 | 474 | context("LoadXml", () => { 475 | 476 | it("simple", () => { 477 | 478 | @XmlElement({ localName: "test" }) 479 | class XmlTest extends XmlObject { 480 | 481 | @XmlAttribute({ localName: "id", defaultValue: "" }) 482 | public Id: string; 483 | 484 | } 485 | 486 | const doc = Parse(``); 487 | 488 | const test = new XmlTest(); 489 | test.LoadXml(doc.documentElement); 490 | 491 | assert.equal(test.Id, "123"); 492 | 493 | }); 494 | 495 | it("required with error", () => { 496 | 497 | @XmlElement({ localName: "test" }) 498 | class XmlTest extends XmlObject { 499 | 500 | @XmlAttribute({ localName: "id", required: true }) 501 | public Id: string; 502 | 503 | } 504 | 505 | const doc = Parse(``); 506 | 507 | const test = new XmlTest(); 508 | 509 | assert.throws(() => { 510 | test.LoadXml(doc.documentElement); 511 | }); 512 | 513 | }); 514 | 515 | it("required", () => { 516 | 517 | @XmlElement({ localName: "test" }) 518 | class XmlTest extends XmlObject { 519 | 520 | @XmlAttribute({ localName: "id", required: true }) 521 | public Id: string; 522 | 523 | } 524 | 525 | const doc = Parse(``); 526 | 527 | const test = new XmlTest(); 528 | test.LoadXml(doc.documentElement); 529 | 530 | assert.equal(test.Id, "123"); 531 | }); 532 | 533 | it("namespace", () => { 534 | 535 | @XmlElement({ localName: "test" }) 536 | class XmlTest extends XmlObject { 537 | 538 | @XmlAttribute({ localName: "id", defaultValue: "", required: true, namespaceURI: "http://some.com" }) 539 | public Id: string; 540 | 541 | } 542 | 543 | { 544 | // correct 545 | const doc = Parse(``); 546 | 547 | const test = new XmlTest(); 548 | test.LoadXml(doc.documentElement); 549 | 550 | assert.equal(test.Id, "123"); 551 | } 552 | 553 | { 554 | // error 555 | const doc = Parse(``); 556 | 557 | const test = new XmlTest(); 558 | assert.throws(() => { 559 | test.LoadXml(doc.documentElement); 560 | }); 561 | } 562 | }); 563 | 564 | it("converter", () => { 565 | 566 | @XmlElement({ localName: "test" }) 567 | class XmlTest extends XmlObject { 568 | 569 | @XmlAttribute({ localName: "value", defaultValue: null, converter: XmlBase64Converter }) 570 | public Value: Uint8Array | null; 571 | 572 | } 573 | 574 | const doc = Parse(``); 575 | 576 | const test = new XmlTest(); 577 | test.LoadXml(doc.documentElement); 578 | 579 | assert.equal(test.Value instanceof Uint8Array, true); 580 | assert.equal(test.Value!.length, 3); 581 | }); 582 | 583 | }); 584 | 585 | }); 586 | 587 | context("Collection", () => { 588 | 589 | context("GetXml", () => { 590 | 591 | it("simple", () => { 592 | 593 | @XmlElement({ localName: "transform" }) 594 | class XmlTransform extends XmlObject { 595 | @XmlChildElement({ defaultValue: "" }) 596 | public Value: string; 597 | } 598 | 599 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 600 | class XmlTransforms extends XmlCollection { 601 | 602 | } 603 | 604 | @XmlElement({ localName: "test" }) 605 | class XmlTest extends XmlObject { 606 | @XmlChildElement({ minOccurs: 1, parser: XmlTransforms }) 607 | public Transforms: XmlTransforms; 608 | } 609 | 610 | const test = new XmlTest(); 611 | 612 | test.Transforms = new XmlTransforms(); 613 | const t = new XmlTransform(); 614 | t.Value = "Hello"; 615 | test.Transforms.Add(t); 616 | 617 | assert.equal(test.toString(), `Hello`); 618 | 619 | }); 620 | 621 | it("no root", () => { 622 | 623 | @XmlElement({ localName: "transform" }) 624 | class XmlTransform extends XmlObject { 625 | @XmlChildElement({ defaultValue: "" }) 626 | public Value: string; 627 | } 628 | 629 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 630 | class XmlTransforms extends XmlCollection { 631 | 632 | } 633 | 634 | @XmlElement({ localName: "test" }) 635 | class XmlTest extends XmlObject { 636 | @XmlChildElement({ minOccurs: 1, parser: XmlTransforms, noRoot: true }) 637 | public Transforms: XmlTransforms; 638 | } 639 | 640 | const test = new XmlTest(); 641 | 642 | test.Transforms = new XmlTransforms(); 643 | const t = new XmlTransform(); 644 | t.Value = "Hello"; 645 | test.Transforms.Add(t); 646 | 647 | assert.equal(test.toString(), `Hello`); 648 | 649 | }); 650 | 651 | it("no root occurs", () => { 652 | 653 | @XmlElement({ localName: "transform" }) 654 | class XmlTransform extends XmlObject { 655 | @XmlChildElement({ defaultValue: "" }) 656 | public Value: string; 657 | constructor(value?: string) { 658 | super(); 659 | if (value) { 660 | this.Value = value; 661 | } 662 | } 663 | } 664 | 665 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 666 | class XmlTransforms extends XmlCollection { 667 | } 668 | 669 | @XmlElement({ localName: "test" }) 670 | class XmlTest extends XmlObject { 671 | @XmlChildElement({ minOccurs: 1, maxOccurs: 4, parser: XmlTransforms, noRoot: true }) 672 | public Transforms: XmlTransforms; 673 | } 674 | 675 | const test = new XmlTest(); 676 | 677 | test.Transforms = new XmlTransforms(); 678 | 679 | assert.throws(() => { 680 | test.toString(); 681 | }); 682 | 683 | test.Transforms.Add(new XmlTransform("1")); 684 | test.Transforms.Add(new XmlTransform("2")); 685 | test.Transforms.Add(new XmlTransform("3")); 686 | test.Transforms.Add(new XmlTransform("4")); 687 | assert.equal(test.toString(), `1234`); 688 | 689 | test.Transforms.Add(new XmlTransform("4")); 690 | assert.throws(() => { 691 | test.toString(); 692 | }); 693 | }); 694 | 695 | it("occurs", () => { 696 | 697 | @XmlElement({ localName: "transform" }) 698 | class XmlTransform extends XmlObject { 699 | @XmlChildElement({ defaultValue: "" }) 700 | public Value: string; 701 | constructor(value?: string) { 702 | super(); 703 | if (value) { 704 | this.Value = value; 705 | } 706 | } 707 | } 708 | 709 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 710 | class XmlTransforms extends XmlCollection { 711 | } 712 | 713 | @XmlElement({ localName: "test" }) 714 | class XmlTest extends XmlObject { 715 | @XmlChildElement({ minOccurs: 1, maxOccurs: 4, parser: XmlTransforms }) 716 | public Transforms: XmlTransforms; 717 | } 718 | 719 | const test = new XmlTest(); 720 | 721 | test.Transforms = new XmlTransforms(); 722 | 723 | assert.throws(() => { 724 | test.toString(); 725 | }); 726 | 727 | test.Transforms.Add(new XmlTransform("1")); 728 | test.Transforms.Add(new XmlTransform("2")); 729 | test.Transforms.Add(new XmlTransform("3")); 730 | test.Transforms.Add(new XmlTransform("4")); 731 | assert.equal(test.toString(), `1234`); 732 | 733 | test.Transforms.Add(new XmlTransform("4")); 734 | assert.throws(() => { 735 | test.toString(); 736 | }); 737 | }); 738 | 739 | }); 740 | 741 | context("LoadXml", () => { 742 | 743 | it("simple", () => { 744 | 745 | @XmlElement({ localName: "transform" }) 746 | class XmlTransform extends XmlObject { 747 | @XmlChildElement({ defaultValue: "" }) 748 | public Value: string; 749 | } 750 | 751 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 752 | class XmlTransforms extends XmlCollection { 753 | 754 | } 755 | 756 | @XmlElement({ localName: "test" }) 757 | class XmlTest extends XmlObject { 758 | @XmlChildElement({ minOccurs: 1, parser: XmlTransforms }) 759 | public Transforms: XmlTransforms; 760 | } 761 | 762 | const doc = Parse("Hello"); 763 | const test = new XmlTest(); 764 | test.LoadXml(doc.documentElement); 765 | 766 | assert.equal(test.Transforms.Count, 1); 767 | assert.equal(test.Transforms.Item(0)!.Value, "Hello"); 768 | 769 | }); 770 | 771 | it("no root", () => { 772 | 773 | @XmlElement({ localName: "transform" }) 774 | class XmlTransform extends XmlObject { 775 | @XmlChildElement({ defaultValue: "" }) 776 | public Value: string; 777 | } 778 | 779 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 780 | class XmlTransforms extends XmlCollection { 781 | 782 | } 783 | 784 | @XmlElement({ localName: "test" }) 785 | class XmlTest extends XmlObject { 786 | @XmlChildElement({ minOccurs: 1, parser: XmlTransforms, noRoot: true }) 787 | public Transforms: XmlTransforms; 788 | } 789 | 790 | const doc = Parse("Hello"); 791 | const test = new XmlTest(); 792 | test.LoadXml(doc.documentElement); 793 | 794 | assert.equal(test.Transforms.Count, 1); 795 | assert.equal(test.Transforms.Item(0)!.Value, "Hello"); 796 | 797 | }); 798 | 799 | it("wrong min occurs", () => { 800 | 801 | @XmlElement({ localName: "transform" }) 802 | class XmlTransform extends XmlObject { 803 | @XmlChildElement({ defaultValue: "" }) 804 | public Value: string; 805 | } 806 | 807 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 808 | class XmlTransforms extends XmlCollection { 809 | 810 | } 811 | 812 | @XmlElement({ localName: "test" }) 813 | class XmlTest extends XmlObject { 814 | @XmlChildElement({ minOccurs: 2, parser: XmlTransforms, noRoot: true }) 815 | public Transforms: XmlTransforms; 816 | } 817 | 818 | const doc = Parse("Hello"); 819 | const test = new XmlTest(); 820 | 821 | assert.throws(() => { 822 | test.LoadXml(doc.documentElement); 823 | }); 824 | 825 | }); 826 | it("wrong max occurs", () => { 827 | 828 | @XmlElement({ localName: "transform" }) 829 | class XmlTransform extends XmlObject { 830 | @XmlChildElement({ defaultValue: "" }) 831 | public Value: string; 832 | } 833 | 834 | @XmlElement({ localName: "transforms", parser: XmlTransform }) 835 | class XmlTransforms extends XmlCollection { 836 | 837 | } 838 | 839 | @XmlElement({ localName: "test" }) 840 | class XmlTest extends XmlObject { 841 | @XmlChildElement({ maxOccurs: 1, parser: XmlTransforms, noRoot: true }) 842 | public Transforms: XmlTransforms; 843 | } 844 | 845 | const doc = Parse("HelloHello"); 846 | const test = new XmlTest(); 847 | 848 | assert.throws(() => { 849 | test.LoadXml(doc.documentElement); 850 | }); 851 | 852 | }); 853 | 854 | }); 855 | 856 | }); 857 | 858 | it("extends", () => { 859 | 860 | @XmlElement({ 861 | localName: "first", 862 | namespaceURI: "http://some.com", 863 | prefix: "p", 864 | }) 865 | class XmlFirst extends XmlObject { 866 | 867 | @XmlAttribute() 868 | public Id: string; 869 | 870 | } 871 | 872 | @XmlElement({ 873 | localName: "second", 874 | }) 875 | class XmlSecond extends XmlFirst { 876 | } 877 | 878 | const first = new XmlFirst(); 879 | first.Id = "1"; 880 | assert.equal(first.toString(), ``); 881 | 882 | const second = new XmlSecond(); 883 | second.Id = "2"; 884 | assert.equal(second.toString(), ``); 885 | assert.equal((XmlFirst as any).prefix, (XmlSecond as any).prefix); 886 | assert.equal((XmlFirst as any).namespaceURI, (XmlSecond as any).namespaceURI); 887 | 888 | }); 889 | 890 | context("XmlContent", () => { 891 | 892 | it("Content", () => { 893 | 894 | @XmlElement({ 895 | localName: "test", 896 | namespaceURI: "http://some.com", 897 | prefix: "p", 898 | }) 899 | class XmlTest extends XmlObject { 900 | 901 | @XmlContent() 902 | public Value: string; 903 | 904 | } 905 | 906 | const test = new XmlTest(); 907 | 908 | assert.equal(test.Value, undefined); 909 | 910 | test.Value = "Test"; 911 | 912 | const xml = test.toString(); 913 | assert.equal(xml, `Test`); 914 | const test2 = XmlTest.LoadXml(xml); 915 | 916 | assert.equal(test2.Value, test.Value); 917 | }); 918 | 919 | it("Converter", () => { 920 | 921 | @XmlElement({ 922 | localName: "test", 923 | namespaceURI: "http://some.com", 924 | prefix: "p", 925 | }) 926 | class XmlTest extends XmlObject { 927 | 928 | @XmlContent({ 929 | converter: XmlNumberConverter, 930 | }) 931 | public Value: number; 932 | 933 | } 934 | 935 | const test = new XmlTest(); 936 | 937 | assert.equal(test.Value, undefined); 938 | 939 | test.Value = 123; 940 | 941 | const xml = test.toString(); 942 | assert.equal(xml, `123`); 943 | 944 | const test2 = XmlTest.LoadXml(xml); 945 | 946 | assert.equal(test2.Value, test.Value); 947 | }); 948 | 949 | it("Required", () => { 950 | 951 | @XmlElement({ 952 | localName: "test", 953 | namespaceURI: "http://some.com", 954 | prefix: "p", 955 | }) 956 | class XmlTest extends XmlObject { 957 | 958 | @XmlAttribute() 959 | public Id: string; 960 | 961 | @XmlContent({ 962 | required: true, 963 | }) 964 | public Value: string; 965 | 966 | } 967 | 968 | const test = new XmlTest(); 969 | 970 | assert.equal(test.Value, undefined); 971 | 972 | test.Id = "1"; 973 | 974 | assert.throws(() => test.GetXml()); 975 | 976 | test.Value = "test"; 977 | 978 | const xml = test.toString(); 979 | assert.equal(xml, `test`); 980 | 981 | assert.throws(() => XmlTest.LoadXml(``)); 982 | 983 | const test2 = XmlTest.LoadXml(xml); 984 | 985 | assert.equal(test2.Value, test.Value); 986 | }); 987 | 988 | it("Default value", () => { 989 | @XmlElement({ 990 | localName: "test", 991 | namespaceURI: "http://some.com", 992 | prefix: "p", 993 | }) 994 | class XmlTest extends XmlObject { 995 | 996 | @XmlAttribute() 997 | public Id: string; 998 | 999 | @XmlContent({ 1000 | required: true, 1001 | defaultValue: "test", 1002 | }) 1003 | public Value: string; 1004 | 1005 | } 1006 | 1007 | const test = new XmlTest(); 1008 | 1009 | assert.equal(test.Value, "test"); 1010 | 1011 | test.Id = "1"; 1012 | 1013 | const xml = test.toString(); 1014 | assert.equal(xml, `test`); 1015 | 1016 | assert.throws(() => XmlTest.LoadXml(``)); 1017 | 1018 | const test2 = XmlTest.LoadXml(xml); 1019 | 1020 | assert.equal(test2.Value, test.Value); 1021 | }); 1022 | 1023 | }); 1024 | 1025 | }); 1026 | -------------------------------------------------------------------------------- /test/error.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { XE, XmlError } from "../src"; 3 | 4 | describe("Error", () => { 5 | 6 | it("Throw error", () => { 7 | assert.throws(() => { 8 | throw new XmlError(XE.NONE); 9 | }); 10 | }); 11 | 12 | it("Error params", () => { 13 | const error = new XmlError(XE.NULL_REFERENCE); 14 | assert.equal(error.code, 1); 15 | assert.equal(error.name, "XmlError"); 16 | assert.equal(error.message, "XMLJS0001: Null reference"); 17 | }); 18 | 19 | it("Error template", () => { 20 | const error = new XmlError(XE.NULL_PARAM, "Object", "name"); 21 | assert.equal(error.code, 2); 22 | // assert.equal(error.name, "XmlError"); 23 | assert.equal(error.message, "XMLJS0002: 'Object' has empty 'name' object"); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /test/get_load.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { XmlAttribute, XmlChildElement, XmlElement } from "../src"; 3 | import { XmlCollection, XmlObject } from "../src"; 4 | import { XmlNumberConverter } from "../src"; 5 | 6 | context("GetXml/LoadXml/HasChanged", () => { 7 | 8 | it("Simple", () => { 9 | @XmlElement({ 10 | localName: "test", 11 | namespaceURI: "http://some.com", 12 | }) 13 | class Test extends XmlObject { 14 | 15 | @XmlAttribute({ localName: "id", defaultValue: "1" }) 16 | public Id: string; 17 | @XmlAttribute({ localName: "class", defaultValue: "2", required: true }) 18 | public Class: string; 19 | 20 | @XmlChildElement({ localName: "algorithm", namespaceURI: "http://some.com", defaultValue: "3" }) 21 | public Algorithm: string; 22 | @XmlChildElement({ localName: "method", namespaceURI: "http://some.com", defaultValue: "4", required: true }) 23 | public Method: string; 24 | } 25 | 26 | const t = new Test(); 27 | 28 | assert.equal(t.toString(), "", "initialized class should be empty"); 29 | 30 | t.Id = "123"; 31 | 32 | const xml = `4`; 33 | assert.equal(t.toString(), xml); 34 | 35 | const p = new Test(); 36 | p.LoadXml(xml); 37 | 38 | assert.equal(p.Id, "123"); 39 | assert.equal(p.Class, "2"); 40 | assert.equal(p.Algorithm, "3"); 41 | assert.equal(p.Method, "4"); 42 | assert.equal(p.HasChanged(), false); 43 | }); 44 | 45 | it("With child element", () => { 46 | 47 | @XmlElement({ 48 | localName: "child", 49 | namespaceURI: "http://some.com", 50 | }) 51 | class Child extends XmlObject { 52 | @XmlAttribute({ localName: "id", defaultValue: "" }) 53 | public Id: string; 54 | } 55 | 56 | @XmlElement({ 57 | localName: "test", 58 | namespaceURI: "http://some.com", 59 | }) 60 | class Test extends XmlObject { 61 | 62 | @XmlChildElement({ parser: Child }) 63 | public Child: Child; 64 | } 65 | 66 | const t = new Test(); 67 | 68 | assert.equal(t.toString(), "", "initialized class should be empty"); 69 | assert.equal(t.HasChanged(), false); 70 | 71 | t.Child.Id = "1"; 72 | 73 | const xml = ``; 74 | 75 | assert.equal(t.HasChanged(), true); 76 | assert.equal(t.toString(), xml); 77 | assert.equal(t.HasChanged(), false); 78 | 79 | const p = Test.LoadXml(xml); 80 | 81 | assert.equal(!!p.Child, true); 82 | assert.equal(p.Child.Id, "1"); 83 | assert.equal(p.Child.NamespaceURI, "http://some.com"); 84 | assert.equal(p.HasChanged(), false); 85 | }); 86 | 87 | it("With child XmlCollection", () => { 88 | 89 | @XmlElement({ 90 | localName: "child", 91 | namespaceURI: "http://some.com", 92 | }) 93 | class Child extends XmlObject { 94 | @XmlAttribute({ localName: "id", defaultValue: "" }) 95 | public Id: string; 96 | } 97 | 98 | @XmlElement({ 99 | localName: "childs", 100 | namespaceURI: "http://some.com", 101 | parser: Child, 102 | }) 103 | class Childs extends XmlCollection { 104 | } 105 | 106 | @XmlElement({ 107 | localName: "test", 108 | namespaceURI: "http://some.com", 109 | }) 110 | class Test extends XmlObject { 111 | 112 | @XmlChildElement({ parser: Childs }) 113 | public Childs: Childs; 114 | } 115 | 116 | const t = new Test(); 117 | 118 | assert.equal(t.toString(), "", "initialized class should be empty"); 119 | 120 | t.Childs.Add(new Child()); 121 | 122 | const xml = ``; 123 | 124 | assert.equal(t.toString(), xml); 125 | 126 | const p = Test.LoadXml(xml); 127 | 128 | assert.equal(p.Childs.Count, 0); 129 | assert.equal(p.HasChanged(), false); 130 | }); 131 | 132 | it("With child requierd XmlCollection", () => { 133 | 134 | @XmlElement({ 135 | localName: "child", 136 | namespaceURI: "http://some.com", 137 | }) 138 | class Child extends XmlObject { 139 | @XmlAttribute({ localName: "id", defaultValue: "" }) 140 | public Id: string; 141 | } 142 | 143 | @XmlElement({ 144 | localName: "childs", 145 | namespaceURI: "http://some.com", 146 | parser: Child, 147 | }) 148 | class Childs extends XmlCollection { 149 | } 150 | 151 | @XmlElement({ 152 | localName: "test", 153 | namespaceURI: "http://some.com", 154 | }) 155 | class Test extends XmlObject { 156 | 157 | @XmlChildElement({ parser: Childs }) 158 | public Childs: Childs; 159 | @XmlChildElement({ localName: "required", parser: Childs, minOccurs: 1 }) 160 | public Childs2: Childs; 161 | } 162 | 163 | const t = new Test(); 164 | 165 | assert.equal(t.toString(), ""); 166 | // assert.throws(() => t.toString()); 167 | 168 | t.Childs.Add(new Child()); 169 | 170 | assert.throws(() => t.toString()); 171 | t.Childs2.Add(new Child()); 172 | assert.throws(() => t.toString()); 173 | t.Childs2.Item(0) !.Id = "test"; 174 | 175 | const xml = ``; 176 | 177 | assert.equal(t.toString(), xml); 178 | 179 | const p = Test.LoadXml(xml); 180 | assert.equal(p.Childs.Count, 0); 181 | assert.equal(p.Childs2.LocalName, "required"); 182 | assert.equal(p.Childs2.Count, 1); 183 | assert.equal(p.Childs2.Item(0) !.Id, "test"); 184 | assert.equal(p.HasChanged(), false); 185 | }); 186 | 187 | it("praser for attributes", () => { 188 | 189 | @XmlElement({ 190 | localName: "test", 191 | namespaceURI: "https://some.com", 192 | }) 193 | class Test extends XmlObject { 194 | 195 | @XmlAttribute({ 196 | localName: "value", 197 | }) 198 | public Value = "test"; 199 | 200 | @XmlAttribute({ 201 | localName: "version", 202 | converter: XmlNumberConverter, 203 | }) 204 | public Version: number; 205 | 206 | } 207 | 208 | const t = new Test(); 209 | 210 | let xml = ``; 211 | assert.equal(t.toString(), xml); 212 | Test.LoadXml(xml); 213 | 214 | t.Version = 1; 215 | 216 | xml = ``; 217 | assert.equal(t.toString(), xml); 218 | Test.LoadXml(xml); 219 | }); 220 | 221 | it("praser for child element", () => { 222 | 223 | @XmlElement({ 224 | localName: "test", 225 | namespaceURI: "https://some.com", 226 | }) 227 | class Test extends XmlObject { 228 | 229 | @XmlChildElement({ 230 | localName: "value", 231 | namespaceURI: "https://some.com", 232 | }) 233 | public Value = "test"; 234 | 235 | @XmlChildElement({ 236 | localName: "version", 237 | namespaceURI: "https://some.com", 238 | converter: XmlNumberConverter, 239 | }) 240 | public Version: number; 241 | 242 | } 243 | 244 | const t = new Test(); 245 | 246 | let xml = `test`; 247 | assert.equal(t.toString(), xml); 248 | Test.LoadXml(xml); 249 | 250 | t.Version = 1; 251 | 252 | xml = `test1`; 253 | assert.equal(t.toString(), xml); 254 | Test.LoadXml(xml); 255 | }); 256 | 257 | }); 258 | -------------------------------------------------------------------------------- /test/namespace_manager.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { NamespaceManager } from "../src"; 3 | 4 | context("NamespaceManager", () => { 5 | 6 | const nm = new NamespaceManager(); 7 | 8 | before(() => { 9 | nm.Add({ 10 | prefix: "n1", 11 | namespace: "http://namespace1", 12 | }); 13 | nm.Add({ 14 | prefix: "n2", 15 | namespace: "http://namespace2", 16 | }); 17 | nm.Add({ 18 | prefix: "n3", 19 | namespace: "http://namespace3", 20 | }); 21 | }); 22 | 23 | context("GetPrefix", () => { 24 | it("Empty", () => { 25 | const n = nm.GetPrefix("n4"); 26 | assert.equal(!!n, false); 27 | }); 28 | it("Exist", () => { 29 | const n = nm.GetPrefix("n2"); 30 | assert.equal(!!n, true); 31 | assert.equal(n!.prefix, "n2"); 32 | assert.equal(n!.namespace, "http://namespace2"); 33 | }); 34 | it("Start index more than list size", () => { 35 | const n = nm.GetPrefix("n3", 10); 36 | assert.equal(!!n, true); 37 | assert.equal(n!.prefix, "n3"); 38 | assert.equal(n!.namespace, "http://namespace3"); 39 | }); 40 | }); 41 | 42 | context("GetNamespace", () => { 43 | it("Empty", () => { 44 | const n = nm.GetNamespace("http://namespace4"); 45 | assert.equal(!!n, false); 46 | }); 47 | it("Exist", () => { 48 | const n = nm.GetNamespace("http://namespace3"); 49 | assert.equal(!!n, true); 50 | assert.equal(n!.prefix, "n3"); 51 | assert.equal(n!.namespace, "http://namespace3"); 52 | }); 53 | it("Start index more than list size", () => { 54 | const n = nm.GetNamespace("http://namespace3", 10); 55 | assert.equal(!!n, true); 56 | assert.equal(n!.prefix, "n3"); 57 | assert.equal(n!.namespace, "http://namespace3"); 58 | }); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /test/object.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { XmlAttribute, XmlElement } from "../src"; 3 | import { XmlObject } from "../src"; 4 | 5 | context("XmlObject", () => { 6 | 7 | it("IsEmpty", () => { 8 | 9 | @XmlElement({ localName: "test", namespaceURI: "http://some.com" }) 10 | class Test extends XmlObject { 11 | @XmlAttribute() 12 | public Id: string; 13 | } 14 | 15 | const test = new Test(); 16 | 17 | assert.equal(test.IsEmpty(), true); 18 | test.Id = "1"; 19 | assert.equal(test.IsEmpty(), false); 20 | 21 | const xml = test.toString(); 22 | const test2 = Test.LoadXml(xml); 23 | assert.equal(test2.IsEmpty(), false); 24 | 25 | }); 26 | 27 | context("Get xml Element", () => { 28 | const xml = ``; 29 | let obj: Test; 30 | 31 | @XmlElement({ 32 | localName: "root", 33 | }) 34 | class Test extends XmlObject { 35 | } 36 | before(() => { 37 | obj = Test.LoadXml(xml); 38 | }); 39 | 40 | context("GetElement", () => { 41 | it("required", () => { 42 | assert.throws(() => { 43 | obj.GetElement("NotExit", true); 44 | }); 45 | }); 46 | it("success", () => { 47 | const node = obj.GetElement("third"); 48 | assert.equal(!!node, true); 49 | assert.equal(node!.nodeName, "third"); 50 | }); 51 | it("element not exist", () => { 52 | const obj2 = new Test(); 53 | assert.throws(() => { 54 | obj2.GetElement("third"); 55 | }); 56 | }); 57 | }); 58 | context("GetChild", () => { 59 | it("required", () => { 60 | assert.throws(() => { 61 | obj.GetChild("NotExit", true); 62 | }); 63 | }); 64 | it("success", () => { 65 | const node = obj.GetChild("third"); 66 | assert.equal(!!node, true); 67 | assert.equal(node!.nodeName, "third"); 68 | }); 69 | it("element not exist", () => { 70 | const obj2 = new Test(); 71 | assert.throws(() => { 72 | obj2.GetChild("third"); 73 | }); 74 | }); 75 | }); 76 | context("GetChildren", () => { 77 | it("by name", () => { 78 | const list = obj.GetChildren("child"); 79 | assert.equal(list.length, 2); 80 | }); 81 | it("by name and namespace", () => { 82 | const list = obj.GetChildren("child", "html://n"); 83 | assert.equal(list.length, 1); 84 | }); 85 | it("element not exist", () => { 86 | const obj2 = new Test(); 87 | assert.throws(() => { 88 | obj2.GetChildren("child"); 89 | }); 90 | }); 91 | }); 92 | context("GetFirstChild", () => { 93 | it("by name", () => { 94 | const node = obj.GetFirstChild("child"); 95 | assert.equal(!!node, true); 96 | assert.equal(node!.localName, "child"); 97 | }); 98 | it("by name and namespace", () => { 99 | const node = obj.GetFirstChild("child", "html://n"); 100 | assert.equal(!!node, true); 101 | assert.equal(node!.localName, "child"); 102 | assert.equal(node!.namespaceURI, "html://n"); 103 | }); 104 | it("element not exist", () => { 105 | const obj2 = new Test(); 106 | assert.throws(() => { 107 | obj2.GetFirstChild("child"); 108 | }); 109 | }); 110 | }); 111 | context("GetAttribute", () => { 112 | it("by name", () => { 113 | const attr = obj.GetAttribute("id", "2"); 114 | assert.equal(attr, "0"); 115 | }); 116 | it("by default", () => { 117 | const attr = obj.GetAttribute("test", "3", false); 118 | assert.equal(attr, "3"); 119 | }); 120 | it("required", () => { 121 | assert.throws(() => { 122 | obj.GetAttribute("test", null, true); 123 | }); 124 | }); 125 | it("element not exist", () => { 126 | const obj2 = new Test(); 127 | assert.throws(() => { 128 | obj2.GetAttribute("id", "4"); 129 | }); 130 | }); 131 | }); 132 | context("GetElementById", () => { 133 | it("id", () => { 134 | const el = obj.GetXml()!; 135 | const f = XmlObject.GetElementById(el, "1")!; 136 | assert.equal(f.localName, "first"); 137 | }); 138 | it("Id", () => { 139 | const el = obj.GetXml()!; 140 | const f = XmlObject.GetElementById(el, "2")!; 141 | assert.equal(f.localName, "second"); 142 | }); 143 | it("ID", () => { 144 | const el = obj.GetXml()!; 145 | const f = XmlObject.GetElementById(el, "3")!; 146 | assert.equal(f.localName, "third"); 147 | }); 148 | }); 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { assign, Parse, SelectNamespaces, SelectSingleNode, Stringify } from "../src"; 3 | 4 | context("utils", () => { 5 | 6 | it("assign", () => { 7 | const obj = assign({}, { prop1: 1 }, { prop2: 2 }, { prop3: 3 }, { prop3: 4 }); 8 | 9 | assert.equal(obj.prop1, 1); 10 | assert.equal(obj.prop2, 2); 11 | assert.equal(obj.prop3, 4); 12 | }); 13 | 14 | context("SelectSingleNode", () => { 15 | it("Empty", () => { 16 | const xml = Parse(""); 17 | const node = SelectSingleNode(xml, ".//second"); 18 | assert.equal(node === null, true); 19 | }); 20 | it("First element", () => { 21 | const xml = Parse(``); 22 | const node = SelectSingleNode(xml, ".//child") as Element; 23 | assert.equal(!!node, true); 24 | assert.equal(node!.attributes.length, 1); 25 | assert.equal(node!.attributes[0].value, "1"); 26 | }); 27 | }); 28 | 29 | it("SelectNamespaces", () => { 30 | const xml = Parse(``); 31 | const namespaces = SelectNamespaces(xml.documentElement); 32 | assert.equal(Object.keys(namespaces).length, 3); 33 | assert.equal(namespaces[""], "html://namespace1"); 34 | assert.equal(namespaces.n1, "html://namespace2"); 35 | assert.equal(namespaces.n2, "html://namespace3"); 36 | }); 37 | 38 | it("Parse/Stringify", () => { 39 | const xmlString = ``; 40 | const xml = Parse(xmlString); 41 | const text = Stringify(xml); 42 | assert.equal(xmlString, text); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/xml_collection.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { XmlAttribute, XmlCollection, XmlElement, XmlObject } from "../src"; 3 | 4 | context("XmlCollection", () => { 5 | @XmlElement({ 6 | localName: "child", 7 | namespaceURI: "http://some.com", 8 | }) 9 | class Child extends XmlObject { 10 | @XmlAttribute({ localName: "id", defaultValue: 0 }) 11 | public Id: number; 12 | 13 | constructor(id?: number) { 14 | super(); 15 | 16 | if (id !== void 0) { 17 | this.Id = id; 18 | } 19 | } 20 | } 21 | 22 | @XmlElement({ 23 | localName: "children", 24 | namespaceURI: "http://some.com", 25 | parser: Child, 26 | }) 27 | class Children extends XmlCollection { 28 | } 29 | 30 | it("Pop", () => { 31 | const col = new Children(); 32 | col.Add(new Child(1)); 33 | col.Add(new Child(2)); 34 | col.Add(new Child(3)); 35 | assert.equal(col.Count, 3); 36 | col.Pop(); 37 | assert.equal(col.Count, 2); 38 | assert.equal(col.Item(0)!.Id, 1); 39 | assert.equal(col.Item(1)!.Id, 2); 40 | }); 41 | 42 | it("RemoveAt", () => { 43 | const col = new Children(); 44 | col.Add(new Child(1)); 45 | col.Add(new Child(2)); 46 | col.Add(new Child(3)); 47 | assert.equal(col.Count, 3); 48 | col.RemoveAt(0); 49 | assert.equal(col.Count, 2); 50 | assert.equal(col.Item(0)!.Id, 2); 51 | assert.equal(col.Item(1)!.Id, 3); 52 | }); 53 | 54 | it("Clear", () => { 55 | const col = new Children(); 56 | col.Add(new Child(1)); 57 | col.Add(new Child(2)); 58 | col.Add(new Child(3)); 59 | assert.equal(col.Count, 3); 60 | col.Clear(); 61 | assert.equal(col.Count, 0); 62 | }); 63 | 64 | it("IsEmpty", () => { 65 | const col = new Children(); 66 | assert.equal(col.IsEmpty(), true); 67 | col.Add(new Child(1)); 68 | assert.equal(col.IsEmpty(), false); 69 | }); 70 | 71 | it("ForEach", () => { 72 | const col = new Children(); 73 | col.Add(new Child(1)); 74 | col.Add(new Child(2)); 75 | col.Add(new Child(3)); 76 | 77 | let id = 1; 78 | col.ForEach((item) => { 79 | assert.equal(item.Id, id++); 80 | }); 81 | }); 82 | 83 | it("Map", () => { 84 | const col = new Children(); 85 | col.Add(new Child(1)); 86 | col.Add(new Child(2)); 87 | col.Add(new Child(3)); 88 | 89 | let id = 1; 90 | col 91 | .Map((item) => { 92 | return item.Id; 93 | }) 94 | .ForEach((item) => { 95 | assert.equal(item, id++); 96 | }); 97 | }); 98 | 99 | it("Filter", () => { 100 | const col = new Children(); 101 | col.Add(new Child(1)); 102 | col.Add(new Child(2)); 103 | col.Add(new Child(3)); 104 | 105 | const list = col.Filter((item) => { 106 | return item.Id === 3; 107 | }); 108 | assert.equal(list.Count, 1); 109 | assert.equal(list.Item(0)!.Id, 3); 110 | }); 111 | 112 | it("Sort", () => { 113 | const col = new Children(); 114 | col.Add(new Child(1)); 115 | col.Add(new Child(3)); 116 | col.Add(new Child(2)); 117 | 118 | const list = col.Sort((a, b) => { 119 | if (a.Id > b.Id) { 120 | return -1; 121 | } 122 | if (a.Id < b.Id) { 123 | return 1; 124 | } 125 | return 0; 126 | }); 127 | assert.equal(list.Count, 3); 128 | assert.equal(list.Item(0)!.Id, 3); 129 | assert.equal(list.Item(1)!.Id, 2); 130 | assert.equal(list.Item(2)!.Id, 1); 131 | }); 132 | 133 | it("Every", () => { 134 | const col = new Children(); 135 | col.Add(new Child(1)); 136 | col.Add(new Child(1)); 137 | col.Add(new Child(1)); 138 | 139 | assert.equal(col.Every((item) => { 140 | return item.Id === 1; 141 | }), true); 142 | 143 | col.Add(new Child(2)); 144 | assert.equal(col.Every((item) => { 145 | return item.Id === 1; 146 | }), false); 147 | }); 148 | 149 | context("HasChanged", () => { 150 | 151 | it("Initialized empty collection is not changed", () => { 152 | const col = new Children(); 153 | assert.equal(col.HasChanged(), false); 154 | }); 155 | 156 | it("Update state on item adding", () => { 157 | const col = new Children(); 158 | col.Add(new Child()); 159 | assert.equal(col.HasChanged(), true); 160 | }); 161 | 162 | it("Update state on item removing", () => { 163 | const col = new Children(); 164 | col.Add(new Child()); 165 | col.GetXml(); 166 | assert.equal(col.HasChanged(), false); 167 | col.RemoveAt(0); 168 | assert.equal(col.HasChanged(), true); 169 | }); 170 | 171 | it("Set unchanged state for loaded XML", () => { 172 | const xml = ``; 173 | const col = new Children(); 174 | col.LoadXml(xml); 175 | assert.equal(col.HasChanged(), false); 176 | assert.equal(col.Item(0)!.HasChanged(), false); 177 | }); 178 | 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "tmp", 7 | "strictPropertyInitialization": false, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "forceConsistentCasingInFileNames": true 11 | }, 12 | "exclude": [ 13 | "dist" 14 | ] 15 | } -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": "dist/types", 6 | "emitDeclarationOnly": true, 7 | "removeComments": false 8 | }, 9 | "exclude": [ "dist", "test" ] 10 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "ban-types": false, 9 | "max-line-length": false, 10 | "prefer-for-of": false, 11 | "no-conditional-assignment": false, 12 | "no-reference": false, 13 | "no-var-requires": false, 14 | "object-literal-sort-keys": false, 15 | "object-literal-shorthand": false, 16 | "space-before-function-paren": false, 17 | "max-classes-per-file": false, 18 | "interface-name": false 19 | }, 20 | "rulesDirectory": [] 21 | } --------------------------------------------------------------------------------