├── .gitattributes ├── .eslintignore ├── .prettierignore ├── .gitignore ├── NOTICE ├── test ├── tsconfig.json └── src │ ├── utils.ts │ ├── options.ts │ └── main.ts ├── tsconfig.json ├── .prettierrc.js ├── .github └── workflows │ └── node.js.yml ├── .eslintrc.js ├── package.json ├── src ├── utils.ts ├── main.ts └── options.ts ├── CHANGES.md ├── README.md ├── examples ├── typescript.ts └── javascript.js └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules 3 | lib 4 | docs 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | docs 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode/ 3 | 4 | # JavaScript and TypeScript 5 | lib/ 6 | test/lib/ 7 | 8 | # Documentation 9 | docs/ 10 | 11 | # Node.js 12 | node_modules/ 13 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | js2xmlparser 2 | Copyright (C) 2016-2021 Michael Kourlas 3 | 4 | The following components are provided under the Apache License, version 2.0 5 | (https://www.apache.org/licenses/LICENSE-2.0): 6 | 7 | xmlcreate 8 | Copyright (C) 2016-2021 Michael Kourlas 9 | 10 | The text of the Apache License 2.0 can be found in the LICENSE file. 11 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["es6"], 6 | "module": "commonjs", 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitReturns": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "outDir": "lib", 12 | "strict": true, 13 | "target": "es5", 14 | "types": ["chai", "mocha"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "lib": ["dom", "es5", "es2015.collection"], 7 | "module": "commonjs", 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitReturns": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "outDir": "lib", 13 | "strict": true, 14 | "target": "es5", 15 | "types": [] 16 | }, 17 | "exclude": ["docs", "examples", "lib", "node_modules", "test"] 18 | } 19 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2021 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | module.exports = { 20 | tabWidth: 4, 21 | bracketSpacing: false, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x, 18.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | - run: npm ci 29 | - run: npm run build 30 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2020-2021 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | module.exports = { 20 | env: { 21 | node: true, 22 | }, 23 | root: true, 24 | parser: "@typescript-eslint/parser", 25 | plugins: ["@typescript-eslint"], 26 | extends: [ 27 | "eslint:recommended", 28 | "plugin:@typescript-eslint/eslint-recommended", 29 | "plugin:@typescript-eslint/recommended", 30 | ], 31 | rules: { 32 | // Maximum line length of 80 33 | "max-len": ["error", {code: 80}], 34 | 35 | // Too late to change this, since interfaces are part of the public API 36 | "@typescript-eslint/interface-name-prefix": 0, 37 | 38 | // Too much noise 39 | "@typescript-eslint/explicit-function-return-type": 0, 40 | 41 | // Allow private functions at bottom of file 42 | "@typescript-eslint/no-use-before-define": 0, 43 | 44 | // Allow private constructors 45 | "@typescript-eslint/no-empty-function": [ 46 | "error", 47 | {allow: ["private-constructors"]}, 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js2xmlparser", 3 | "version": "5.0.0", 4 | "description": "Parses JavaScript objects into XML", 5 | "keywords": [ 6 | "convert", 7 | "converter", 8 | "javascript", 9 | "js", 10 | "json", 11 | "object", 12 | "objects", 13 | "parse", 14 | "parser", 15 | "xml" 16 | ], 17 | "license": "Apache-2.0", 18 | "author": { 19 | "name": "Michael Kourlas", 20 | "email": "michael@kourlas.com" 21 | }, 22 | "files": [ 23 | "lib", 24 | "CHANGES.md", 25 | "LICENSE", 26 | "NOTICE", 27 | "package.json", 28 | "README.md" 29 | ], 30 | "main": "./lib/main.js", 31 | "typings": "./lib/main", 32 | "repository": { 33 | "type": "git", 34 | "url": "git://github.com/michaelkourlas/node-js2xmlparser.git" 35 | }, 36 | "scripts": { 37 | "build": "npm run-script prod && npm run-script test-prod && npm run-script docs", 38 | "clean": "rimraf lib", 39 | "clean-docs": "rimraf docs", 40 | "clean-test": "rimraf test/lib", 41 | "dev": "npm run-script clean && npm run-script format && npm run-script lint && tsc -p tsconfig.json --sourceMap", 42 | "docs": "npm run-script clean-docs && typedoc --out docs --excludePrivate src/main.ts", 43 | "format": "prettier --write .", 44 | "lint": "eslint . --ext .ts", 45 | "prod": "npm run-script clean && npm run-script format && npm run-script lint && tsc -p tsconfig.json", 46 | "test-dev": "npm run-script clean-test && tsc -p test/tsconfig.json --sourceMap && mocha --recursive test/lib", 47 | "test-prod": "npm run-script clean-test && tsc -p test/tsconfig.json && mocha --recursive test/lib" 48 | }, 49 | "dependencies": { 50 | "xmlcreate": "^2.0.4" 51 | }, 52 | "devDependencies": { 53 | "@types/chai": "^4.3.3", 54 | "@types/mocha": "^9.1.1", 55 | "@typescript-eslint/eslint-plugin": "^5.38.0", 56 | "@typescript-eslint/parser": "^5.38.0", 57 | "chai": "^4.3.6", 58 | "eslint": "^8.23.1", 59 | "mocha": "^10.0.0", 60 | "prettier": "^2.7.1", 61 | "rimraf": "^3.0.2", 62 | "typedoc": "^0.23.15", 63 | "typescript": "^4.8.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2020 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export function isUndefined(val: unknown): val is undefined { 18 | return Object.prototype.toString.call(val) === "[object Undefined]"; 19 | } 20 | 21 | export function isNull(val: unknown): val is null { 22 | return Object.prototype.toString.call(val) === "[object Null]"; 23 | } 24 | 25 | export function isObject(val: unknown): val is Record { 26 | return Object.prototype.toString.call(val) === "[object Object]"; 27 | } 28 | 29 | export function isArray(val: unknown): val is unknown[] { 30 | return Object.prototype.toString.call(val) === "[object Array]"; 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/ban-types 34 | export function isFunction(val: unknown): val is Function { 35 | return Object.prototype.toString.call(val) === "[object Function]"; 36 | } 37 | 38 | export function isSet(val: unknown): val is Set { 39 | return Object.prototype.toString.call(val) === "[object Set]"; 40 | } 41 | 42 | export function isMap(val: unknown): val is Map { 43 | return Object.prototype.toString.call(val) === "[object Map]"; 44 | } 45 | 46 | /** 47 | * Returns a string representation of the specified value, as given by the 48 | * value's toString() method (if it has one) or the global String() function 49 | * (if it does not). 50 | * 51 | * @param value The value to convert to a string. 52 | * 53 | * @returns A string representation of the specified value. 54 | */ 55 | // eslint-disable-next-line max-len 56 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types 57 | export function stringify(value: any): string { 58 | if (!isUndefined(value) && !isNull(value)) { 59 | if (isFunction(value?.toString)) { 60 | value = value.toString(); 61 | } 62 | } 63 | return String(value); 64 | } 65 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 5.0.0 2 | 3 | - Allow typeHandlers to be used with bare text and attribute values 4 | - Clarify that typeHandlers cannot be used with aliases in documentation 5 | 6 | ## 4.0.2 7 | 8 | - Update dependencies 9 | - Export options interfaces in main module 10 | - Update example to include root attribute 11 | 12 | ## 4.0.1 13 | 14 | - Update dependencies 15 | - Use ESLint instead of TSLint 16 | - Use npm instead of gulp 17 | 18 | ## 4.0.0 19 | 20 | - Do not indent multi-line strings 21 | - Use self-closing tags, unless otherwise specified 22 | - Add option to automatically replace invalid characters with U+FFFD 23 | - Add option to suppress certain values from output 24 | - Add support for adding to existing xmlcreate object 25 | - Remove certain unnecessary validation rules 26 | - Bug fixes 27 | - Correct errors in documentation 28 | 29 | ## 3.0.0 30 | 31 | - Bug fixes 32 | - Add null and undefined in type declarations 33 | - Remove explicit engines requirement 34 | 35 | ## 2.0.2 36 | 37 | - Bug fixes 38 | 39 | ## 2.0.1 40 | 41 | - Remove unnecessary development dependencies from npm shrinkwrap 42 | 43 | ## 2.0.0 44 | 45 | - Re-write in TypeScript 46 | - Re-write to use xmlcreate (greatly simplifies module source) 47 | - Added support for the ECMAScript 2015 Map and Set objects 48 | - New method of calling module: 49 | 50 | ```javascript 51 | var js2xmlparser = require("js2xmlparser"); 52 | 53 | var root = "root"; 54 | var data = {hello: "world"}; 55 | var options = {}; 56 | 57 | // old method (no longer works): 58 | // js2xmlparser(root, data, options); 59 | 60 | // new method: 61 | js2xmlparser.parse(root, data, options); 62 | ``` 63 | 64 | - New options and changes to functionality of some existing options: 65 | - `declaration` contains additional options 66 | - `attributeString` has additional functionality 67 | - `valueString` has additional functionality 68 | - The functionality provided by `prettyPrinting` is now provided by the new 69 | `format` option, which contains additional options 70 | - `arrayMap` is now `wrapHandlers` to reflect the fact that wrapping is 71 | provided for both arrays and ES2015 sets 72 | - `convertMap` is now `typeHandlers` to match the name change to `arrayMap` 73 | - The functionality provided by `useCDATA` is now provided by the new 74 | `cdataInvalidChars` and `cdataKeys` options, which also provide additional 75 | functionality 76 | - Added support for document type definitions using the `dtd` option 77 | 78 | ## 1.0.0 79 | 80 | - First stable release 81 | - Add arrayMap feature 82 | - Switch to semantic versioning 83 | - Switch to Apache 2.0 license 84 | 85 | ## 0.1.9 86 | 87 | - Fix error in example.js 88 | 89 | ## 0.1.8 90 | 91 | - Reconcile readme and tests with examples 92 | 93 | ## 0.1.7 94 | 95 | - Added .gitattributes to .gitignore file 96 | - Minor tweaks to examples 97 | 98 | ## 0.1.6 99 | 100 | - Addition of alias string option 101 | - Minor changes to examples 102 | - Minor fixes to tests 103 | 104 | ## 0.1.5 105 | 106 | - Bug fixes 107 | - Minor changes to examples 108 | 109 | ## 0.1.4 110 | 111 | - Removed callFunctions option (functionality already provided by convertMap option) 112 | - Removed wrapArray option (functionality already provided by existing array functionality) 113 | - Escape numbers when at tbe beginning of an element name 114 | - Edits to documentation 115 | - Added tests 116 | - Added copyright headers to individual JS files 117 | 118 | ## 0.1.3 119 | 120 | - Fixed crash when undefined objects are converted to strings 121 | - Added callFunctions option 122 | - Added wrapArray option 123 | - Added useCDATA option 124 | - Added convertMap option 125 | - Added copyright year and "and other contributors" to license 126 | 127 | ## 0.1.2 128 | 129 | - Fixed crash when null objects are converted to strings 130 | 131 | ## 0.1.1 132 | 133 | - Fixed accidental truncation of XML when pretty-printing is disabled 134 | - Removed copyright year from license 135 | 136 | ## 0.1.0 137 | 138 | - Initial release 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js2xmlparser 2 | 3 | [![Node.js CI](https://github.com/michaelkourlas/node-js2xmlparser/actions/workflows/node.js.yml/badge.svg)](https://github.com/michaelkourlas/node-js2xmlparser/actions/workflows/node.js.yml) 4 | [![npm version](https://badge.fury.io/js/js2xmlparser.svg)](https://badge.fury.io/js/js2xmlparser) 5 | 6 | ## Overview 7 | 8 | js2xmlparser is a Node.js module that parses JavaScript objects into XML. 9 | 10 | ## Features 11 | 12 | Since XML is a data-interchange format, js2xmlparser is designed primarily for 13 | JSON-type objects, arrays and primitive data types, like many of the other 14 | JavaScript to XML parsers currently available for Node.js. 15 | 16 | However, js2xmlparser is capable of parsing any object, including native 17 | JavaScript objects such as `Date` and `RegExp`, by taking advantage of each 18 | object's `toString` function or, if this function does not exist, the `String` 19 | constructor. 20 | 21 | js2xmlparser also has support for the `Map` and `Set` objects introduced in 22 | ECMAScript 2015, treating them as JSON-type objects and arrays respectively. 23 | Support for `Map`s is necessary to generate XML with elements in a specific 24 | order, since JSON-type objects do not guarantee insertion order. `Map` keys are 25 | always converted to strings using the method described above. 26 | 27 | js2xmlparser also supports a number of constructs unique to XML: 28 | 29 | - attributes (through an attribute property in objects) 30 | - mixed content (through value properties in objects) 31 | - multiple elements with the same name (through arrays) 32 | 33 | js2xmlparser can also pretty-print the XML it outputs. 34 | 35 | ## Installation 36 | 37 | The easiest way to install js2xmlparser is using npm: 38 | 39 | ``` 40 | npm install js2xmlparser 41 | ``` 42 | 43 | You can also build js2xmlparser from source using npm: 44 | 45 | ``` 46 | git clone https://github.com/michaelkourlas/node-js2xmlparser.git 47 | npm install 48 | npm run-script build 49 | ``` 50 | 51 | The `build` script will build the production variant of js2xmlparser, run all 52 | tests, and build the documentation. 53 | 54 | You can build the production variant without running tests using the script 55 | `prod`. You can also build the development version using the script `dev`. 56 | The only difference between the two is that the development version includes 57 | source maps. 58 | 59 | ## Usage 60 | 61 | The documentation for the current version is available [here](http://www.kourlas.com/node-js2xmlparser/docs/5.0.0/). 62 | 63 | You can also build the documentation using npm: 64 | 65 | ``` 66 | npm run-script docs 67 | ``` 68 | 69 | ## Examples 70 | 71 | The following example illustrates the basic usage of js2xmlparser: 72 | 73 | ```javascript 74 | var js2xmlparser = require("js2xmlparser"); 75 | 76 | var obj = { 77 | "@": { 78 | type: "natural", 79 | }, 80 | firstName: "John", 81 | lastName: "Smith", 82 | dateOfBirth: new Date(1964, 7, 26), 83 | address: { 84 | "@": { 85 | type: "home", 86 | }, 87 | streetAddress: "3212 22nd St", 88 | city: "Chicago", 89 | state: "Illinois", 90 | zip: 10000, 91 | }, 92 | phone: [ 93 | { 94 | "@": { 95 | type: "home", 96 | }, 97 | "#": "123-555-4567", 98 | }, 99 | { 100 | "@": { 101 | type: "cell", 102 | }, 103 | "#": "890-555-1234", 104 | }, 105 | { 106 | "@": { 107 | type: "work", 108 | }, 109 | "#": "567-555-8901", 110 | }, 111 | ], 112 | email: "john@smith.com", 113 | }; 114 | 115 | console.log(js2xmlparser.parse("person", obj)); 116 | ``` 117 | 118 | This example produces the following XML: 119 | 120 | ```xml 121 | 122 | 123 | John 124 | Smith 125 | Wed Aug 26 1964 00:00:00 GMT-0400 (Eastern Summer Time) 126 |
127 | 3212 22nd St 128 | Chicago 129 | Illinois 130 | 10000 131 |
132 | 123-555-4567 133 | 890-555-1234 134 | 567-555-8901 135 | john@smith.com 136 |
137 | ``` 138 | 139 | Additional examples can be found in the examples directory. 140 | 141 | ## Tests 142 | 143 | js2xmlparser includes a set of tests to verify core functionality. You can run 144 | the tests using npm: 145 | 146 | ``` 147 | npm run-script test-prod 148 | ``` 149 | 150 | The only difference between the `test-prod` and `test-dev` scripts is that the 151 | development version includes source maps. 152 | 153 | ## License 154 | 155 | js2xmlparser is licensed under the [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0). 156 | -------------------------------------------------------------------------------- /examples/typescript.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2019 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {parse} from "../lib/main"; 18 | 19 | /** 20 | * This example demonstrates a very simple usage of js2xmlparser. 21 | */ 22 | const example1 = () => { 23 | const obj = { 24 | firstName: "John", 25 | lastName: "Smith", 26 | }; 27 | console.log(parse("person", obj)); 28 | console.log(); 29 | }; 30 | example1(); 31 | 32 | /** 33 | * This example demonstrates a more complex usage of j2xmlparser. 34 | */ 35 | const example2 = () => { 36 | const obj = { 37 | "@": { 38 | type: "natural", 39 | }, 40 | firstName: "John", 41 | lastName: "Smith", 42 | dateOfBirth: new Date(1964, 7, 26), 43 | address: { 44 | "@": { 45 | type: "home", 46 | }, 47 | streetAddress: "3212 22nd St", 48 | city: "Chicago", 49 | state: "Illinois", 50 | zip: 10000, 51 | }, 52 | phone: [ 53 | { 54 | "@": { 55 | type: "home", 56 | }, 57 | "#": "123-555-4567", 58 | }, 59 | { 60 | "@": { 61 | type: "cell", 62 | }, 63 | "#": "890-555-1234", 64 | }, 65 | { 66 | "@": { 67 | type: "work", 68 | }, 69 | "#": "567-555-8901", 70 | }, 71 | ], 72 | email: "john@smith.com", 73 | }; 74 | console.log(parse("person", obj)); 75 | console.log(); 76 | }; 77 | example2(); 78 | 79 | /** 80 | * This example demonstrates some of js2xmlparser's options. 81 | */ 82 | const example3 = () => { 83 | const options = { 84 | aliasString: "exAlias", 85 | attributeString: "exAttr", 86 | cdataKeys: ["exCdata", "exCdata2"], 87 | declaration: { 88 | include: true, 89 | encoding: "UTF-16", 90 | standalone: "yes", 91 | version: "1.1", 92 | }, 93 | dtd: { 94 | include: true, 95 | name: "exName", 96 | sysId: "exSysId", 97 | pubId: "exPubId", 98 | }, 99 | format: { 100 | doubleQuotes: true, 101 | indent: "\t", 102 | newline: "\r\n", 103 | pretty: true, 104 | }, 105 | typeHandlers: { 106 | "[object Number]": (value: any) => { 107 | return value + 17; 108 | }, 109 | }, 110 | valueString: "exVal", 111 | wrapHandlers: { 112 | exArr: () => { 113 | return "exArrInner"; 114 | }, 115 | }, 116 | }; 117 | 118 | const obj = { 119 | ex1: "ex2", 120 | exVal_1: 123, 121 | ex3: ["ex4", "ex5"], 122 | ex6: { 123 | exAttr_1: { 124 | ex7: "ex8", 125 | ex9: "ex10", 126 | }, 127 | ex11: "ex12", 128 | ex13: { 129 | ex14: "ex15", 130 | }, 131 | ex16: [ 132 | "ex17", 133 | { 134 | ex18: "ex19", 135 | }, 136 | ], 137 | exArr: [ 138 | "ex20", 139 | { 140 | ex21: "ex22", 141 | }, 142 | ], 143 | exAttr_2: { 144 | ex23: "ex24", 145 | ex25: "ex26", 146 | }, 147 | }, 148 | exVal_2: "ex27", 149 | ex28: ["ex29", "ex30"], 150 | ex31: true, 151 | ex32: undefined, 152 | ex33: null, 153 | ex34: 3.4, 154 | ex35: () => { 155 | return "ex36"; 156 | }, 157 | ex37: "i { 30 | describe("#isUndefined", () => { 31 | it("should return true for undefined", () => { 32 | assert.isTrue(isUndefined(undefined)); 33 | }); 34 | 35 | it("should return false for values that are not undefined", () => { 36 | assert.isFalse(isUndefined("test")); 37 | assert.isFalse(isUndefined(3)); 38 | assert.isFalse(isUndefined(null)); 39 | assert.isFalse(isUndefined(true)); 40 | }); 41 | }); 42 | 43 | describe("#isNull", () => { 44 | it("should return true for null", () => { 45 | assert.isTrue(isNull(null)); 46 | }); 47 | 48 | it("should return false for values that are not null", () => { 49 | assert.isFalse(isNull("test")); 50 | assert.isFalse(isNull(3)); 51 | assert.isFalse(isNull(undefined)); 52 | assert.isFalse(isNull(true)); 53 | }); 54 | }); 55 | 56 | describe("#isObject", () => { 57 | it("should return true for objects", () => { 58 | assert.isTrue(isObject({a: "b"})); 59 | assert.isTrue(isObject({})); 60 | }); 61 | 62 | it("should return false for values that are not objects", () => { 63 | assert.isFalse(isObject("test")); 64 | assert.isFalse(isObject(3)); 65 | assert.isFalse(isObject(undefined)); 66 | assert.isFalse(isObject(true)); 67 | assert.isFalse(isObject(null)); 68 | }); 69 | }); 70 | 71 | describe("#isArray", () => { 72 | it("should return true for arrays", () => { 73 | assert.isTrue(isArray(["a", "b"])); 74 | assert.isTrue(isArray(["a", 3])); 75 | assert.isTrue(isArray([])); 76 | }); 77 | 78 | it("should return false for values that are not arrays", () => { 79 | assert.isFalse(isArray("test")); 80 | assert.isFalse(isArray(3)); 81 | assert.isFalse(isArray(undefined)); 82 | assert.isFalse(isArray(true)); 83 | assert.isFalse(isArray(null)); 84 | }); 85 | }); 86 | 87 | describe("#isFunction", () => { 88 | it("should return true for functions", () => { 89 | assert.isTrue(isFunction(() => 0)); 90 | assert.isTrue(isFunction(() => "test")); 91 | }); 92 | 93 | it("should return false for values that are not functions", () => { 94 | assert.isFalse(isFunction("test")); 95 | assert.isFalse(isFunction(3)); 96 | assert.isFalse(isFunction(undefined)); 97 | assert.isFalse(isFunction(true)); 98 | assert.isFalse(isFunction(null)); 99 | }); 100 | }); 101 | 102 | describe("#isSet", () => { 103 | it("should return true for sets", () => { 104 | assert.isTrue(isSet(new Set())); 105 | assert.isTrue(isSet(new Set(["a", "b"]))); 106 | }); 107 | 108 | it("should return false for values that are not sets", () => { 109 | assert.isFalse(isSet("test")); 110 | assert.isFalse(isSet(3)); 111 | assert.isFalse(isSet(undefined)); 112 | assert.isFalse(isSet(true)); 113 | assert.isFalse(isSet(null)); 114 | }); 115 | }); 116 | 117 | describe("#isMap", () => { 118 | it("should return true for sets", () => { 119 | assert.isTrue(isMap(new Map())); 120 | assert.isTrue( 121 | isMap( 122 | new Map([ 123 | ["a", "b"], 124 | ["c", "d"], 125 | ]) 126 | ) 127 | ); 128 | }); 129 | 130 | it("should return false for values that are not sets", () => { 131 | assert.isFalse(isMap("test")); 132 | assert.isFalse(isMap(3)); 133 | assert.isFalse(isMap(undefined)); 134 | assert.isFalse(isMap(true)); 135 | assert.isFalse(isMap(null)); 136 | }); 137 | }); 138 | 139 | describe("#stringify", () => { 140 | it( 141 | "should return a valid string representation for all primitive" + 142 | " types", 143 | () => { 144 | assert.strictEqual(stringify(null), "null"); 145 | assert.strictEqual(stringify(undefined), "undefined"); 146 | assert.strictEqual(stringify(3), "3"); 147 | assert.strictEqual(stringify("test"), "test"); 148 | assert.strictEqual(stringify(true), "true"); 149 | } 150 | ); 151 | 152 | it( 153 | "should return a valid string representation for all object" + 154 | " versions of primitive types", 155 | () => { 156 | assert.strictEqual(stringify(new Number(3)), "3"); 157 | assert.strictEqual(stringify(new String("test")), "test"); 158 | assert.strictEqual(stringify(new Boolean(true)), "true"); 159 | } 160 | ); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2020 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import {XmlAttribute, XmlDocument, XmlElement} from "xmlcreate"; 17 | import {IOptions, Options} from "./options"; 18 | import { 19 | isArray, 20 | isMap, 21 | isNull, 22 | isObject, 23 | isSet, 24 | isUndefined, 25 | stringify, 26 | } from "./utils"; 27 | 28 | export { 29 | IOptions, 30 | IDeclarationOptions, 31 | IDtdOptions, 32 | IFormatOptions, 33 | ITypeHandlers, 34 | IWrapHandlers, 35 | } from "./options"; 36 | 37 | /** 38 | * Indicates that an object of a particular type should be suppressed from the 39 | * XML output. 40 | * 41 | * See the `typeHandlers` property in {@link IOptions} for more details. 42 | */ 43 | export class Absent { 44 | private static _instance = new Absent(); 45 | 46 | private constructor() {} 47 | 48 | /** 49 | * Returns the sole instance of Absent. 50 | */ 51 | public static get instance(): Absent { 52 | return Absent._instance; 53 | } 54 | } 55 | 56 | /** 57 | * Gets the type handler associated with a value. 58 | */ 59 | function getHandler( 60 | value: unknown, 61 | options: Options 62 | ): ((value: unknown) => unknown) | undefined { 63 | const type = Object.prototype.toString.call(value); 64 | let handler: ((value: unknown) => unknown) | undefined; 65 | if (Object.prototype.hasOwnProperty.call(options.typeHandlers, "*")) { 66 | handler = options.typeHandlers["*"]; 67 | } 68 | if (Object.prototype.hasOwnProperty.call(options.typeHandlers, type)) { 69 | handler = options.typeHandlers[type]; 70 | } 71 | return handler; 72 | } 73 | 74 | /** 75 | * Parses a string into XML and adds it to the parent element or attribute. 76 | */ 77 | function parseString( 78 | str: string, 79 | parentElement: XmlAttribute | XmlElement, 80 | options: Options 81 | ): void { 82 | const requiresCdata = (s: string) => { 83 | return ( 84 | (options.cdataInvalidChars && 85 | (s.indexOf("<") !== -1 || s.indexOf("&") !== -1)) || 86 | options.cdataKeys.indexOf(parentElement.name) !== -1 || 87 | options.cdataKeys.indexOf("*") !== -1 88 | ); 89 | }; 90 | 91 | if (parentElement instanceof XmlElement) { 92 | if (requiresCdata(str)) { 93 | const cdataStrs = str.split("]]>"); 94 | for (let i = 0; i < cdataStrs.length; i++) { 95 | if (requiresCdata(cdataStrs[i])) { 96 | parentElement.cdata({ 97 | charData: cdataStrs[i], 98 | replaceInvalidCharsInCharData: 99 | options.replaceInvalidChars, 100 | }); 101 | } else { 102 | parentElement.charData({ 103 | charData: cdataStrs[i], 104 | replaceInvalidCharsInCharData: 105 | options.replaceInvalidChars, 106 | }); 107 | } 108 | if (i < cdataStrs.length - 1) { 109 | parentElement.charData({ 110 | charData: "]]>", 111 | replaceInvalidCharsInCharData: 112 | options.replaceInvalidChars, 113 | }); 114 | } 115 | } 116 | } else { 117 | parentElement.charData({ 118 | charData: str, 119 | replaceInvalidCharsInCharData: options.replaceInvalidChars, 120 | }); 121 | } 122 | } else { 123 | parentElement.text({ 124 | charData: str, 125 | replaceInvalidCharsInCharData: options.replaceInvalidChars, 126 | }); 127 | } 128 | } 129 | 130 | /** 131 | * Parses an attribute into XML and adds it to the parent element. 132 | */ 133 | function parseAttribute( 134 | name: string, 135 | value: unknown, 136 | parentElement: XmlElement, 137 | options: Options 138 | ): void { 139 | const attribute = parentElement.attribute({ 140 | name, 141 | replaceInvalidCharsInName: options.replaceInvalidChars, 142 | }); 143 | parseString(stringify(value), attribute, options); 144 | } 145 | 146 | /** 147 | * Parses an object or Map entry into XML and adds it to the parent element. 148 | */ 149 | function parseObjectOrMapEntry( 150 | key: string, 151 | value: unknown, 152 | parentElement: XmlElement, 153 | options: Options 154 | ): void { 155 | // Alias key 156 | if (key === options.aliasString) { 157 | parentElement.name = stringify(value); 158 | return; 159 | } 160 | 161 | // Attributes key 162 | if (key.indexOf(options.attributeString) === 0 && isObject(value)) { 163 | for (const subkey of Object.keys(value)) { 164 | let subvalue = value[subkey]; 165 | 166 | const handler = getHandler(subvalue, options); 167 | if (!isUndefined(handler)) { 168 | subvalue = handler(subvalue); 169 | if (handler(value) === Absent.instance) { 170 | continue; 171 | } 172 | } 173 | 174 | parseAttribute(subkey, subvalue, parentElement, options); 175 | } 176 | return; 177 | } 178 | 179 | // Value key 180 | if (key.indexOf(options.valueString) === 0) { 181 | parseValue(key, value, parentElement, options); 182 | return; 183 | } 184 | 185 | // Standard handling (create new element for entry) 186 | let element = parentElement; 187 | if (!isArray(value) && !isSet(value)) { 188 | // If handler for value returns absent, then do not add element 189 | const handler = getHandler(value, options); 190 | if (!isUndefined(handler)) { 191 | if (handler(value) === Absent.instance) { 192 | return; 193 | } 194 | } 195 | 196 | element = parentElement.element({ 197 | name: key, 198 | replaceInvalidCharsInName: options.replaceInvalidChars, 199 | useSelfClosingTagIfEmpty: options.useSelfClosingTagIfEmpty, 200 | }); 201 | } 202 | parseValue(key, value, element, options); 203 | } 204 | 205 | /** 206 | * Parses an Object or Map into XML and adds it to the parent element. 207 | */ 208 | function parseObjectOrMap( 209 | objectOrMap: Record | Map, 210 | parentElement: XmlElement, 211 | options: Options 212 | ): void { 213 | if (isMap(objectOrMap)) { 214 | objectOrMap.forEach((value: unknown, key: unknown) => { 215 | parseObjectOrMapEntry( 216 | stringify(key), 217 | value, 218 | parentElement, 219 | options 220 | ); 221 | }); 222 | } else { 223 | for (const key of Object.keys(objectOrMap)) { 224 | parseObjectOrMapEntry( 225 | key, 226 | objectOrMap[key], 227 | parentElement, 228 | options 229 | ); 230 | } 231 | } 232 | } 233 | 234 | /** 235 | * Parses an array or Set into XML and adds it to the parent element. 236 | */ 237 | function parseArrayOrSet( 238 | key: string, 239 | arrayOrSet: unknown[] | Set, 240 | parentElement: XmlElement, 241 | options: Options 242 | ): void { 243 | let arrayNameFunc: 244 | | ((key: string, value: unknown) => string | null) 245 | | undefined; 246 | if (Object.prototype.hasOwnProperty.call(options.wrapHandlers, "*")) { 247 | arrayNameFunc = options.wrapHandlers["*"]; 248 | } 249 | if (Object.prototype.hasOwnProperty.call(options.wrapHandlers, key)) { 250 | arrayNameFunc = options.wrapHandlers[key]; 251 | } 252 | 253 | let arrayKey = key; 254 | let arrayElement = parentElement; 255 | if (!isUndefined(arrayNameFunc)) { 256 | const arrayNameFuncKey = arrayNameFunc(arrayKey, arrayOrSet); 257 | if (!isNull(arrayNameFuncKey)) { 258 | arrayKey = arrayNameFuncKey; 259 | arrayElement = parentElement.element({ 260 | name: key, 261 | replaceInvalidCharsInName: options.replaceInvalidChars, 262 | useSelfClosingTagIfEmpty: options.useSelfClosingTagIfEmpty, 263 | }); 264 | } 265 | } 266 | 267 | arrayOrSet.forEach((item: unknown) => { 268 | let element = arrayElement; 269 | if (!isArray(item) && !isSet(item)) { 270 | // If handler for value returns absent, then do not add element 271 | const handler = getHandler(item, options); 272 | if (!isUndefined(handler)) { 273 | if (handler(item) === Absent.instance) { 274 | return; 275 | } 276 | } 277 | 278 | element = arrayElement.element({ 279 | name: arrayKey, 280 | replaceInvalidCharsInName: options.replaceInvalidChars, 281 | useSelfClosingTagIfEmpty: options.useSelfClosingTagIfEmpty, 282 | }); 283 | } 284 | parseValue(arrayKey, item, element, options); 285 | }); 286 | } 287 | 288 | /** 289 | * Parses an arbitrary JavaScript value into XML and adds it to the parent 290 | * element. 291 | */ 292 | function parseValue( 293 | key: string, 294 | value: unknown, 295 | parentElement: XmlElement, 296 | options: Options 297 | ): void { 298 | // If a handler for a particular type is user-defined, use that handler 299 | // instead of the defaults 300 | const handler = getHandler(value, options); 301 | if (!isUndefined(handler)) { 302 | value = handler(value); 303 | } 304 | 305 | if (isObject(value) || isMap(value)) { 306 | parseObjectOrMap(value, parentElement, options); 307 | return; 308 | } 309 | if (isArray(value) || isSet(value)) { 310 | parseArrayOrSet(key, value, parentElement, options); 311 | return; 312 | } 313 | 314 | parseString(stringify(value), parentElement, options); 315 | } 316 | 317 | /** 318 | * Converts the specified object to XML and adds the XML representation to the 319 | * specified XmlElement object using the specified options. 320 | * 321 | * This function does not add a root element. In addition, it does not add an 322 | * XML declaration or DTD, and the associated options in {@link IOptions} are 323 | * ignored. If desired, these must be added manually. 324 | */ 325 | export function parseToExistingElement( 326 | element: XmlElement, 327 | object: unknown, 328 | options?: IOptions 329 | ): void { 330 | const opts: Options = new Options(options); 331 | parseValue(element.name, object, element, opts); 332 | } 333 | 334 | /** 335 | * Returns a XML string representation of the specified object using the 336 | * specified options. 337 | * 338 | * `root` is the name of the root XML element. When the object is converted 339 | * to XML, it will be a child of this root element. 340 | */ 341 | export function parse( 342 | root: string, 343 | object: unknown, 344 | options?: IOptions 345 | ): string { 346 | const opts = new Options(options); 347 | const document = new XmlDocument({ 348 | validation: opts.validation, 349 | }); 350 | if (opts.declaration.include) { 351 | document.decl(opts.declaration); 352 | } 353 | if (opts.dtd.include) { 354 | document.dtd({ 355 | // Validated in options.ts 356 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 357 | name: opts.dtd.name!, 358 | pubId: opts.dtd.pubId, 359 | sysId: opts.dtd.sysId, 360 | }); 361 | } 362 | const rootElement = document.element({ 363 | name: root, 364 | replaceInvalidCharsInName: opts.replaceInvalidChars, 365 | useSelfClosingTagIfEmpty: opts.useSelfClosingTagIfEmpty, 366 | }); 367 | parseToExistingElement(rootElement, object, options); 368 | return document.toString(opts.format); 369 | } 370 | -------------------------------------------------------------------------------- /test/src/options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2020 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {assert} from "chai"; 18 | import {ITypeHandlers, IWrapHandlers, Options} from "../../lib/options"; 19 | 20 | describe("options", () => { 21 | describe("#Options", () => { 22 | describe("aliasString", () => { 23 | it("should leave the specified property unchanged if valid", () => { 24 | const options = { 25 | aliasString: "=", 26 | }; 27 | assert.strictEqual( 28 | new Options(options).aliasString, 29 | options.aliasString 30 | ); 31 | 32 | options.aliasString = "test"; 33 | assert.strictEqual( 34 | new Options(options).aliasString, 35 | options.aliasString 36 | ); 37 | }); 38 | 39 | it( 40 | "should return a validated version of the specified property" + 41 | " if undefined", 42 | () => { 43 | const options = {}; 44 | assert.strictEqual(new Options(options).aliasString, "="); 45 | } 46 | ); 47 | }); 48 | 49 | describe("attributeString", () => { 50 | it("should leave the specified property unchanged if valid", () => { 51 | const options = { 52 | attributeString: "@", 53 | }; 54 | assert.strictEqual( 55 | new Options(options).attributeString, 56 | options.attributeString 57 | ); 58 | 59 | options.attributeString = "test"; 60 | assert.strictEqual( 61 | new Options(options).attributeString, 62 | options.attributeString 63 | ); 64 | }); 65 | 66 | it( 67 | "should return a validated version of the specified property" + 68 | " if undefined", 69 | () => { 70 | const options = {}; 71 | assert.strictEqual( 72 | new Options(options).attributeString, 73 | "@" 74 | ); 75 | } 76 | ); 77 | }); 78 | 79 | describe("cdataInvalidChars", () => { 80 | it("should leave the specified property unchanged if valid", () => { 81 | const options = { 82 | cdataInvalidChars: false, 83 | }; 84 | assert.strictEqual( 85 | new Options(options).cdataInvalidChars, 86 | options.cdataInvalidChars 87 | ); 88 | 89 | options.cdataInvalidChars = true; 90 | assert.strictEqual( 91 | new Options(options).cdataInvalidChars, 92 | options.cdataInvalidChars 93 | ); 94 | }); 95 | 96 | it( 97 | "should return a validated version of the specified property" + 98 | " if undefined", 99 | () => { 100 | const options = {}; 101 | assert.strictEqual( 102 | new Options(options).cdataInvalidChars, 103 | false 104 | ); 105 | } 106 | ); 107 | }); 108 | 109 | describe("cdataKeys", () => { 110 | it("should leave the specified property unchanged if valid", () => { 111 | const options = { 112 | cdataKeys: ["test", "test2"], 113 | }; 114 | assert.deepEqual( 115 | new Options(options).cdataKeys, 116 | options.cdataKeys 117 | ); 118 | 119 | options.cdataKeys = []; 120 | assert.deepEqual( 121 | new Options(options).cdataKeys, 122 | options.cdataKeys 123 | ); 124 | }); 125 | 126 | it( 127 | "should return a validated version of the specified property" + 128 | " if undefined", 129 | () => { 130 | const options = {}; 131 | assert.deepEqual(new Options(options).cdataKeys, []); 132 | } 133 | ); 134 | }); 135 | 136 | describe("declaration", () => { 137 | it("should leave the specified property unchanged if valid", () => { 138 | const options = { 139 | declaration: { 140 | encoding: undefined, 141 | include: true, 142 | standalone: undefined, 143 | version: undefined, 144 | }, 145 | }; 146 | assert.deepEqual( 147 | new Options(options).declaration, 148 | options.declaration 149 | ); 150 | 151 | options.declaration = { 152 | encoding: undefined, 153 | include: false, 154 | standalone: undefined, 155 | version: undefined, 156 | }; 157 | assert.deepEqual( 158 | new Options(options).declaration, 159 | options.declaration 160 | ); 161 | }); 162 | 163 | it( 164 | "should return a validated version of the specified property" + 165 | " if undefined", 166 | () => { 167 | const options = {}; 168 | assert.deepEqual(new Options(options).declaration, { 169 | encoding: undefined, 170 | include: true, 171 | standalone: undefined, 172 | version: undefined, 173 | }); 174 | } 175 | ); 176 | }); 177 | 178 | describe("dtd", () => { 179 | it("should leave the specified property unchanged if valid", () => { 180 | { 181 | const options = { 182 | dtd: { 183 | include: false, 184 | name: undefined, 185 | pubId: undefined, 186 | sysId: undefined, 187 | }, 188 | }; 189 | assert.deepEqual(new Options(options).dtd, options.dtd); 190 | } 191 | 192 | { 193 | const options = { 194 | dtd: { 195 | include: true, 196 | name: "abc", 197 | pubId: undefined, 198 | sysId: undefined, 199 | }, 200 | }; 201 | assert.deepEqual(new Options(options).dtd, options.dtd); 202 | } 203 | }); 204 | 205 | it( 206 | "should throw an error if the specified options object" + 207 | " contains invalid options", 208 | () => { 209 | // eslint-disable-next-line max-len 210 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 211 | const options: any = { 212 | dtd: { 213 | include: true, 214 | }, 215 | }; 216 | assert.throws(() => new Options(options)); 217 | } 218 | ); 219 | 220 | it( 221 | "should return a validated version of the specified property" + 222 | " if undefined", 223 | () => { 224 | const options = {}; 225 | assert.deepEqual(new Options(options).dtd, { 226 | include: false, 227 | name: undefined, 228 | pubId: undefined, 229 | sysId: undefined, 230 | }); 231 | } 232 | ); 233 | }); 234 | 235 | describe("format", () => { 236 | it("should leave the specified property unchanged if valid", () => { 237 | const options = { 238 | format: { 239 | doubleQuotes: undefined, 240 | indent: undefined, 241 | newline: undefined, 242 | pretty: undefined, 243 | }, 244 | }; 245 | assert.deepEqual(new Options(options).format, options.format); 246 | }); 247 | 248 | it( 249 | "should return a validated version of the specified property" + 250 | " if undefined", 251 | () => { 252 | const options = {}; 253 | assert.deepEqual(new Options(options).format, { 254 | doubleQuotes: undefined, 255 | indent: undefined, 256 | newline: undefined, 257 | pretty: undefined, 258 | }); 259 | } 260 | ); 261 | }); 262 | 263 | describe("replaceInvalidChars", () => { 264 | it("should leave the specified property unchanged if valid", () => { 265 | const options = { 266 | replaceInvalidChars: false, 267 | }; 268 | assert.strictEqual( 269 | new Options(options).replaceInvalidChars, 270 | options.replaceInvalidChars 271 | ); 272 | 273 | options.replaceInvalidChars = true; 274 | assert.strictEqual( 275 | new Options(options).replaceInvalidChars, 276 | options.replaceInvalidChars 277 | ); 278 | }); 279 | 280 | it( 281 | "should return a validated version of the specified property" + 282 | " if undefined", 283 | () => { 284 | const options = {}; 285 | assert.strictEqual( 286 | new Options(options).replaceInvalidChars, 287 | false 288 | ); 289 | } 290 | ); 291 | }); 292 | 293 | describe("typeHandlers", () => { 294 | it("should leave the specified property unchanged if valid", () => { 295 | const typeHandlers: ITypeHandlers = { 296 | test1: () => { 297 | return "test2"; 298 | }, 299 | test3: () => { 300 | return "test4"; 301 | }, 302 | }; 303 | const options = { 304 | typeHandlers, 305 | }; 306 | assert.deepEqual( 307 | new Options(options).typeHandlers, 308 | options.typeHandlers 309 | ); 310 | 311 | options.typeHandlers = {}; 312 | assert.deepEqual( 313 | new Options(options).typeHandlers, 314 | options.typeHandlers 315 | ); 316 | }); 317 | 318 | it( 319 | "should return a validated version of the specified property" + 320 | " if undefined", 321 | () => { 322 | const options = {}; 323 | assert.deepEqual(new Options(options).typeHandlers, {}); 324 | } 325 | ); 326 | }); 327 | 328 | describe("useSelfClosingTagIfEmpty", () => { 329 | it("should leave the specified property unchanged if valid", () => { 330 | const options = { 331 | useSelfClosingTagIfEmpty: true, 332 | }; 333 | assert.strictEqual( 334 | new Options(options).useSelfClosingTagIfEmpty, 335 | options.useSelfClosingTagIfEmpty 336 | ); 337 | 338 | options.useSelfClosingTagIfEmpty = false; 339 | assert.strictEqual( 340 | new Options(options).useSelfClosingTagIfEmpty, 341 | options.useSelfClosingTagIfEmpty 342 | ); 343 | }); 344 | 345 | it( 346 | "should return a validated version of the specified property" + 347 | " if undefined", 348 | () => { 349 | const options = {}; 350 | assert.strictEqual( 351 | new Options(options).useSelfClosingTagIfEmpty, 352 | true 353 | ); 354 | } 355 | ); 356 | }); 357 | 358 | describe("validation", () => { 359 | it("should leave the specified property unchanged if valid", () => { 360 | const options = { 361 | validation: true, 362 | }; 363 | assert.strictEqual( 364 | new Options(options).validation, 365 | options.validation 366 | ); 367 | 368 | options.validation = false; 369 | assert.strictEqual( 370 | new Options(options).validation, 371 | options.validation 372 | ); 373 | }); 374 | 375 | it( 376 | "should return a validated version of the specified property" + 377 | " if undefined", 378 | () => { 379 | const options = {}; 380 | assert.strictEqual(new Options(options).validation, true); 381 | } 382 | ); 383 | }); 384 | 385 | describe("valueString", () => { 386 | it("should leave the specified property unchanged if valid", () => { 387 | const options = { 388 | valueString: "#", 389 | }; 390 | assert.strictEqual( 391 | new Options(options).valueString, 392 | options.valueString 393 | ); 394 | 395 | options.valueString = "test"; 396 | assert.strictEqual( 397 | new Options(options).valueString, 398 | options.valueString 399 | ); 400 | }); 401 | 402 | it( 403 | "should return a validated version of the specified property" + 404 | " if undefined", 405 | () => { 406 | const options = {}; 407 | assert.strictEqual(new Options(options).valueString, "#"); 408 | } 409 | ); 410 | }); 411 | 412 | describe("wrapHandlers", () => { 413 | it("should leave the specified property unchanged if valid", () => { 414 | const wrapHandlers: IWrapHandlers = { 415 | test1: () => { 416 | return "test2"; 417 | }, 418 | test3: () => { 419 | return "test4"; 420 | }, 421 | }; 422 | const options = { 423 | wrapHandlers, 424 | }; 425 | assert.deepEqual( 426 | new Options(options).wrapHandlers, 427 | options.wrapHandlers 428 | ); 429 | 430 | options.wrapHandlers = {}; 431 | assert.deepEqual( 432 | new Options(options).wrapHandlers, 433 | options.wrapHandlers 434 | ); 435 | }); 436 | 437 | it( 438 | "should return a validated version of the specified property" + 439 | " if undefined", 440 | () => { 441 | const options = {}; 442 | assert.deepEqual(new Options(options).wrapHandlers, {}); 443 | } 444 | ); 445 | }); 446 | }); 447 | }); 448 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2020 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {isUndefined} from "./utils"; 18 | 19 | /** 20 | * The options associated with parsing an object and formatting the resulting 21 | * XML. 22 | */ 23 | export interface IOptions { 24 | /** 25 | * If an object or map contains a key that, when converted to a string, 26 | * is equal to the value of `aliasString`, then the name of the XML element 27 | * containing the object will be replaced with the value associated with 28 | * said key. 29 | * 30 | * For example, if `aliasString` is `"="`, then the following object: 31 | * ```javascript 32 | * { 33 | * "abc": { 34 | * "=": "def" 35 | * "#": "ghi" 36 | * } 37 | * } 38 | * ``` 39 | * will result in the following XML for a root element named `"root"`: 40 | * ```xml 41 | * 42 | * ghi 43 | * 44 | * ``` 45 | * 46 | * The handlers in `typeHandlers` are not applied to the value. 47 | * 48 | * The default alias string is `"="`. 49 | */ 50 | aliasString?: string; 51 | /** 52 | * If an object or map contains a key that, when converted to a string, 53 | * begins with the value of `attributeString`, then the value mapped by 54 | * said key will be interpreted as attributes for the XML element for that 55 | * object. 56 | * 57 | * The keys of the value of `attributeString` are interpreted as attribute 58 | * names, while the values mapping to those keys are interpreted as 59 | * attribute values. 60 | * 61 | * For example, if `attributeString` is `"@"`, then the following object: 62 | * ```javascript 63 | * { 64 | * "abc": { 65 | * "@1": { 66 | * "ghi": "jkl", 67 | * "mno": "pqr" 68 | * }, 69 | * "stu": "vwx", 70 | * "@2": { 71 | * "yza": "bcd" 72 | * }, 73 | * } 74 | * } 75 | * ``` 76 | * will result in the following XML for a root element named `"root"`: 77 | * ```xml 78 | * 79 | * 80 | * vwx 81 | * 82 | * 83 | * ``` 84 | * 85 | * The handlers in `typeHandlers` are applied to the values. 86 | * 87 | * The default attribute string is `"@"`. 88 | */ 89 | attributeString?: string; 90 | /** 91 | * Whether to enclose any text containing the characters `<` or `&` 92 | * in CDATA sections. If this is false, these characters shall be replaced 93 | * with XML escape characters instead. 94 | * 95 | * By default, this is disabled. 96 | */ 97 | cdataInvalidChars?: boolean; 98 | /** 99 | * If an object or map contains a key that, when converted to a string, is 100 | * equal to an item in `cdataKeys`, then the value mapped by said key will 101 | * be enclosed in a CDATA section. 102 | * 103 | * For example, if `cdataKeys` is: 104 | * ```javascript 105 | * [ 106 | * "abc" 107 | * ] 108 | * ``` 109 | * then the following object: 110 | * ```javascript 111 | * { 112 | * "abc": "def&", 113 | * "ghi": "jkl", 114 | * "mno": "pqr<" 115 | * } 116 | * ``` 117 | * will result in the following XML for a root element named `"root"`: 118 | * ```xml 119 | * 120 | * 121 | * jlk 122 | * pqr< 123 | * 124 | * ``` 125 | * 126 | * If `cdataKeys` has a key named `"*"`, then that entry will match all 127 | * keys. 128 | * 129 | * By default, this is an empty array. 130 | */ 131 | cdataKeys?: string[]; 132 | /** 133 | * The options associated with the XML declaration. 134 | */ 135 | declaration?: IDeclarationOptions; 136 | /** 137 | * The options associated with the XML document type definition. 138 | */ 139 | dtd?: IDtdOptions; 140 | /** 141 | * The options associated with the formatting of the XML document. 142 | */ 143 | format?: IFormatOptions; 144 | /** 145 | * Whether to replace any characters that are not valid in XML in particular 146 | * contexts with the Unicode replacement character, U+FFFD. 147 | * 148 | * At present this is limited to attribute names and values; element names 149 | * and character data; CDATA sections; and comments. This may be extended 150 | * in future. 151 | * 152 | * By default, this is disabled. 153 | */ 154 | replaceInvalidChars?: boolean; 155 | /** 156 | * If a value has a type (as defined by calling `Object.prototype.toString` 157 | * on the value) equal to a key in `typeHandlers`, then said value will be 158 | * replaced by the return value of the function mapped to by the key in 159 | * `typeHandlers`. This function is called with the value as a parameter. 160 | * 161 | * If one of these functions returns the sole instance of {@link Absent}, 162 | * then the value will be suppressed from the XML output altogether. 163 | * 164 | * For example, if `typeHandlers` is: 165 | * ```javascript 166 | * { 167 | * "[object Date]": function(value) { 168 | * return value.getYear(); 169 | * }, 170 | * "[object Null]": function(value) { 171 | * return Absent.instance; 172 | * } 173 | * } 174 | * ``` 175 | * then the following object: 176 | * ```javascript 177 | * { 178 | * "abc": new Date(2012, 10, 31), 179 | * "def": null 180 | * } 181 | * ``` 182 | * will result in the following XML for a root element named `"root"`: 183 | * ```xml 184 | * 185 | * 2012 186 | * 187 | * ``` 188 | * 189 | * If `typeHandlers` has a key named `"*"`, then that entry will match all 190 | * values, unless there is a more specific entry. 191 | * 192 | * Note that normal parsing still occurs for the value returned by the 193 | * function; it is not directly converted to a string. 194 | * 195 | * The default value is an empty object. 196 | */ 197 | typeHandlers?: ITypeHandlers; 198 | /** 199 | * Whether to use a self-closing tag for empty elements. 200 | * 201 | * For example, the following element will be used: 202 | * ```xml 203 | * 204 | * ``` 205 | * instead of: 206 | * ```xml 207 | * 208 | * ``` 209 | * 210 | * By default, this is enabled. 211 | */ 212 | useSelfClosingTagIfEmpty?: boolean; 213 | /** 214 | * Whether to throw an exception if basic XML validation fails while 215 | * building the document. 216 | * 217 | * By default, this is enabled. 218 | */ 219 | validation?: boolean; 220 | /** 221 | * If an object or map contains a key that, when converted to a string, 222 | * begins with the value of `valueString`, then the value mapped by said key 223 | * will be represented as bare text within the XML element for that object. 224 | * 225 | * For example, if `valueString` is `"#"`, then the following object: 226 | * ```javascript 227 | * new Map([ 228 | * ["#1", "abc"], 229 | * ["def", "ghi"], 230 | * ["#2", "jkl"] 231 | * ]) 232 | * ``` 233 | * will result in the following XML for a root element named `"root"`: 234 | * ```xml 235 | * 236 | * abc 237 | * ghi 238 | * jkl 239 | * 240 | * ``` 241 | * 242 | * The handlers in `typeHandlers` are applied to the value. 243 | * 244 | * The default value is `"#"`. 245 | */ 246 | valueString?: string; 247 | /** 248 | * If an object or map contains a key that, when converted to a string, is 249 | * equal to a key in `wrapHandlers`, and the key in said object or map maps 250 | * to an array or set, then all items in the array or set will be wrapped 251 | * in an XML element with the same name as the key. 252 | * 253 | * The key in `wrapHandlers` must map to a function that is called with the 254 | * key name, as well as the array or set, as parameters. This function must 255 | * return a string or value that can be converted to a string, which will 256 | * become the name for each XML element for each item in the array or set. 257 | * Alternatively, this function may return `null` to indicate that no 258 | * wrapping should occur. 259 | * 260 | * For example, if `wrapHandlers` is: 261 | * ```javascript 262 | * { 263 | * "abc": function(key, value) { 264 | * return "def"; 265 | * } 266 | * } 267 | * ``` 268 | * then the following object: 269 | * ```javascript 270 | * { 271 | * "ghi": "jkl", 272 | * "mno": { 273 | * "pqr": ["s", "t"] 274 | * }, 275 | * "uvw": { 276 | * "abc": ["x", "y"] 277 | * } 278 | * } 279 | * ``` 280 | * will result in the following XML for a root element named `"root"`: 281 | * ```xml 282 | * 283 | * jkl 284 | * 285 | * s 286 | * t 287 | * 288 | * 289 | * 290 | * x 291 | * y 292 | * 293 | * 294 | * 295 | * ``` 296 | * 297 | * If `wrapHandlers` has a key named `"*"`, then that entry will 298 | * match all arrays and sets, unless there is a more specific entry. 299 | * 300 | * The default value is an empty object. 301 | */ 302 | wrapHandlers?: IWrapHandlers; 303 | } 304 | 305 | /** 306 | * Implementation of the IOptions interface used to provide default values 307 | * to fields. 308 | */ 309 | export class Options implements IOptions { 310 | public aliasString = "="; 311 | public attributeString = "@"; 312 | public cdataInvalidChars = false; 313 | public cdataKeys: string[] = []; 314 | public declaration: DeclarationOptions; 315 | public dtd: DtdOptions; 316 | public format: FormatOptions; 317 | public replaceInvalidChars = false; 318 | public typeHandlers: TypeHandlers; 319 | public useSelfClosingTagIfEmpty = true; 320 | public validation = true; 321 | public valueString = "#"; 322 | public wrapHandlers: WrapHandlers; 323 | 324 | constructor(options: IOptions = {}) { 325 | if (!isUndefined(options.validation)) { 326 | this.validation = options.validation; 327 | } 328 | 329 | if (!isUndefined(options.aliasString)) { 330 | this.aliasString = options.aliasString; 331 | } 332 | 333 | if (!isUndefined(options.attributeString)) { 334 | this.attributeString = options.attributeString; 335 | } 336 | 337 | if (!isUndefined(options.cdataInvalidChars)) { 338 | this.cdataInvalidChars = options.cdataInvalidChars; 339 | } 340 | 341 | if (!isUndefined(options.cdataKeys)) { 342 | this.cdataKeys = options.cdataKeys; 343 | } 344 | 345 | this.declaration = new DeclarationOptions(options.declaration); 346 | 347 | this.dtd = new DtdOptions(this.validation, options.dtd); 348 | 349 | this.format = new FormatOptions(options.format); 350 | 351 | if (!isUndefined(options.replaceInvalidChars)) { 352 | this.replaceInvalidChars = options.replaceInvalidChars; 353 | } 354 | 355 | this.typeHandlers = new TypeHandlers(options.typeHandlers); 356 | 357 | if (!isUndefined(options.useSelfClosingTagIfEmpty)) { 358 | this.useSelfClosingTagIfEmpty = options.useSelfClosingTagIfEmpty; 359 | } 360 | 361 | if (!isUndefined(options.valueString)) { 362 | this.valueString = options.valueString; 363 | } 364 | 365 | this.wrapHandlers = new WrapHandlers(options.wrapHandlers); 366 | } 367 | } 368 | 369 | /** 370 | * The options associated with the XML declaration. An example of an XML 371 | * declaration is as follows: 372 | * 373 | * ```xml 374 | * 375 | * ``` 376 | */ 377 | export interface IDeclarationOptions { 378 | /** 379 | * Whether to include a declaration in the generated XML. By default, 380 | * one is included. 381 | */ 382 | include?: boolean; 383 | /** 384 | * The encoding attribute to be included in the declaration. If defined, 385 | * this value must be a valid encoding. By default, no encoding attribute 386 | * is included. 387 | */ 388 | encoding?: string; 389 | /** 390 | * The value of the standalone attribute to be included in the declaration. 391 | * If defined, this value must be "yes" or "no". By default, no standalone 392 | * attribute is included. 393 | */ 394 | standalone?: string; 395 | /** 396 | * The XML version to be included in the declaration. If defined, this 397 | * value must be a valid XML version number. Defaults to "1.0". 398 | */ 399 | version?: string; 400 | } 401 | 402 | /** 403 | * Implementation of the IDeclarationOptions interface used to provide default 404 | * values to fields. 405 | */ 406 | export class DeclarationOptions implements IDeclarationOptions { 407 | public include = true; 408 | public encoding?: string; 409 | public standalone?: string; 410 | public version?: string; 411 | 412 | constructor(declarationOptions: IDeclarationOptions = {}) { 413 | if (!isUndefined(declarationOptions.include)) { 414 | this.include = declarationOptions.include; 415 | } 416 | 417 | // Validation performed by xmlcreate 418 | this.encoding = declarationOptions.encoding; 419 | this.standalone = declarationOptions.standalone; 420 | this.version = declarationOptions.version; 421 | } 422 | } 423 | 424 | /** 425 | * The options associated with the XML document type definition (DTD). An 426 | * example of a DTD is as follows: 427 | * 428 | * ```xml 429 | * 431 | * ``` 432 | */ 433 | export interface IDtdOptions { 434 | /** 435 | * Whether to include a DTD in the generated XML. By default, no DTD is 436 | * included. 437 | */ 438 | include?: boolean; 439 | /** 440 | * The name of the DTD. This value cannot be left undefined if `include` 441 | * is true. 442 | */ 443 | name?: string; 444 | /** 445 | * The system identifier of the DTD, excluding quotation marks. By default, 446 | * no system identifier is included. 447 | */ 448 | sysId?: string; 449 | /** 450 | * The public identifier of the DTD, excluding quotation marks. If a public 451 | * identifier is provided, a system identifier must be provided as well. 452 | * By default, no public identifier is included. 453 | */ 454 | pubId?: string; 455 | } 456 | 457 | /** 458 | * Implementation of the IDtdOptions interface used to provide default values 459 | * to fields. 460 | */ 461 | export class DtdOptions implements IDtdOptions { 462 | public include = false; 463 | public name?: string; 464 | public sysId?: string; 465 | public pubId?: string; 466 | 467 | constructor(validation: boolean, dtdOptions: IDtdOptions = {}) { 468 | if (!isUndefined(dtdOptions.include)) { 469 | this.include = dtdOptions.include; 470 | } 471 | 472 | if (validation && isUndefined(dtdOptions.name) && this.include) { 473 | throw new Error( 474 | "options.dtd.name should be defined if" + 475 | " options.dtd.include is true" 476 | ); 477 | } 478 | 479 | this.name = dtdOptions.name; 480 | this.sysId = dtdOptions.sysId; 481 | this.pubId = dtdOptions.pubId; 482 | } 483 | } 484 | 485 | /** 486 | * The options associated with the formatting of the XML document. 487 | */ 488 | export interface IFormatOptions { 489 | /** 490 | * Whether double quotes or single quotes should be used in XML attributes. 491 | * By default, single quotes are used. 492 | */ 493 | doubleQuotes?: boolean; 494 | /** 495 | * The indent string used for pretty-printing. The default indent string is 496 | * four spaces. 497 | */ 498 | indent?: string; 499 | /** 500 | * The newline string used for pretty-printing. The default newline string 501 | * is "\n". 502 | */ 503 | newline?: string; 504 | /** 505 | * Whether pretty-printing is enabled. By default, pretty-printing is 506 | * enabled. 507 | */ 508 | pretty?: boolean; 509 | } 510 | 511 | /** 512 | * Implementation of the IFormatOptions interface used to provide default values 513 | * to fields. 514 | */ 515 | export class FormatOptions implements IFormatOptions { 516 | public doubleQuotes?: boolean; 517 | public indent?: string; 518 | public newline?: string; 519 | public pretty?: boolean; 520 | 521 | constructor(formatOptions: IFormatOptions = {}) { 522 | this.doubleQuotes = formatOptions.doubleQuotes; 523 | this.indent = formatOptions.indent; 524 | this.newline = formatOptions.newline; 525 | this.pretty = formatOptions.pretty; 526 | } 527 | } 528 | 529 | /** 530 | * Map for the `typeHandlers` property in the {@link IOptions} interface. 531 | */ 532 | export interface ITypeHandlers { 533 | /** 534 | * Mapping between the type of a value in an object to a function taking 535 | * this value and returning a replacement value. 536 | */ 537 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 538 | [type: string]: (value: any) => unknown; 539 | } 540 | 541 | /** 542 | * Implementation of the ITypeHandlers interface used to provide default values 543 | * to fields. 544 | */ 545 | export class TypeHandlers implements ITypeHandlers { 546 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 547 | [type: string]: (value: any) => unknown; 548 | 549 | constructor(typeHandlers: ITypeHandlers = {}) { 550 | for (const key in typeHandlers) { 551 | if (Object.prototype.hasOwnProperty.call(typeHandlers, key)) { 552 | this[key] = typeHandlers[key]; 553 | } 554 | } 555 | } 556 | } 557 | 558 | /** 559 | * Map for the `wrapHandlers` property in the {@link IOptions} interface. 560 | */ 561 | export interface IWrapHandlers { 562 | /** 563 | * Mapping between the string version of a key in an object or map with a 564 | * value that is an array or set to a function taking the string version 565 | * of that key, as well as that array or set. 566 | * 567 | * This function returns either a string that will become the name for each 568 | * XML element for each item in the array or set, or `null` to indicate that 569 | * wrapping should not occur. 570 | */ 571 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 572 | [key: string]: (key: string, value: any) => string | null; 573 | } 574 | 575 | /** 576 | * Implementation of the IWrapHandlers interface used to provide default values 577 | * to fields. 578 | */ 579 | export class WrapHandlers implements IWrapHandlers { 580 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 581 | [key: string]: (key: string, value: any) => string | null; 582 | 583 | constructor(wrapHandlers: IWrapHandlers = {}) { 584 | for (const key in wrapHandlers) { 585 | if (Object.prototype.hasOwnProperty.call(wrapHandlers, key)) { 586 | this[key] = wrapHandlers[key]; 587 | } 588 | } 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /test/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016-2020 Michael Kourlas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import {assert} from "chai"; 18 | import {document} from "xmlcreate"; 19 | import {Absent, parse, parseToExistingElement} from "../../lib/main"; 20 | import {IOptions, ITypeHandlers, IWrapHandlers} from "../../lib/options"; 21 | import {isSet} from "../../lib/utils"; 22 | 23 | const simpleOptions: IOptions = { 24 | declaration: { 25 | include: false, 26 | }, 27 | format: { 28 | pretty: false, 29 | }, 30 | }; 31 | 32 | describe("parser", () => { 33 | describe("#parse", () => { 34 | it("primitives", () => { 35 | assert.strictEqual( 36 | parse("root", "string", simpleOptions), 37 | "string" 38 | ); 39 | assert.strictEqual( 40 | parse("root", 3, simpleOptions), 41 | "3" 42 | ); 43 | assert.strictEqual( 44 | parse("root", true, simpleOptions), 45 | "true" 46 | ); 47 | assert.strictEqual( 48 | parse("root", undefined, simpleOptions), 49 | "undefined" 50 | ); 51 | assert.strictEqual( 52 | parse("root", null, simpleOptions), 53 | "null" 54 | ); 55 | }); 56 | 57 | it("object versions of primitives", () => { 58 | assert.strictEqual( 59 | parse("root", new String("string"), simpleOptions), 60 | "string" 61 | ); 62 | assert.strictEqual( 63 | parse("root", new Number(3), simpleOptions), 64 | "3" 65 | ); 66 | assert.strictEqual( 67 | parse("root", new Boolean(true), simpleOptions), 68 | "true" 69 | ); 70 | }); 71 | 72 | it("simple objects and maps", () => { 73 | assert.strictEqual(parse("root", {}, simpleOptions), ""); 74 | assert.strictEqual( 75 | parse("root", {test: "123"}, simpleOptions), 76 | "123" 77 | ); 78 | assert.strictEqual( 79 | parse("root", {test: "123", test2: "456"}, simpleOptions), 80 | "123456" + "" 81 | ); 82 | assert.strictEqual( 83 | parse("root", new Map(), simpleOptions), 84 | "" 85 | ); 86 | assert.strictEqual( 87 | parse("root", new Map([["test", "123"]]), simpleOptions), 88 | "123" 89 | ); 90 | assert.strictEqual( 91 | parse( 92 | "root", 93 | new Map([ 94 | ["test", "123"], 95 | ["test2", "456"], 96 | ]), 97 | simpleOptions 98 | ), 99 | "123456" 100 | ); 101 | }); 102 | 103 | it("simple arrays and sets", () => { 104 | assert.strictEqual(parse("root", [], simpleOptions), ""); 105 | assert.strictEqual( 106 | parse("root", ["test", "123"], simpleOptions), 107 | "test123" + "" 108 | ); 109 | assert.strictEqual( 110 | parse("root", new Set(), simpleOptions), 111 | "" 112 | ); 113 | assert.strictEqual( 114 | parse("root", new Set(["test", "123"]), simpleOptions), 115 | "test123" + "" 116 | ); 117 | }); 118 | 119 | it("functions and regular expressions", () => { 120 | assert.strictEqual( 121 | parse("root", () => "test", simpleOptions), 122 | 'function () { return "test"; }' + "" 123 | ); 124 | assert.strictEqual( 125 | parse("root", /test/, simpleOptions), 126 | "/test/" 127 | ); 128 | }); 129 | 130 | it("primitives in objects and maps", () => { 131 | assert.strictEqual( 132 | parse( 133 | "root", 134 | { 135 | test: "str", 136 | test2: 3, 137 | test3: true, 138 | test4: undefined, 139 | test5: null, 140 | test6: new String("str2"), 141 | test7: new Number(6), 142 | test8: new Boolean(false), 143 | }, 144 | simpleOptions 145 | ), 146 | "str3true" + 147 | "undefinednullstr2" + 148 | "6false" 149 | ); 150 | assert.strictEqual( 151 | parse( 152 | "root", 153 | new Map([ 154 | ["test", "str"], 155 | ["test2", 3], 156 | ["test3", true], 157 | ["test4", undefined], 158 | ["test5", null], 159 | ["test6", new String("str2")], 160 | ["test7", new Number(6)], 161 | ["test8", new Boolean(false)], 162 | ]), 163 | simpleOptions 164 | ), 165 | "str3true" + 166 | "undefinednullstr2" + 167 | "6false" 168 | ); 169 | assert.strictEqual( 170 | parse( 171 | "root", 172 | new Map([ 173 | [false, "str1"], 174 | [undefined, "str2"], 175 | [null, "str3"], 176 | ]), 177 | simpleOptions 178 | ), 179 | "str1str2" + 180 | "str3" 181 | ); 182 | }); 183 | 184 | it("primitives in arrays and sets", () => { 185 | assert.strictEqual( 186 | parse( 187 | "root", 188 | [ 189 | "test", 190 | 3, 191 | false, 192 | undefined, 193 | null, 194 | new String("str1"), 195 | new Number(5), 196 | new Boolean(false), 197 | ], 198 | simpleOptions 199 | ), 200 | "test3false" + 201 | "undefinednullstr1" + 202 | "5false" 203 | ); 204 | assert.strictEqual( 205 | parse( 206 | "root", 207 | new Set([ 208 | "test", 209 | 3, 210 | false, 211 | undefined, 212 | null, 213 | new String("str1"), 214 | new Number(5), 215 | new Boolean(false), 216 | ]), 217 | simpleOptions 218 | ), 219 | "test3false" + 220 | "undefinednullstr1" + 221 | "5false" 222 | ); 223 | }); 224 | 225 | it("nested objects and maps", () => { 226 | assert.strictEqual( 227 | parse( 228 | "root", 229 | { 230 | test: { 231 | test15: "test16", 232 | test17: { 233 | test18: "test19", 234 | test20: "test21", 235 | }, 236 | test2: new Map([ 237 | ["test3", "test4"], 238 | [ 239 | "test5", 240 | { 241 | test6: "test7", 242 | test8: "test9", 243 | }, 244 | ], 245 | [ 246 | "test10", 247 | new Map([ 248 | ["test11", "test12"], 249 | ["test13", "test14"], 250 | ]), 251 | ], 252 | ]), 253 | }, 254 | }, 255 | simpleOptions 256 | ), 257 | "test16" + 258 | "test19test21" + 259 | "test4" + 260 | "test7test9" + 261 | "test12test14" + 262 | "" 263 | ); 264 | }); 265 | 266 | it("nested arrays and sets", () => { 267 | assert.strictEqual( 268 | parse( 269 | "root", 270 | [ 271 | ["a", "b", "c", ["d", "e"]], 272 | new Set([ 273 | "f", 274 | "g", 275 | "h", 276 | new Set(["i", "j"]), 277 | ["k", "l"], 278 | ]), 279 | ], 280 | simpleOptions 281 | ), 282 | "abc" + 283 | "defg" + 284 | "hijk" + 285 | "l" 286 | ); 287 | }); 288 | 289 | it("complex combinations of objects, maps, arrays, and sets", () => { 290 | assert.strictEqual( 291 | parse( 292 | "root", 293 | { 294 | test1: { 295 | test12: new Set([ 296 | "test13", 297 | { 298 | test14: "test15", 299 | test16: "test17", 300 | }, 301 | new Map([ 302 | ["test18", "test19"], 303 | ["test20", "test21"], 304 | ]), 305 | ]), 306 | test2: [ 307 | "test3", 308 | { 309 | test4: "test5", 310 | test6: "test7", 311 | }, 312 | new Map([ 313 | ["test8", "test9"], 314 | ["test10", "test11"], 315 | ]), 316 | ], 317 | test43: "test44", 318 | }, 319 | test22: new Map([ 320 | ["test45", "test46"], 321 | [ 322 | "test23", 323 | [ 324 | "test24", 325 | { 326 | test25: "test26", 327 | test27: "test28", 328 | }, 329 | new Map([ 330 | ["test29", "test30"], 331 | ["test31", "test32"], 332 | ]), 333 | ], 334 | ], 335 | [ 336 | "test33", 337 | new Set([ 338 | "test34", 339 | { 340 | test35: "test36", 341 | test37: "test38", 342 | }, 343 | new Map([ 344 | ["test39", "test40"], 345 | ["test41", "test42"], 346 | ]), 347 | ]), 348 | ], 349 | ]), 350 | }, 351 | simpleOptions 352 | ), 353 | "test13" + 354 | "test15test17" + 355 | "test19test21" + 356 | "test3test5" + 357 | "test7" + 358 | "test9test11" + 359 | "test44test46" + 360 | "test24test26" + 361 | "test28test30" + 362 | "test32test34" + 363 | "test36test38" + 364 | "test40" + 365 | "test42" 366 | ); 367 | }); 368 | 369 | describe("options", () => { 370 | it("aliasString", () => { 371 | const aliasStringOptions: IOptions = { 372 | aliasString: "_customAliasString", 373 | declaration: { 374 | include: false, 375 | }, 376 | format: { 377 | pretty: false, 378 | }, 379 | }; 380 | 381 | assert.strictEqual( 382 | parse( 383 | "root", 384 | { 385 | "=": "testRoot", 386 | test1: "test2", 387 | test3: "test4", 388 | }, 389 | simpleOptions 390 | ), 391 | "test2" + 392 | "test4" 393 | ); 394 | assert.strictEqual( 395 | parse( 396 | "root", 397 | new Map([ 398 | ["=", "testRoot"], 399 | ["test1", "test2"], 400 | ["test3", "test4"], 401 | ]), 402 | simpleOptions 403 | ), 404 | "test2" + 405 | "test4" 406 | ); 407 | assert.strictEqual( 408 | parse( 409 | "root", 410 | { 411 | test1: "test2", 412 | test3: { 413 | "=": "test4", 414 | test5: "test6", 415 | }, 416 | test7: new Map([ 417 | ["=", "test8"], 418 | ["test9", "test10"], 419 | ]), 420 | }, 421 | simpleOptions 422 | ), 423 | "test2test6" + 424 | "test10" + 425 | "" 426 | ); 427 | assert.strictEqual( 428 | parse( 429 | "root", 430 | new Map([ 431 | ["test1", "test2"], 432 | [ 433 | "test3", 434 | { 435 | "=": "test4", 436 | test5: "test6", 437 | }, 438 | ], 439 | [ 440 | "test7", 441 | new Map([ 442 | ["=", "test8"], 443 | ["test9", "test10"], 444 | ]), 445 | ], 446 | ]), 447 | simpleOptions 448 | ), 449 | "test2test6" + 450 | "test10" + 451 | "" 452 | ); 453 | assert.strictEqual( 454 | parse( 455 | "root", 456 | [ 457 | { 458 | "=": "test1", 459 | test2: "test3", 460 | }, 461 | new Map([ 462 | ["=", "test4"], 463 | ["test5", "test6"], 464 | ]), 465 | { 466 | test7: "test8", 467 | test9: [ 468 | { 469 | "=": "test10", 470 | test11: "test12", 471 | }, 472 | new Map([ 473 | ["=", "test13"], 474 | ["test14", "test15"], 475 | ]), 476 | ], 477 | }, 478 | ], 479 | simpleOptions 480 | ), 481 | "test3" + 482 | "test6test8" + 483 | "test12" + 484 | "test15" + 485 | "" 486 | ); 487 | assert.strictEqual( 488 | parse( 489 | "root", 490 | new Set([ 491 | { 492 | "=": "test1", 493 | test2: "test3", 494 | }, 495 | new Map([ 496 | ["=", "test4"], 497 | ["test5", "test6"], 498 | ]), 499 | { 500 | test7: "test8", 501 | test9: new Set([ 502 | { 503 | "=": "test10", 504 | test11: "test12", 505 | }, 506 | new Map([ 507 | ["=", "test13"], 508 | ["test14", "test15"], 509 | ]), 510 | ]), 511 | }, 512 | ]), 513 | simpleOptions 514 | ), 515 | "test3" + 516 | "test6test8" + 517 | "test12" + 518 | "test15" + 519 | "" 520 | ); 521 | assert.strictEqual( 522 | parse( 523 | "root", 524 | { 525 | _customAliasString: "test1", 526 | test2: "test3", 527 | test4: { 528 | _customAliasString: "test5", 529 | test6: "test7", 530 | }, 531 | }, 532 | aliasStringOptions 533 | ), 534 | "test3test7" + 535 | "" 536 | ); 537 | }); 538 | 539 | it("attributeString", () => { 540 | const attributeStringOptions: IOptions = { 541 | attributeString: "attributeString", 542 | declaration: { 543 | include: false, 544 | }, 545 | format: { 546 | pretty: false, 547 | }, 548 | }; 549 | 550 | assert.strictEqual( 551 | parse( 552 | "root", 553 | { 554 | "@": { 555 | test1: "test2", 556 | test3: "test4", 557 | test5: 3, 558 | test6: null, 559 | test7: undefined, 560 | test8: true, 561 | }, 562 | }, 563 | simpleOptions 564 | ), 565 | "" 567 | ); 568 | 569 | assert.strictEqual( 570 | parse( 571 | "root", 572 | { 573 | test5: { 574 | "@": { 575 | test1: "test2", 576 | test3: "test4", 577 | }, 578 | test6: "test7", 579 | }, 580 | }, 581 | simpleOptions 582 | ), 583 | "" + 584 | "test7" 585 | ); 586 | 587 | assert.throws(() => { 588 | parse( 589 | "root", 590 | { 591 | attributeString: { 592 | test1: "test2", 593 | test3: "test4", 594 | }, 595 | test5: { 596 | "@": { 597 | test1: "test2", 598 | test3: "test4", 599 | }, 600 | }, 601 | }, 602 | attributeStringOptions 603 | ); 604 | }); 605 | }); 606 | 607 | it("cdataInvalidChars", () => { 608 | const cdataInvalidCharsOptions: IOptions = { 609 | cdataInvalidChars: true, 610 | declaration: { 611 | include: false, 612 | }, 613 | format: { 614 | pretty: false, 615 | }, 616 | }; 617 | 618 | assert.strictEqual( 619 | parse( 620 | "root", 621 | { 622 | test1: "a&b", 623 | test2: "cd&ea&bc<d" + 629 | "a&b<c]]>d&e<f" 630 | ); 631 | 632 | assert.strictEqual( 633 | parse( 634 | "root", 635 | { 636 | test1: "a&b", 637 | test2: "cd&ecdata_not_required", 639 | }, 640 | cdataInvalidCharsOptions 641 | ), 642 | "" + 643 | "" + 644 | "]]>]]>cdata_not_required" + 645 | "" 646 | ); 647 | }); 648 | 649 | it("cdataKeys", () => { 650 | const cdataKeysOptions: IOptions = { 651 | cdataKeys: ["test1"], 652 | declaration: { 653 | include: false, 654 | }, 655 | format: { 656 | pretty: false, 657 | }, 658 | }; 659 | const cdataKeysWildcardOptions: IOptions = { 660 | cdataKeys: ["test1", "*"], 661 | declaration: { 662 | include: false, 663 | }, 664 | format: { 665 | pretty: false, 666 | }, 667 | }; 668 | 669 | assert.strictEqual( 670 | parse( 671 | "root", 672 | { 673 | test1: "ab", 674 | test2: { 675 | test1: "ab&", 676 | }, 677 | test3: "ab&", 678 | test4: "cd", 679 | test5: { 680 | test1: "ab&]]>no_cdata_required", 681 | }, 682 | }, 683 | cdataKeysOptions 684 | ), 685 | "" + 686 | "ab&" + 687 | "cd" + 688 | "]]>" + 689 | "" 690 | ); 691 | 692 | assert.strictEqual( 693 | parse( 694 | "root", 695 | { 696 | test1: "ab", 697 | test2: { 698 | test1: "ab&", 699 | }, 700 | test3: "ab&", 701 | test4: "cd", 702 | test5: { 703 | test1: "ab&]]>no_cdata_required", 704 | }, 705 | }, 706 | cdataKeysWildcardOptions 707 | ), 708 | "" + 709 | "" + 710 | "" + 711 | "]]>" + 712 | "" 713 | ); 714 | }); 715 | 716 | it("declaration", () => { 717 | const declOptions: IOptions = { 718 | declaration: { 719 | encoding: "UTF-8", 720 | include: true, 721 | standalone: "yes", 722 | version: "1.0", 723 | }, 724 | format: { 725 | pretty: false, 726 | }, 727 | }; 728 | 729 | assert.strictEqual( 730 | parse( 731 | "root", 732 | { 733 | test1: "test2", 734 | }, 735 | declOptions 736 | ), 737 | "test2" 739 | ); 740 | }); 741 | 742 | it("dtd", () => { 743 | const dtdOptions: IOptions = { 744 | declaration: { 745 | include: false, 746 | }, 747 | dtd: { 748 | include: true, 749 | name: "a", 750 | pubId: "c", 751 | sysId: "b", 752 | }, 753 | format: { 754 | pretty: false, 755 | }, 756 | }; 757 | 758 | assert.strictEqual( 759 | parse( 760 | "root", 761 | { 762 | test1: "test2", 763 | }, 764 | dtdOptions 765 | ), 766 | "test2" + 767 | "" 768 | ); 769 | }); 770 | 771 | it("format", () => { 772 | const formatOptions: IOptions = { 773 | declaration: { 774 | include: false, 775 | }, 776 | format: { 777 | doubleQuotes: true, 778 | indent: "\t", 779 | newline: "\r\n", 780 | pretty: true, 781 | }, 782 | }; 783 | 784 | assert.strictEqual( 785 | parse( 786 | "root", 787 | { 788 | test1: "test2", 789 | test3: "test4\ntest5", 790 | }, 791 | formatOptions 792 | ), 793 | "\r\n\ttest2\r\n\t" + 794 | "test4\ntest5\r\n" 795 | ); 796 | }); 797 | 798 | it("replaceInvalidChars", () => { 799 | const replaceInvalidCharsOptions: IOptions = { 800 | declaration: { 801 | include: false, 802 | }, 803 | format: { 804 | pretty: false, 805 | }, 806 | replaceInvalidChars: true, 807 | }; 808 | 809 | assert.strictEqual( 810 | parse( 811 | "root", 812 | { 813 | test1: "test2\u0001", 814 | }, 815 | replaceInvalidCharsOptions 816 | ), 817 | "test2\uFFFD" 818 | ); 819 | }); 820 | 821 | it("typeHandlers", () => { 822 | const typeHandlers: ITypeHandlers = { 823 | "[object Null]": () => Absent.instance, 824 | // eslint-disable-next-line max-len 825 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 826 | "[object Number]": (val: any) => val + 17, 827 | }; 828 | const typeHandlersOptions: IOptions = { 829 | declaration: { 830 | include: false, 831 | }, 832 | format: { 833 | pretty: false, 834 | }, 835 | typeHandlers, 836 | }; 837 | 838 | const typeHandlersWildcard: ITypeHandlers = { 839 | // eslint-disable-next-line max-len 840 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 841 | "*": (val: any) => { 842 | if (typeof val === "string") { 843 | return val + "abc"; 844 | } else { 845 | return val; 846 | } 847 | }, 848 | // eslint-disable-next-line max-len 849 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 850 | "[object Number]": (val: any) => val + 17, 851 | }; 852 | const typeHandlersWildcardOptions: IOptions = { 853 | declaration: { 854 | include: false, 855 | }, 856 | format: { 857 | pretty: false, 858 | }, 859 | typeHandlers: typeHandlersWildcard, 860 | }; 861 | 862 | assert.strictEqual( 863 | parse( 864 | "root", 865 | { 866 | test1: 3, 867 | test2: "test3", 868 | test4: null, 869 | }, 870 | typeHandlersOptions 871 | ), 872 | "20test3" 873 | ); 874 | 875 | assert.strictEqual( 876 | parse( 877 | "root", 878 | { 879 | test1: { 880 | "@": { 881 | test2: null, 882 | test3: 26, 883 | }, 884 | "#": null, 885 | }, 886 | "#": 3, 887 | test4: "test5", 888 | }, 889 | typeHandlersOptions 890 | ), 891 | "20test5" + 892 | "" 893 | ); 894 | 895 | assert.strictEqual( 896 | parse( 897 | "root", 898 | { 899 | test1: 3, 900 | test2: "test3", 901 | test4: null, 902 | }, 903 | typeHandlersWildcardOptions 904 | ), 905 | "20test3abc" + 906 | "null" 907 | ); 908 | }); 909 | 910 | it("useSelfClosingTagIfEmpty", () => { 911 | const useSelfClosingTagIfEmptyOptions: IOptions = { 912 | declaration: { 913 | include: false, 914 | }, 915 | format: { 916 | pretty: false, 917 | }, 918 | useSelfClosingTagIfEmpty: false, 919 | }; 920 | 921 | assert.strictEqual( 922 | parse( 923 | "root", 924 | { 925 | test1: "", 926 | }, 927 | useSelfClosingTagIfEmptyOptions 928 | ), 929 | "" 930 | ); 931 | assert.strictEqual( 932 | parse( 933 | "root", 934 | { 935 | test1: "", 936 | }, 937 | simpleOptions 938 | ), 939 | "" 940 | ); 941 | }); 942 | 943 | it("validation", () => { 944 | const validationOptions: IOptions = { 945 | declaration: { 946 | include: false, 947 | }, 948 | format: { 949 | pretty: false, 950 | }, 951 | validation: false, 952 | }; 953 | 954 | assert.strictEqual( 955 | parse( 956 | "root", 957 | { 958 | test1: "\u0001", 959 | }, 960 | validationOptions 961 | ), 962 | "\u0001" 963 | ); 964 | assert.throws(() => 965 | parse( 966 | "root", 967 | { 968 | test1: "\u0001", 969 | }, 970 | simpleOptions 971 | ) 972 | ); 973 | }); 974 | 975 | it("valueString", () => { 976 | const valueStringOptions: IOptions = { 977 | declaration: { 978 | include: false, 979 | }, 980 | format: { 981 | pretty: false, 982 | }, 983 | valueString: "valueString", 984 | }; 985 | 986 | assert.strictEqual( 987 | parse( 988 | "root", 989 | { 990 | test1: { 991 | "#": "test6", 992 | test2: "test3", 993 | test4: "test5", 994 | }, 995 | test13: { 996 | "#": 3, 997 | }, 998 | test14: { 999 | "#": true, 1000 | }, 1001 | test15: { 1002 | "#": null, 1003 | }, 1004 | test16: { 1005 | "#": undefined, 1006 | }, 1007 | test7: new Map([ 1008 | ["test8", "test9"], 1009 | ["#", "test10"], 1010 | ["test11", "test12"], 1011 | ]), 1012 | }, 1013 | simpleOptions 1014 | ), 1015 | "test6test3test5" + 1016 | "3true" + 1017 | "nullundefined" + 1018 | "test9test10" + 1019 | "test12" 1020 | ); 1021 | 1022 | assert.strictEqual( 1023 | parse( 1024 | "root", 1025 | { 1026 | test1: { 1027 | test2: "test3", 1028 | test4: "test5", 1029 | valueString: "test6", 1030 | }, 1031 | }, 1032 | valueStringOptions 1033 | ), 1034 | "test3test5" + 1035 | "test6" 1036 | ); 1037 | }); 1038 | 1039 | it("wrapHandlers", () => { 1040 | const wrapHandlers: IWrapHandlers = { 1041 | test1: () => "test2", 1042 | test17: () => null, 1043 | // eslint-disable-next-line max-len 1044 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1045 | test3: (key: string, value: any) => 1046 | "test4" + 1047 | key + 1048 | (isSet(value) ? value.values().next().value : value[0]), 1049 | }; 1050 | const wrapHandlersOptions: IOptions = { 1051 | declaration: { 1052 | include: false, 1053 | }, 1054 | format: { 1055 | pretty: false, 1056 | }, 1057 | wrapHandlers, 1058 | }; 1059 | 1060 | const wrapHandlersWildcard: IWrapHandlers = { 1061 | "*": () => "test5", 1062 | test1: () => "test2", 1063 | // eslint-disable-next-line max-len 1064 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 1065 | test3: (key: string, value: any) => 1066 | "test4" + 1067 | key + 1068 | (isSet(value) ? value.values().next().value : value[0]), 1069 | }; 1070 | const wrapHandlersWildcardOptions: IOptions = { 1071 | declaration: { 1072 | include: false, 1073 | }, 1074 | format: { 1075 | pretty: false, 1076 | }, 1077 | wrapHandlers: wrapHandlersWildcard, 1078 | }; 1079 | 1080 | assert.strictEqual( 1081 | parse( 1082 | "root", 1083 | { 1084 | test1: ["test6", "test7"], 1085 | test10: new Map([ 1086 | ["test1", ["test11", "test12"]], 1087 | ["test3", new Set(["test13", "test14"])], 1088 | ]), 1089 | test17: ["test18", "test19"], 1090 | test3: new Set(["test8", "test9"]), 1091 | }, 1092 | wrapHandlersOptions 1093 | ), 1094 | "test6test7" + 1095 | "test11" + 1096 | "test12" + 1097 | "test13" + 1098 | "test14" + 1099 | "test18" + 1100 | "test19" + 1101 | "test8test9" + 1102 | "" 1103 | ); 1104 | 1105 | assert.strictEqual( 1106 | parse( 1107 | "root", 1108 | { 1109 | test1: ["test6", "test7"], 1110 | test10: new Map([ 1111 | ["test1", ["test11", "test12"]], 1112 | ["test3", new Set(["test13", "test14"])], 1113 | ]), 1114 | test17: ["test18", "test19"], 1115 | test3: new Set(["test8", "test9"]), 1116 | }, 1117 | wrapHandlersWildcardOptions 1118 | ), 1119 | "test6test7" + 1120 | "test11" + 1121 | "test12" + 1122 | "test13test14" + 1123 | "" + 1124 | "test18test19" + 1125 | "test8" + 1126 | "test9" + 1127 | "" 1128 | ); 1129 | }); 1130 | }); 1131 | }); 1132 | 1133 | it("#parseToExistingElement", () => { 1134 | const d = document(); 1135 | d.procInst({target: "test4"}); 1136 | const e = d.element({name: "test"}); 1137 | parseToExistingElement(e, {test2: "test3"}, simpleOptions); 1138 | assert.strictEqual( 1139 | d.toString({pretty: false}), 1140 | "test3" 1141 | ); 1142 | }); 1143 | }); 1144 | --------------------------------------------------------------------------------