├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .nycrc ├── .travis.yml ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── package-lock.json ├── package.json ├── perf ├── basic │ ├── escaping.coffee │ └── object.coffee ├── index.coffee └── perf.list ├── src ├── Derivation.coffee ├── DocumentPosition.coffee ├── NodeType.coffee ├── OperationType.coffee ├── Utility.coffee ├── WriterState.coffee ├── XMLAttribute.coffee ├── XMLCData.coffee ├── XMLCharacterData.coffee ├── XMLComment.coffee ├── XMLDOMConfiguration.coffee ├── XMLDOMErrorHandler.coffee ├── XMLDOMImplementation.coffee ├── XMLDOMStringList.coffee ├── XMLDTDAttList.coffee ├── XMLDTDElement.coffee ├── XMLDTDEntity.coffee ├── XMLDTDNotation.coffee ├── XMLDeclaration.coffee ├── XMLDocType.coffee ├── XMLDocument.coffee ├── XMLDocumentCB.coffee ├── XMLDocumentFragment.coffee ├── XMLDummy.coffee ├── XMLElement.coffee ├── XMLNamedNodeMap.coffee ├── XMLNode.coffee ├── XMLNodeFilter.coffee ├── XMLNodeList.coffee ├── XMLProcessingInstruction.coffee ├── XMLRaw.coffee ├── XMLStreamWriter.coffee ├── XMLStringWriter.coffee ├── XMLStringifier.coffee ├── XMLText.coffee ├── XMLTypeInfo.coffee ├── XMLUserDataHandler.coffee ├── XMLWriterBase.coffee └── index.coffee ├── test ├── basic │ ├── attributes.coffee │ ├── begin.coffee │ ├── cdata.coffee │ ├── clone.coffee │ ├── comment.coffee │ ├── createxml.coffee │ ├── doc.coffee │ ├── doctype.coffee │ ├── escaping.coffee │ ├── headless.coffee │ ├── import.coffee │ ├── insert.coffee │ ├── instructions.coffee │ ├── multipleinstances.coffee │ ├── nodetype.coffee │ ├── object.coffee │ ├── objectconsistency.coffee │ ├── prettyattributes.coffee │ ├── prettyattributesstream.coffee │ ├── prevnextroot.coffee │ ├── primitives.coffee │ ├── removeitem.coffee │ ├── skipnullelements.coffee │ ├── streamwriter.coffee │ ├── string.coffee │ ├── stringify.coffee │ ├── stringwriter.coffee │ ├── text.coffee │ ├── tostring.coffee │ ├── validatechar.coffee │ └── xmldeclaration.coffee ├── callback │ ├── attributes.coffee │ ├── beginwithcallback.coffee │ ├── indentation.coffee │ ├── instructions.coffee │ └── object.coffee ├── common.coffee ├── dom │ ├── level1.coffee │ ├── level2.coffee │ └── level3.coffee ├── guards │ ├── attributes.coffee │ ├── beginwithcallback.coffee │ ├── builder.coffee │ ├── cdata.coffee │ ├── comment.coffee │ ├── dtdattlist.coffee │ ├── dtdelement.coffee │ ├── dtdentity.coffee │ ├── dtdnotation.coffee │ ├── element.coffee │ ├── instruction.coffee │ ├── node.coffee │ ├── raw.coffee │ ├── stringify.coffee │ └── text.coffee ├── issues │ ├── 117.coffee │ ├── 120.coffee │ ├── 122.coffee │ ├── 147.coffee │ ├── 157.coffee │ ├── 159.coffee │ ├── 171.coffee │ ├── 175.coffee │ ├── 176.coffee │ ├── 187.coffee │ ├── 190.coffee │ ├── 193.coffee │ ├── 194.coffee │ ├── 195.coffee │ ├── 196.coffee │ ├── 208.coffee │ ├── 208.xml │ ├── 213.coffee │ ├── 222.coffee │ ├── 235.coffee │ ├── 235.json │ ├── 239.coffee │ ├── 249.coffee │ ├── 93.coffee │ ├── 96.coffee │ ├── 97.coffee │ └── encoding.coffee └── xpath │ └── simple.coffee └── typings └── index.d.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: oozcitak 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | coverage/ 4 | .vs/ 5 | .nyc_output -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | appveyor.yml 3 | src 4 | test 5 | coverage 6 | .nyc_output 7 | CHANGELOG.md 8 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["lcov", "text"], 3 | "extension": [".coffee"], 4 | "sourceMap": false, 5 | "instrument": false 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | os: 4 | - "linux" 5 | - "osx" 6 | 7 | node_js: 8 | - "8" 9 | - "10" 10 | - "12" 11 | - "node" 12 | 13 | after_success: 14 | - "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls" 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Coffee", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", 15 | "args": [ 16 | "\"test/**/*.coffee\"" 17 | ], 18 | "outFiles": [ 19 | "${workspaceFolder}/**/*.js" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Ozgur Ozcitak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xmlbuilder-js 2 | 3 | An XML builder for [node.js](https://nodejs.org/) similar to 4 | [java-xmlbuilder](https://github.com/jmurty/java-xmlbuilder). 5 | 6 | [![License](http://img.shields.io/npm/l/xmlbuilder.svg?style=flat-square)](http://opensource.org/licenses/MIT) 7 | [![NPM Version](http://img.shields.io/npm/v/xmlbuilder.svg?style=flat-square)](https://npmjs.com/package/xmlbuilder) 8 | [![NPM Downloads](https://img.shields.io/npm/dm/xmlbuilder.svg?style=flat-square)](https://npmjs.com/package/xmlbuilder) 9 | 10 | [![Travis Build Status](http://img.shields.io/travis/com/oozcitak/xmlbuilder-js.svg?style=flat-square)](https://www.travis-ci.com/github/oozcitak/xmlbuilder-js) 11 | [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/bf7odb20hj77isry?svg=true)](https://ci.appveyor.com/project/oozcitak/xmlbuilder-js) 12 | [![Dev Dependency Status](http://img.shields.io/david/dev/oozcitak/xmlbuilder-js.svg?style=flat-square)](https://david-dm.org/oozcitak/xmlbuilder-js) 13 | [![Code Coverage](https://img.shields.io/coveralls/oozcitak/xmlbuilder-js.svg?style=flat-square)](https://coveralls.io/github/oozcitak/xmlbuilder-js) 14 | 15 | ### Announcing `xmlbuilder2`: 16 | 17 | The new release of `xmlbuilder` is available at [`xmlbuilder2`](https://github.com/oozcitak/xmlbuilder2)! `xmlbuilder2` has been redesigned from the ground up to be fully conforming to the [modern DOM specification](https://dom.spec.whatwg.org). It supports XML namespaces, provides built-in converters for multiple formats, collection functions, and more. Please see [upgrading from xmlbuilder](https://oozcitak.github.io/xmlbuilder2/upgrading-from-xmlbuilder.html) in the wiki. 18 | 19 | New development will be focused towards `xmlbuilder2`; `xmlbuilder` will only receive critical bug fixes. 20 | 21 | ### Installation: 22 | 23 | ``` sh 24 | npm install xmlbuilder 25 | ``` 26 | 27 | ### Usage: 28 | 29 | ``` js 30 | var builder = require('xmlbuilder'); 31 | 32 | var xml = builder.create('root') 33 | .ele('xmlbuilder') 34 | .ele('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 35 | .end({ pretty: true}); 36 | 37 | console.log(xml); 38 | ``` 39 | 40 | will result in: 41 | 42 | ``` xml 43 | 44 | 45 | 46 | git://github.com/oozcitak/xmlbuilder-js.git 47 | 48 | 49 | ``` 50 | 51 | It is also possible to convert objects into nodes: 52 | 53 | ``` js 54 | var builder = require('xmlbuilder'); 55 | 56 | var obj = { 57 | root: { 58 | xmlbuilder: { 59 | repo: { 60 | '@type': 'git', // attributes start with @ 61 | '#text': 'git://github.com/oozcitak/xmlbuilder-js.git' // text node 62 | } 63 | } 64 | } 65 | }; 66 | 67 | var xml = builder.create(obj).end({ pretty: true}); 68 | console.log(xml); 69 | ``` 70 | 71 | If you need to do some processing: 72 | 73 | ``` js 74 | var builder = require('xmlbuilder'); 75 | 76 | var root = builder.create('squares'); 77 | root.com('f(x) = x^2'); 78 | for(var i = 1; i <= 5; i++) 79 | { 80 | var item = root.ele('data'); 81 | item.att('x', i); 82 | item.att('y', i * i); 83 | } 84 | 85 | var xml = root.end({ pretty: true}); 86 | console.log(xml); 87 | ``` 88 | 89 | This will result in: 90 | 91 | ``` xml 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ``` 102 | 103 | ### Documentation: 104 | See the [wiki](https://github.com/oozcitak/xmlbuilder-js/wiki) for details and [examples](https://github.com/oozcitak/xmlbuilder-js/wiki/Examples) for more complex examples. 105 | 106 | ### Donations: 107 | Please consider becoming a backer or sponsor to help support development. 108 | 109 | [Donate Button](https://opencollective.com/xmlbuilder) 110 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "8" 4 | - nodejs_version: "10" 5 | - nodejs_version: "12" 6 | - nodejs_version: "" # latest 7 | 8 | install: 9 | - ps: "Install-Product node $env:nodejs_version" 10 | - "npm install" 11 | 12 | test_script: 13 | - "node --version" 14 | - "npm --version" 15 | - "npm test" 16 | 17 | build: off 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmlbuilder", 3 | "version": "15.1.1", 4 | "keywords": [ 5 | "xml", 6 | "xmlbuilder" 7 | ], 8 | "homepage": "http://github.com/oozcitak/xmlbuilder-js", 9 | "description": "An XML builder for node.js", 10 | "author": "Ozgur Ozcitak ", 11 | "contributors": [], 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/oozcitak/xmlbuilder-js.git" 16 | }, 17 | "bugs": { 18 | "url": "http://github.com/oozcitak/xmlbuilder-js/issues" 19 | }, 20 | "main": "./lib/index", 21 | "typings": "./typings/index.d.ts", 22 | "engines": { 23 | "node": ">=8.0" 24 | }, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "coffee-coverage": "*", 28 | "coffeescript": "2.4.1", 29 | "coveralls": "*", 30 | "istanbul": "*", 31 | "mocha": "*", 32 | "nyc": "*", 33 | "xpath": "*", 34 | "git-state": "*" 35 | }, 36 | "mocha": { 37 | "require": [ 38 | "coffeescript/register", 39 | "coffee-coverage/register-istanbul", 40 | "test/common.coffee" 41 | ], 42 | "recursive": true, 43 | "ui": "tdd", 44 | "reporter": "dot" 45 | }, 46 | "scripts": { 47 | "prepublishOnly": "coffee -co lib src", 48 | "test": "nyc mocha \"test/**/*.coffee\"", 49 | "perf": "coffee ./perf/index.coffee" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /perf/basic/object.coffee: -------------------------------------------------------------------------------- 1 | perf 'Create simple object', 100000, (run) -> 2 | obj = 3 | ele: "simple element" 4 | person: 5 | name: "John" 6 | '@age': 35 7 | '?pi': 'mypi' 8 | '#comment': 'Good guy' 9 | '#cdata': 'well formed!' 10 | unescaped: 11 | '#raw': '&<>&' 12 | address: 13 | city: "Istanbul" 14 | street: "End of long and winding road" 15 | contact: 16 | phone: [ "555-1234", "555-1235" ] 17 | id: () -> return 42 18 | details: 19 | '#text': 'classified' 20 | 21 | run () -> xml(obj) 22 | -------------------------------------------------------------------------------- /perf/index.coffee: -------------------------------------------------------------------------------- 1 | builder = require('../src/index') 2 | git = require('git-state') 3 | fs = require('fs') 4 | path = require('path') 5 | { performance, PerformanceObserver } = require('perf_hooks') 6 | 7 | global.xml = builder.create 8 | global.doc = builder.begin 9 | 10 | global.perf = (description, count, func) -> 11 | 12 | totalTime = 0 13 | 14 | callback = (userFunction) -> 15 | startTime = performance.now() 16 | for i in [1..count] 17 | userFunction() 18 | endTime = performance.now() 19 | totalTime += endTime - startTime 20 | func(callback) 21 | 22 | averageTime = totalTime / count 23 | 24 | version = require('../package.json').version 25 | working = gitWorking(gitDir) 26 | if working then version = version + "*" 27 | if not perfObj[version] then perfObj[version] = { } 28 | 29 | perfObj[version][description] = averageTime.toFixed(4) 30 | 31 | readPerf = (filename) -> 32 | if not fs.existsSync(filename) then fs.closeSync(fs.openSync(filename, 'w')) 33 | str = fs.readFileSync(filename, 'utf8') 34 | if str then JSON.parse(str) else { } 35 | 36 | runPerf = (dirPath) -> 37 | for file from walkDir(dirPath) 38 | filename = path.basename(file) 39 | if filename is "index.coffee" or filename is "perf.list" then continue 40 | require(file) 41 | 42 | walkDir = (dirPath) -> 43 | for file in fs.readdirSync(dirPath) 44 | filePath = path.join(dirPath, file) 45 | stat = fs.statSync(filePath) 46 | if stat.isFile() then yield filePath else if stat.isDirectory() then yield from walkDir(filePath) 47 | return undefined 48 | 49 | gitWorking = (dirPath) -> 50 | return git.isGitSync(dirPath) and git.dirtySync(dirPath) 51 | 52 | printPerf = (perfObj) -> 53 | sorted = sortByVersion(perfObj) 54 | 55 | for sortedItems in sorted 56 | version = sortedItems.version 57 | items = sortedItems.item 58 | sortedItem = sortByDesc(items) 59 | 60 | if parseVersion(version)[3] 61 | console.log "\x1b[4mv%s (Working Tree):\x1b[0m", version 62 | else 63 | console.log "\x1b[4mv%s:\x1b[0m", version 64 | 65 | longestDescription = 0 66 | for item in sortedItem 67 | descriptionLength = item.description.length 68 | if descriptionLength > longestDescription 69 | longestDescription = descriptionLength 70 | 71 | for item in sortedItem 72 | description = item.description 73 | averageTime = item.averageTime 74 | prevItem = findPrevPerf(sorted, version, description) 75 | if prevItem 76 | if averageTime < prevItem.item[description] 77 | console.log " - \x1b[36m%s\x1b[0m \x1b[1m\x1b[32m%s\x1b[0m ms (v%s was \x1b[1m%s\x1b[0m ms, -\x1b[1m%s\x1b[0m%)", padRight(description, longestDescription), averageTime, prevItem.version, prevItem.item[description], (-100*(averageTime - prevItem.item[description]) / prevItem.item[description]).toFixed(0) 78 | else if averageTime > prevItem.item[description] 79 | console.log " - \x1b[36m%s\x1b[0m \x1b[1m\x1b[31m%s\x1b[0m ms (v%s was \x1b[1m%s\x1b[0m ms, +\x1b[1m%s\x1b[0m%)", padRight(description, longestDescription), averageTime, prevItem.version, prevItem.item[description], (100*(averageTime - prevItem.item[description]) / prevItem.item[description]).toFixed(0) 80 | else 81 | console.log " - \x1b[36m%s\x1b[0m \x1b[1m%s\x1b[0m ms (v%s was \x1b[1m%s\x1b[0m ms, \x1b[1m%s\x1b[0m%)", padRight(description, longestDescription), averageTime, prevItem.version, prevItem.item[description], (100*(averageTime - prevItem.item[description]) / prevItem.item[description]).toFixed(0) 82 | else 83 | console.log " - \x1b[36m%s\x1b[0m \x1b[1m%s\x1b[0m ms (no previous result)", padRight(description, longestDescription), averageTime 84 | 85 | padRight = (str, len) -> 86 | str + " ".repeat(len - str.length) 87 | 88 | writePerf = (filename, perfObj) -> 89 | writePerfObj = { } 90 | for version, items of perfObj 91 | if not parseVersion(version)[3] 92 | writePerfObj[version] = items 93 | fs.writeFileSync(filename, JSON.stringify(writePerfObj, null, 2) , 'utf-8') 94 | 95 | findPrevPerf = (sorted, version, description) -> 96 | prev = undefined 97 | for item in sorted 98 | if compareVersion(item.version, version) is -1 99 | if item.item[description] 100 | prev = item 101 | return prev 102 | 103 | sortByVersion = (perfObj) -> 104 | sorted = [] 105 | for version, items of perfObj 106 | sorted.push 107 | version: version 108 | item: items 109 | sorted.sort (item1, item2) -> 110 | compareVersion(item1.version, item2.version) 111 | 112 | sortByDesc = (item) -> 113 | sorted = [] 114 | for description, averageTime of item 115 | sorted.push 116 | description: description 117 | averageTime: averageTime 118 | sorted.sort (item1, item2) -> 119 | if item1.description < item2.description then -1 else 1 120 | 121 | parseVersion = (version) -> 122 | isDirty = version[version.length - 1] is "*" 123 | if isDirty then version = version.substr(0, version.length - 1) 124 | v = version.split('.') 125 | v.push(isDirty) 126 | return v 127 | 128 | compareVersion = (v1, v2) -> 129 | v1 = parseVersion(v1) 130 | v2 = parseVersion(v2) 131 | 132 | if v1[0] < v2[0] 133 | -1 134 | else if v1[0] > v2[0] 135 | 1 136 | else # v1[0] = v2[0] 137 | if v1[1] < v2[1] 138 | -1 139 | else if v1[1] > v2[1] 140 | 1 141 | else # v1[1] = v2[1] 142 | if v1[2] < v2[2] 143 | -1 144 | else if v1[2] > v2[2] 145 | 1 146 | else # v1[2] = v2[2] 147 | if v1[3] and not v2[3] 148 | 1 149 | else if v2[3] and not v1[3] 150 | -1 151 | else 152 | 0 153 | 154 | 155 | perfDir = __dirname 156 | gitDir = path.resolve(__dirname, '..') 157 | perfFile = path.join(perfDir, './perf.list') 158 | perfObj = readPerf(perfFile) 159 | runPerf(perfDir) 160 | printPerf(perfObj) 161 | writePerf(perfFile, perfObj) 162 | -------------------------------------------------------------------------------- /perf/perf.list: -------------------------------------------------------------------------------- 1 | { 2 | "13.0.2": { 3 | "Attribute value escaping": "0.0058", 4 | "Attribute value escaping (long value)": "0.0159", 5 | "Attribute value escaping (no replacement)": "0.0002", 6 | "Create simple object": "0.0218", 7 | "Text escaping": "0.0061", 8 | "Text escaping (long text)": "0.0046", 9 | "Text escaping (no replacement)": "0.0002" 10 | } 11 | } -------------------------------------------------------------------------------- /src/Derivation.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Restriction : 1 3 | Extension : 2 4 | Union : 4 5 | List : 8 6 | -------------------------------------------------------------------------------- /src/DocumentPosition.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Disconnected : 1 3 | Preceding : 2 4 | Following : 4 5 | Contains : 8 6 | ContainedBy : 16 7 | ImplementationSpecific : 32 8 | -------------------------------------------------------------------------------- /src/NodeType.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Element : 1 3 | Attribute : 2 4 | Text : 3 5 | CData : 4 6 | EntityReference : 5 7 | EntityDeclaration : 6 8 | ProcessingInstruction : 7 9 | Comment : 8 10 | Document : 9 11 | DocType : 10 12 | DocumentFragment : 11 13 | NotationDeclaration : 12 14 | # Numeric codes up to 200 are reserved to W3C for possible future use. 15 | # Following are types internal to this library: 16 | Declaration : 201 17 | Raw : 202 18 | AttributeDeclaration : 203 19 | ElementDeclaration : 204 20 | Dummy : 205 21 | -------------------------------------------------------------------------------- /src/OperationType.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | Clones : 1 3 | Imported : 2 4 | Deleted : 3 5 | Renamed : 4 6 | Adopted : 5 7 | -------------------------------------------------------------------------------- /src/Utility.coffee: -------------------------------------------------------------------------------- 1 | # Copies all enumerable own properties from `sources` to `target` 2 | assign = (target, sources...) -> 3 | if isFunction Object.assign 4 | Object.assign.apply null, arguments 5 | else 6 | for source in sources when source? 7 | target[key] = source[key] for own key of source 8 | 9 | return target 10 | 11 | # Determines if `val` is a Function object 12 | isFunction = (val) -> 13 | !!val and Object.prototype.toString.call(val) is '[object Function]' 14 | 15 | # Determines if `val` is an Object 16 | isObject = (val) -> 17 | !!val and typeof val in ['function', 'object'] 18 | 19 | # Determines if `val` is an Array 20 | isArray = (val) -> 21 | if isFunction Array.isArray 22 | Array.isArray val 23 | else 24 | Object.prototype.toString.call(val) is '[object Array]' 25 | 26 | # Determines if `val` is an empty Array or an Object with no own properties 27 | isEmpty = (val) -> 28 | if isArray val 29 | return !val.length 30 | else 31 | for own key of val 32 | return false 33 | return true 34 | 35 | # Determines if `val` is a plain Object 36 | isPlainObject = (val) -> 37 | isObject(val) and 38 | (proto = Object.getPrototypeOf val) and 39 | (ctor = proto.constructor) and 40 | (typeof ctor is 'function') and 41 | (ctor instanceof ctor) and 42 | (Function.prototype.toString.call(ctor) is Function.prototype.toString.call(Object)) 43 | 44 | # Gets the primitive value of an object 45 | getValue = (obj) -> 46 | if isFunction obj.valueOf 47 | obj.valueOf() 48 | else 49 | obj 50 | 51 | module.exports.assign = assign 52 | module.exports.isFunction = isFunction 53 | module.exports.isObject = isObject 54 | module.exports.isArray = isArray 55 | module.exports.isEmpty = isEmpty 56 | module.exports.isPlainObject = isPlainObject 57 | module.exports.getValue = getValue 58 | 59 | -------------------------------------------------------------------------------- /src/WriterState.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | None : 0 3 | OpenTag : 1 4 | InsideTag : 2 5 | CloseTag : 3 -------------------------------------------------------------------------------- /src/XMLAttribute.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLNode = require './XMLNode' 3 | 4 | # Represents an attribute 5 | module.exports = class XMLAttribute 6 | 7 | 8 | # Initializes a new instance of `XMLAttribute` 9 | # 10 | # `parent` the parent node 11 | # `name` attribute target 12 | # `value` attribute value 13 | constructor: (@parent, name, value) -> 14 | if @parent 15 | @options = @parent.options 16 | @stringify = @parent.stringify 17 | 18 | if not name? 19 | throw new Error "Missing attribute name. " + @debugInfo(name) 20 | 21 | @name = @stringify.name name 22 | @value = @stringify.attValue value 23 | @type = NodeType.Attribute 24 | # DOM level 3 25 | @isId = false 26 | @schemaTypeInfo = null 27 | 28 | 29 | # DOM level 1 30 | Object.defineProperty @::, 'nodeType', get: () -> @type 31 | Object.defineProperty @::, 'ownerElement', get: () -> @parent 32 | 33 | 34 | # DOM level 3 35 | Object.defineProperty @::, 'textContent', 36 | get: () -> @value 37 | set: (value) -> @value = value or '' 38 | 39 | 40 | # DOM level 4 41 | Object.defineProperty @::, 'namespaceURI', get: () -> '' 42 | Object.defineProperty @::, 'prefix', get: () -> '' 43 | Object.defineProperty @::, 'localName', get: () -> @name 44 | Object.defineProperty @::, 'specified', get: () -> true 45 | 46 | 47 | # Creates and returns a deep clone of `this` 48 | clone: () -> 49 | Object.create @ 50 | 51 | 52 | # Converts the XML fragment to string 53 | # 54 | # `options.pretty` pretty prints the result 55 | # `options.indent` indentation for pretty print 56 | # `options.offset` how many indentations to add to every line for pretty print 57 | # `options.newline` newline sequence for pretty print 58 | toString: (options) -> 59 | @options.writer.attribute @, @options.writer.filterOptions(options) 60 | 61 | 62 | # Returns debug string for this node 63 | debugInfo: (name) -> 64 | name = name or @name 65 | 66 | if not name? 67 | "parent: <" + @parent.name + ">" 68 | else 69 | "attribute: {" + name + "}, parent: <" + @parent.name + ">" 70 | 71 | 72 | isEqualNode: (node) -> 73 | if node.namespaceURI isnt @namespaceURI then return false 74 | if node.prefix isnt @prefix then return false 75 | if node.localName isnt @localName then return false 76 | if node.value isnt @value then return false 77 | 78 | return true -------------------------------------------------------------------------------- /src/XMLCData.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLCharacterData = require './XMLCharacterData' 3 | 4 | # Represents a CDATA node 5 | module.exports = class XMLCData extends XMLCharacterData 6 | 7 | 8 | # Initializes a new instance of `XMLCData` 9 | # 10 | # `text` CDATA text 11 | constructor: (parent, text) -> 12 | super parent 13 | 14 | if not text? 15 | throw new Error "Missing CDATA text. " + @debugInfo() 16 | 17 | @name = "#cdata-section" 18 | @type = NodeType.CData 19 | @value = @stringify.cdata text 20 | 21 | 22 | # Creates and returns a deep clone of `this` 23 | clone: () -> 24 | Object.create @ 25 | 26 | 27 | # Converts the XML fragment to string 28 | # 29 | # `options.pretty` pretty prints the result 30 | # `options.indent` indentation for pretty print 31 | # `options.offset` how many indentations to add to every line for pretty print 32 | # `options.newline` newline sequence for pretty print 33 | toString: (options) -> 34 | @options.writer.cdata @, @options.writer.filterOptions(options) 35 | 36 | -------------------------------------------------------------------------------- /src/XMLCharacterData.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | 3 | # Represents a character data node 4 | module.exports = class XMLCharacterData extends XMLNode 5 | 6 | 7 | # Initializes a new instance of `XMLCharacterData` 8 | # 9 | constructor: (parent) -> 10 | super parent 11 | 12 | @value = '' 13 | 14 | # DOM level 1 15 | Object.defineProperty @::, 'data', 16 | get: () -> @value 17 | set: (value) -> @value = value or '' 18 | Object.defineProperty @::, 'length', get: () -> @value.length 19 | 20 | # DOM level 3 21 | Object.defineProperty @::, 'textContent', 22 | get: () -> @value 23 | set: (value) -> @value = value or '' 24 | 25 | 26 | # Creates and returns a deep clone of `this` 27 | clone: () -> 28 | Object.create @ 29 | 30 | 31 | # DOM level 1 functions to be implemented later 32 | substringData: (offset, count) -> throw new Error "This DOM method is not implemented." + @debugInfo() 33 | appendData: (arg) -> throw new Error "This DOM method is not implemented." + @debugInfo() 34 | insertData: (offset, arg) -> throw new Error "This DOM method is not implemented." + @debugInfo() 35 | deleteData: (offset, count) -> throw new Error "This DOM method is not implemented." + @debugInfo() 36 | replaceData: (offset, count, arg) -> throw new Error "This DOM method is not implemented." + @debugInfo() 37 | 38 | 39 | isEqualNode: (node) -> 40 | if not super.isEqualNode(node) then return false 41 | if node.data isnt @data then return false 42 | 43 | return true -------------------------------------------------------------------------------- /src/XMLComment.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLCharacterData = require './XMLCharacterData' 3 | 4 | # Represents a comment node 5 | module.exports = class XMLComment extends XMLCharacterData 6 | 7 | 8 | # Initializes a new instance of `XMLComment` 9 | # 10 | # `text` comment text 11 | constructor: (parent, text) -> 12 | super parent 13 | 14 | if not text? 15 | throw new Error "Missing comment text. " + @debugInfo() 16 | 17 | @name = "#comment" 18 | @type = NodeType.Comment 19 | @value = @stringify.comment text 20 | 21 | 22 | # Creates and returns a deep clone of `this` 23 | clone: () -> 24 | Object.create @ 25 | 26 | 27 | # Converts the XML fragment to string 28 | # 29 | # `options.pretty` pretty prints the result 30 | # `options.indent` indentation for pretty print 31 | # `options.offset` how many indentations to add to every line for pretty print 32 | # `options.newline` newline sequence for pretty print 33 | toString: (options) -> 34 | @options.writer.comment @, @options.writer.filterOptions(options) 35 | -------------------------------------------------------------------------------- /src/XMLDOMConfiguration.coffee: -------------------------------------------------------------------------------- 1 | XMLDOMErrorHandler = require './XMLDOMErrorHandler' 2 | XMLDOMStringList = require './XMLDOMStringList' 3 | 4 | # Implements the DOMConfiguration interface 5 | module.exports = class XMLDOMConfiguration 6 | 7 | constructor: () -> 8 | @defaultParams = 9 | "canonical-form": false 10 | "cdata-sections": false 11 | "comments": false 12 | "datatype-normalization": false 13 | "element-content-whitespace": true 14 | "entities": true 15 | "error-handler": new XMLDOMErrorHandler() 16 | "infoset": true 17 | "validate-if-schema": false 18 | "namespaces": true 19 | "namespace-declarations": true 20 | "normalize-characters": false 21 | "schema-location": '' 22 | "schema-type": '' 23 | "split-cdata-sections": true 24 | "validate": false 25 | "well-formed": true 26 | 27 | @params = clonedSelf = Object.create @defaultParams 28 | 29 | 30 | # Returns the list of parameter names 31 | Object.defineProperty @::, 'parameterNames', get: () -> 32 | new XMLDOMStringList(Object.keys(@defaultParams)) 33 | 34 | 35 | # Gets the value of a parameter. 36 | # 37 | # `name` name of the parameter 38 | getParameter: (name) -> 39 | if @params.hasOwnProperty(name) 40 | return @params[name] 41 | else 42 | return null 43 | 44 | 45 | # Checks if setting a parameter to a specific value is supported. 46 | # 47 | # `name` name of the parameter 48 | # `value` parameter value 49 | canSetParameter: (name, value) -> true 50 | 51 | 52 | # Sets the value of a parameter. 53 | # 54 | # `name` name of the parameter 55 | # `value` new value or null if the user wishes to unset the parameter 56 | setParameter: (name, value) -> 57 | if value? 58 | @params[name] = value 59 | else 60 | delete @params[name] 61 | -------------------------------------------------------------------------------- /src/XMLDOMErrorHandler.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Represents the error handler for DOM operations 3 | module.exports = class XMLDOMErrorHandler 4 | 5 | 6 | # Initializes a new instance of `XMLDOMErrorHandler` 7 | # 8 | constructor: () -> 9 | 10 | # Called on the error handler when an error occurs. 11 | # 12 | # `error` the error message as a string 13 | handleError: (error) -> 14 | throw new Error(error) 15 | -------------------------------------------------------------------------------- /src/XMLDOMImplementation.coffee: -------------------------------------------------------------------------------- 1 | # Implements the DOMImplementation interface 2 | module.exports = class XMLDOMImplementation 3 | 4 | 5 | # Tests if the DOM implementation implements a specific feature. 6 | # 7 | # `feature` package name of the feature to test. In Level 1, the 8 | # legal values are "HTML" and "XML" (case-insensitive). 9 | # `version` version number of the package name to test. 10 | # In Level 1, this is the string "1.0". If the version is 11 | # not specified, supporting any version of the feature will 12 | # cause the method to return true. 13 | hasFeature: (feature, version) -> 14 | return true 15 | 16 | 17 | # Creates a new document type declaration. 18 | # 19 | # `qualifiedName` qualified name of the document type to be created 20 | # `publicId` public identifier of the external subset 21 | # `systemId` system identifier of the external subset 22 | createDocumentType: (qualifiedName, publicId, systemId) -> 23 | throw new Error "This DOM method is not implemented." 24 | 25 | 26 | # Creates a new document. 27 | # 28 | # `namespaceURI` namespace URI of the document element to create 29 | # `qualifiedName` the qualified name of the document to be created 30 | # `doctype` the type of document to be created or null 31 | createDocument: (namespaceURI, qualifiedName, doctype) -> 32 | throw new Error "This DOM method is not implemented." 33 | 34 | 35 | # Creates a new HTML document. 36 | # 37 | # `title` document title 38 | createHTMLDocument: (title) -> 39 | throw new Error "This DOM method is not implemented." 40 | 41 | 42 | # Returns a specialized object which implements the specialized APIs 43 | # of the specified feature and version. 44 | # 45 | # `feature` name of the feature requested. 46 | # `version` version number of the feature to test 47 | getFeature: (feature, version) -> 48 | throw new Error "This DOM method is not implemented." 49 | -------------------------------------------------------------------------------- /src/XMLDOMStringList.coffee: -------------------------------------------------------------------------------- 1 | # Represents a list of string entries 2 | module.exports = class XMLDOMStringList 3 | 4 | 5 | # Initializes a new instance of `XMLDOMStringList` 6 | # This is just a wrapper around an ordinary 7 | # JS array. 8 | # 9 | # `arr` the array of string values 10 | constructor: (arr) -> 11 | @arr = arr or [] 12 | 13 | # Returns the number of strings in the list. 14 | Object.defineProperty @::, 'length', get: () -> @arr.length 15 | 16 | # Returns the indexth item in the collection. 17 | # 18 | # `index` index into the collection 19 | item: (index) -> @arr[index] or null 20 | 21 | # Test if a string is part of this DOMStringList. 22 | # 23 | # `str` the string to look for 24 | contains: (str) -> @arr.indexOf(str) isnt -1 25 | -------------------------------------------------------------------------------- /src/XMLDTDAttList.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | NodeType = require './NodeType' 3 | 4 | # Represents an attribute list 5 | module.exports = class XMLDTDAttList extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLDTDAttList` 9 | # 10 | # `parent` the parent `XMLDocType` element 11 | # `elementName` the name of the element containing this attribute 12 | # `attributeName` attribute name 13 | # `attributeType` type of the attribute 14 | # `defaultValueType` default value type (either #REQUIRED, #IMPLIED, 15 | # #FIXED or #DEFAULT) 16 | # `defaultValue` default value of the attribute 17 | # (only used for #FIXED or #DEFAULT) 18 | constructor: (parent, elementName, attributeName, attributeType, defaultValueType, defaultValue) -> 19 | super parent 20 | 21 | if not elementName? 22 | throw new Error "Missing DTD element name. " + @debugInfo() 23 | if not attributeName? 24 | throw new Error "Missing DTD attribute name. " + @debugInfo(elementName) 25 | if not attributeType 26 | throw new Error "Missing DTD attribute type. " + @debugInfo(elementName) 27 | if not defaultValueType 28 | throw new Error "Missing DTD attribute default. " + @debugInfo(elementName) 29 | if defaultValueType.indexOf('#') != 0 30 | defaultValueType = '#' + defaultValueType 31 | if not defaultValueType.match /^(#REQUIRED|#IMPLIED|#FIXED|#DEFAULT)$/ 32 | throw new Error "Invalid default value type; expected: #REQUIRED, #IMPLIED, #FIXED or #DEFAULT. " + @debugInfo(elementName) 33 | if defaultValue and not defaultValueType.match /^(#FIXED|#DEFAULT)$/ 34 | throw new Error "Default value only applies to #FIXED or #DEFAULT. " + @debugInfo(elementName) 35 | 36 | @elementName = @stringify.name elementName 37 | @type = NodeType.AttributeDeclaration 38 | @attributeName = @stringify.name attributeName 39 | @attributeType = @stringify.dtdAttType attributeType 40 | @defaultValue = @stringify.dtdAttDefault defaultValue if defaultValue 41 | @defaultValueType = defaultValueType 42 | 43 | 44 | # Converts the XML fragment to string 45 | # 46 | # `options.pretty` pretty prints the result 47 | # `options.indent` indentation for pretty print 48 | # `options.offset` how many indentations to add to every line for pretty print 49 | # `options.newline` newline sequence for pretty print 50 | toString: (options) -> 51 | @options.writer.dtdAttList @, @options.writer.filterOptions(options) 52 | -------------------------------------------------------------------------------- /src/XMLDTDElement.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | NodeType = require './NodeType' 3 | 4 | # Represents an attribute 5 | module.exports = class XMLDTDElement extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLDTDElement` 9 | # 10 | # `parent` the parent `XMLDocType` element 11 | # `name` element name 12 | # `value` element content (defaults to #PCDATA) 13 | constructor: (parent, name, value) -> 14 | super parent 15 | 16 | if not name? 17 | throw new Error "Missing DTD element name. " + @debugInfo() 18 | if not value 19 | value = '(#PCDATA)' 20 | if Array.isArray value 21 | value = '(' + value.join(',') + ')' 22 | 23 | @name = @stringify.name name 24 | @type = NodeType.ElementDeclaration 25 | @value = @stringify.dtdElementValue value 26 | 27 | 28 | # Converts the XML fragment to string 29 | # 30 | # `options.pretty` pretty prints the result 31 | # `options.indent` indentation for pretty print 32 | # `options.offset` how many indentations to add to every line for pretty print 33 | # `options.newline` newline sequence for pretty print 34 | toString: (options) -> 35 | @options.writer.dtdElement @, @options.writer.filterOptions(options) 36 | -------------------------------------------------------------------------------- /src/XMLDTDEntity.coffee: -------------------------------------------------------------------------------- 1 | { isObject } = require './Utility' 2 | 3 | XMLNode = require './XMLNode' 4 | NodeType = require './NodeType' 5 | 6 | # Represents an entity declaration in the DTD 7 | module.exports = class XMLDTDEntity extends XMLNode 8 | 9 | 10 | # Initializes a new instance of `XMLDTDEntity` 11 | # 12 | # `parent` the parent `XMLDocType` element 13 | # `pe` whether this is a parameter entity or a general entity 14 | # defaults to `false` (general entity) 15 | # `name` the name of the entity 16 | # `value` internal entity value or an object with external entity details 17 | # `value.pubID` public identifier 18 | # `value.sysID` system identifier 19 | # `value.nData` notation declaration 20 | constructor: (parent, pe, name, value) -> 21 | super parent 22 | 23 | if not name? 24 | throw new Error "Missing DTD entity name. " + @debugInfo(name) 25 | if not value? 26 | throw new Error "Missing DTD entity value. " + @debugInfo(name) 27 | 28 | @pe = !!pe 29 | @name = @stringify.name name 30 | @type = NodeType.EntityDeclaration 31 | 32 | if not isObject value 33 | @value = @stringify.dtdEntityValue value 34 | @internal = true 35 | else 36 | if not value.pubID and not value.sysID 37 | throw new Error "Public and/or system identifiers are required for an external entity. " + @debugInfo(name) 38 | if value.pubID and not value.sysID 39 | throw new Error "System identifier is required for a public external entity. " + @debugInfo(name) 40 | 41 | @internal = false 42 | @pubID = @stringify.dtdPubID value.pubID if value.pubID? 43 | @sysID = @stringify.dtdSysID value.sysID if value.sysID? 44 | 45 | @nData = @stringify.dtdNData value.nData if value.nData? 46 | if @pe and @nData 47 | throw new Error "Notation declaration is not allowed in a parameter entity. " + @debugInfo(name) 48 | 49 | # DOM level 1 50 | Object.defineProperty @::, 'publicId', get: () -> @pubID 51 | Object.defineProperty @::, 'systemId', get: () -> @sysID 52 | Object.defineProperty @::, 'notationName', get: () -> @nData or null 53 | 54 | # DOM level 3 55 | Object.defineProperty @::, 'inputEncoding', get: () -> null 56 | Object.defineProperty @::, 'xmlEncoding', get: () -> null 57 | Object.defineProperty @::, 'xmlVersion', get: () -> null 58 | 59 | # Converts the XML fragment to string 60 | # 61 | # `options.pretty` pretty prints the result 62 | # `options.indent` indentation for pretty print 63 | # `options.offset` how many indentations to add to every line for pretty print 64 | # `options.newline` newline sequence for pretty print 65 | toString: (options) -> 66 | @options.writer.dtdEntity @, @options.writer.filterOptions(options) 67 | -------------------------------------------------------------------------------- /src/XMLDTDNotation.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | NodeType = require './NodeType' 3 | 4 | # Represents a NOTATION entry in the DTD 5 | module.exports = class XMLDTDNotation extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLDTDNotation` 9 | # 10 | # `parent` the parent `XMLDocType` element 11 | # `name` the name of the notation 12 | # `value` an object with external entity details 13 | # `value.pubID` public identifier 14 | # `value.sysID` system identifier 15 | constructor: (parent, name, value) -> 16 | super parent 17 | 18 | if not name? 19 | throw new Error "Missing DTD notation name. " + @debugInfo(name) 20 | if not value.pubID and not value.sysID 21 | throw new Error "Public or system identifiers are required for an external entity. " + @debugInfo(name) 22 | 23 | @name = @stringify.name name 24 | @type = NodeType.NotationDeclaration 25 | @pubID = @stringify.dtdPubID value.pubID if value.pubID? 26 | @sysID = @stringify.dtdSysID value.sysID if value.sysID? 27 | 28 | # DOM level 1 29 | Object.defineProperty @::, 'publicId', get: () -> @pubID 30 | Object.defineProperty @::, 'systemId', get: () -> @sysID 31 | 32 | 33 | # Converts the XML fragment to string 34 | # 35 | # `options.pretty` pretty prints the result 36 | # `options.indent` indentation for pretty print 37 | # `options.offset` how many indentations to add to every line for pretty print 38 | # `options.newline` newline sequence for pretty print 39 | toString: (options) -> 40 | @options.writer.dtdNotation @, @options.writer.filterOptions(options) 41 | -------------------------------------------------------------------------------- /src/XMLDeclaration.coffee: -------------------------------------------------------------------------------- 1 | { isObject } = require './Utility' 2 | 3 | XMLNode = require './XMLNode' 4 | NodeType = require './NodeType' 5 | 6 | # Represents the XML declaration 7 | module.exports = class XMLDeclaration extends XMLNode 8 | 9 | 10 | # Initializes a new instance of `XMLDeclaration` 11 | # 12 | # `parent` the document object 13 | # 14 | # `version` A version number string, e.g. 1.0 15 | # `encoding` Encoding declaration, e.g. UTF-8 16 | # `standalone` standalone document declaration: true or false 17 | constructor: (parent, version, encoding, standalone) -> 18 | super parent 19 | 20 | # arguments may also be passed as an object 21 | if isObject version 22 | { version, encoding, standalone } = version 23 | 24 | version = '1.0' if not version 25 | 26 | @type = NodeType.Declaration 27 | @version = @stringify.xmlVersion version 28 | @encoding = @stringify.xmlEncoding encoding if encoding? 29 | @standalone = @stringify.xmlStandalone standalone if standalone? 30 | 31 | 32 | # Converts to string 33 | # 34 | # `options.pretty` pretty prints the result 35 | # `options.indent` indentation for pretty print 36 | # `options.offset` how many indentations to add to every line for pretty print 37 | # `options.newline` newline sequence for pretty print 38 | toString: (options) -> 39 | @options.writer.declaration @, @options.writer.filterOptions(options) 40 | -------------------------------------------------------------------------------- /src/XMLDocType.coffee: -------------------------------------------------------------------------------- 1 | { isObject } = require './Utility' 2 | 3 | XMLNode = require './XMLNode' 4 | NodeType = require './NodeType' 5 | XMLDTDAttList = require './XMLDTDAttList' 6 | XMLDTDEntity = require './XMLDTDEntity' 7 | XMLDTDElement = require './XMLDTDElement' 8 | XMLDTDNotation = require './XMLDTDNotation' 9 | XMLNamedNodeMap = require './XMLNamedNodeMap' 10 | 11 | # Represents doctype declaration 12 | module.exports = class XMLDocType extends XMLNode 13 | 14 | 15 | # Initializes a new instance of `XMLDocType` 16 | # 17 | # `parent` the document object 18 | # 19 | # `pubID` public identifier of the external subset 20 | # `sysID` system identifier of the external subset 21 | constructor: (parent, pubID, sysID) -> 22 | super parent 23 | 24 | @type = NodeType.DocType 25 | # set DTD name to the name of the root node 26 | if parent.children 27 | for child in parent.children 28 | if child.type is NodeType.Element 29 | @name = child.name 30 | break 31 | 32 | @documentObject = parent 33 | 34 | # arguments may also be passed as an object 35 | if isObject pubID 36 | { pubID, sysID } = pubID 37 | 38 | if not sysID? 39 | [sysID, pubID] = [pubID, sysID] 40 | 41 | @pubID = @stringify.dtdPubID pubID if pubID? 42 | @sysID = @stringify.dtdSysID sysID if sysID? 43 | 44 | # DOM level 1 45 | Object.defineProperty @::, 'entities', get: () -> 46 | nodes = {} 47 | nodes[child.name] = child for child in @children when (child.type is NodeType.EntityDeclaration) and not child.pe 48 | new XMLNamedNodeMap nodes 49 | Object.defineProperty @::, 'notations', get: () -> 50 | nodes = {} 51 | nodes[child.name] = child for child in @children when child.type is NodeType.NotationDeclaration 52 | new XMLNamedNodeMap nodes 53 | 54 | # DOM level 2 55 | Object.defineProperty @::, 'publicId', get: () -> @pubID 56 | Object.defineProperty @::, 'systemId', get: () -> @sysID 57 | Object.defineProperty @::, 'internalSubset', get: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 58 | 59 | 60 | # Creates an element type declaration 61 | # 62 | # `name` element name 63 | # `value` element content (defaults to #PCDATA) 64 | element: (name, value) -> 65 | child = new XMLDTDElement @, name, value 66 | @children.push child 67 | return @ 68 | 69 | 70 | # Creates an attribute declaration 71 | # 72 | # `elementName` the name of the element containing this attribute 73 | # `attributeName` attribute name 74 | # `attributeType` type of the attribute (defaults to CDATA) 75 | # `defaultValueType` default value type (either #REQUIRED, #IMPLIED, #FIXED or 76 | # #DEFAULT) (defaults to #IMPLIED) 77 | # `defaultValue` default value of the attribute 78 | # (only used for #FIXED or #DEFAULT) 79 | attList: (elementName, attributeName, attributeType, defaultValueType, defaultValue) -> 80 | child = new XMLDTDAttList @, elementName, attributeName, attributeType, defaultValueType, defaultValue 81 | @children.push child 82 | return @ 83 | 84 | 85 | # Creates a general entity declaration 86 | # 87 | # `name` the name of the entity 88 | # `value` internal entity value or an object with external entity details 89 | # `value.pubID` public identifier 90 | # `value.sysID` system identifier 91 | # `value.nData` notation declaration 92 | entity: (name, value) -> 93 | child = new XMLDTDEntity @, false, name, value 94 | @children.push child 95 | return @ 96 | 97 | 98 | # Creates a parameter entity declaration 99 | # 100 | # `name` the name of the entity 101 | # `value` internal entity value or an object with external entity details 102 | # `value.pubID` public identifier 103 | # `value.sysID` system identifier 104 | pEntity: (name, value) -> 105 | child = new XMLDTDEntity @, true, name, value 106 | @children.push child 107 | return @ 108 | 109 | 110 | # Creates a NOTATION declaration 111 | # 112 | # `name` the name of the notation 113 | # `value` an object with external entity details 114 | # `value.pubID` public identifier 115 | # `value.sysID` system identifier 116 | notation: (name, value) -> 117 | child = new XMLDTDNotation @, name, value 118 | @children.push child 119 | return @ 120 | 121 | 122 | # Converts to string 123 | # 124 | # `options.pretty` pretty prints the result 125 | # `options.indent` indentation for pretty print 126 | # `options.offset` how many indentations to add to every line for pretty print 127 | # `options.newline` newline sequence for pretty print 128 | toString: (options) -> 129 | @options.writer.docType @, @options.writer.filterOptions(options) 130 | 131 | 132 | # Aliases 133 | ele: (name, value) -> @element name, value 134 | att: (elementName, attributeName, attributeType, defaultValueType, defaultValue) -> 135 | @attList elementName, attributeName, attributeType, defaultValueType, defaultValue 136 | ent: (name, value) -> @entity name, value 137 | pent: (name, value) -> @pEntity name, value 138 | not: (name, value) -> @notation name, value 139 | up: () -> @root() or @documentObject 140 | 141 | 142 | isEqualNode: (node) -> 143 | if not super.isEqualNode(node) then return false 144 | if node.name isnt @name then return false 145 | if node.publicId isnt @publicId then return false 146 | if node.systemId isnt @systemId then return false 147 | return true -------------------------------------------------------------------------------- /src/XMLDocument.coffee: -------------------------------------------------------------------------------- 1 | { isPlainObject } = require './Utility' 2 | 3 | XMLDOMImplementation = require './XMLDOMImplementation' 4 | XMLDOMConfiguration = require './XMLDOMConfiguration' 5 | XMLNode = require './XMLNode' 6 | NodeType = require './NodeType' 7 | XMLStringifier = require './XMLStringifier' 8 | XMLStringWriter = require './XMLStringWriter' 9 | 10 | # Represents an XML builder 11 | module.exports = class XMLDocument extends XMLNode 12 | 13 | 14 | # Initializes a new instance of `XMLDocument` 15 | # 16 | # `options.keepNullNodes` whether nodes with null values will be kept 17 | # or ignored: true or false 18 | # `options.keepNullAttributes` whether attributes with null values will be 19 | # kept or ignored: true or false 20 | # `options.ignoreDecorators` whether decorator strings will be ignored when 21 | # converting JS objects: true or false 22 | # `options.separateArrayItems` whether array items are created as separate 23 | # nodes when passed as an object value: true or false 24 | # `options.noDoubleEncoding` whether existing html entities are encoded: 25 | # true or false 26 | # `options.stringify` a set of functions to use for converting values to 27 | # strings 28 | # `options.writer` the default XML writer to use for converting nodes to 29 | # string. If the default writer is not set, the built-in XMLStringWriter 30 | # will be used instead. 31 | constructor: (options) -> 32 | super null 33 | 34 | @name = "#document" 35 | @type = NodeType.Document 36 | @documentURI = null 37 | @domConfig = new XMLDOMConfiguration() 38 | 39 | options or= {} 40 | if not options.writer then options.writer = new XMLStringWriter() 41 | 42 | @options = options 43 | @stringify = new XMLStringifier options 44 | 45 | # DOM level 1 46 | Object.defineProperty @::, 'implementation', value: new XMLDOMImplementation() 47 | Object.defineProperty @::, 'doctype', get: () -> 48 | for child in @children 49 | if child.type is NodeType.DocType 50 | return child 51 | return null 52 | Object.defineProperty @::, 'documentElement', get: () -> @rootObject or null 53 | 54 | 55 | # DOM level 3 56 | Object.defineProperty @::, 'inputEncoding', get: () -> null 57 | Object.defineProperty @::, 'strictErrorChecking', get: () -> false 58 | Object.defineProperty @::, 'xmlEncoding', get: () -> 59 | if @children.length isnt 0 and @children[0].type is NodeType.Declaration 60 | return @children[0].encoding 61 | else 62 | return null 63 | Object.defineProperty @::, 'xmlStandalone', get: () -> 64 | if @children.length isnt 0 and @children[0].type is NodeType.Declaration 65 | return @children[0].standalone is 'yes' 66 | else 67 | return false 68 | Object.defineProperty @::, 'xmlVersion', get: () -> 69 | if @children.length isnt 0 and @children[0].type is NodeType.Declaration 70 | return @children[0].version 71 | else 72 | return "1.0" 73 | 74 | 75 | # DOM level 4 76 | Object.defineProperty @::, 'URL', get: () -> @documentURI 77 | Object.defineProperty @::, 'origin', get: () -> null 78 | Object.defineProperty @::, 'compatMode', get: () -> null 79 | Object.defineProperty @::, 'characterSet', get: () -> null 80 | Object.defineProperty @::, 'contentType', get: () -> null 81 | 82 | 83 | # Ends the document and passes it to the given XML writer 84 | # 85 | # `writer` is either an XML writer or a plain object to pass to the 86 | # constructor of the default XML writer. The default writer is assigned when 87 | # creating the XML document. Following flags are recognized by the 88 | # built-in XMLStringWriter: 89 | # `writer.pretty` pretty prints the result 90 | # `writer.indent` indentation for pretty print 91 | # `writer.offset` how many indentations to add to every line for pretty print 92 | # `writer.newline` newline sequence for pretty print 93 | end: (writer) -> 94 | writerOptions = {} 95 | 96 | if not writer 97 | writer = @options.writer 98 | else if isPlainObject(writer) 99 | writerOptions = writer 100 | writer = @options.writer 101 | 102 | writer.document @, writer.filterOptions(writerOptions) 103 | 104 | 105 | # Converts the XML document to string 106 | # 107 | # `options.pretty` pretty prints the result 108 | # `options.indent` indentation for pretty print 109 | # `options.offset` how many indentations to add to every line for pretty print 110 | # `options.newline` newline sequence for pretty print 111 | toString: (options) -> 112 | @options.writer.document @, @options.writer.filterOptions(options) 113 | 114 | # DOM level 1 functions to be implemented later 115 | createElement: (tagName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 116 | createDocumentFragment: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 117 | createTextNode: (data) -> throw new Error "This DOM method is not implemented." + @debugInfo() 118 | createComment: (data) -> throw new Error "This DOM method is not implemented." + @debugInfo() 119 | createCDATASection: (data) -> throw new Error "This DOM method is not implemented." + @debugInfo() 120 | createProcessingInstruction: (target, data) -> throw new Error "This DOM method is not implemented." + @debugInfo() 121 | createAttribute: (name) -> throw new Error "This DOM method is not implemented." + @debugInfo() 122 | createEntityReference: (name) -> throw new Error "This DOM method is not implemented." + @debugInfo() 123 | getElementsByTagName: (tagname) -> throw new Error "This DOM method is not implemented." + @debugInfo() 124 | 125 | # DOM level 2 functions to be implemented later 126 | importNode: (importedNode, deep) -> throw new Error "This DOM method is not implemented." + @debugInfo() 127 | createElementNS: (namespaceURI, qualifiedName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 128 | createAttributeNS: (namespaceURI, qualifiedName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 129 | getElementsByTagNameNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 130 | getElementById: (elementId) -> throw new Error "This DOM method is not implemented." + @debugInfo() 131 | 132 | # DOM level 3 functions to be implemented later 133 | adoptNode: (source) -> throw new Error "This DOM method is not implemented." + @debugInfo() 134 | normalizeDocument: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 135 | renameNode: (node, namespaceURI, qualifiedName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 136 | 137 | # DOM level 4 functions to be implemented later 138 | getElementsByClassName: (classNames) -> throw new Error "This DOM method is not implemented." + @debugInfo() 139 | createEvent: (eventInterface) -> throw new Error "This DOM method is not implemented." + @debugInfo() 140 | createRange: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 141 | createNodeIterator: (root, whatToShow, filter) -> throw new Error "This DOM method is not implemented." + @debugInfo() 142 | createTreeWalker: (root, whatToShow, filter) -> throw new Error "This DOM method is not implemented." + @debugInfo() -------------------------------------------------------------------------------- /src/XMLDocumentFragment.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | NodeType = require './NodeType' 3 | 4 | # Represents a CDATA node 5 | module.exports = class XMLDocumentFragment extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLDocumentFragment` 9 | # 10 | constructor: () -> 11 | super null 12 | 13 | @name = "#document-fragment" 14 | @type = NodeType.DocumentFragment 15 | -------------------------------------------------------------------------------- /src/XMLDummy.coffee: -------------------------------------------------------------------------------- 1 | XMLNode = require './XMLNode' 2 | NodeType = require './NodeType' 3 | 4 | # Represents a raw node 5 | module.exports = class XMLDummy extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLDummy` 9 | # 10 | # `XMLDummy` is a special node representing a node with 11 | # a null value. Dummy nodes are created while recursively 12 | # building the XML tree. Simply skipping null values doesn't 13 | # work because that would break the recursive chain. 14 | constructor: (parent) -> 15 | super parent 16 | 17 | @type = NodeType.Dummy 18 | 19 | 20 | # Creates and returns a deep clone of `this` 21 | clone: () -> 22 | Object.create @ 23 | 24 | 25 | # Converts the XML fragment to string 26 | # 27 | # `options.pretty` pretty prints the result 28 | # `options.indent` indentation for pretty print 29 | # `options.offset` how many indentations to add to every line for pretty print 30 | # `options.newline` newline sequence for pretty print 31 | toString: (options) -> 32 | '' 33 | 34 | -------------------------------------------------------------------------------- /src/XMLElement.coffee: -------------------------------------------------------------------------------- 1 | { isObject, isFunction, getValue } = require './Utility' 2 | 3 | XMLNode = require './XMLNode' 4 | NodeType = require './NodeType' 5 | XMLAttribute = require './XMLAttribute' 6 | XMLNamedNodeMap = require './XMLNamedNodeMap' 7 | 8 | # Represents an element of the XML document 9 | module.exports = class XMLElement extends XMLNode 10 | 11 | 12 | # Initializes a new instance of `XMLElement` 13 | # 14 | # `parent` the parent node 15 | # `name` element name 16 | # `attributes` an object containing name/value pairs of attributes 17 | constructor: (parent, name, attributes) -> 18 | super parent 19 | 20 | if not name? 21 | throw new Error "Missing element name. " + @debugInfo() 22 | 23 | @name = @stringify.name name 24 | @type = NodeType.Element 25 | @attribs = {} 26 | @schemaTypeInfo = null 27 | 28 | @attribute attributes if attributes? 29 | 30 | # set properties if this is the root node 31 | if parent.type is NodeType.Document 32 | @isRoot = true 33 | @documentObject = parent 34 | parent.rootObject = @ 35 | # set dtd name 36 | if parent.children 37 | for child in parent.children 38 | if child.type is NodeType.DocType 39 | child.name = @name 40 | break 41 | 42 | # DOM level 1 43 | Object.defineProperty @::, 'tagName', get: () -> @name 44 | 45 | 46 | # DOM level 4 47 | Object.defineProperty @::, 'namespaceURI', get: () -> '' 48 | Object.defineProperty @::, 'prefix', get: () -> '' 49 | Object.defineProperty @::, 'localName', get: () -> @name 50 | Object.defineProperty @::, 'id', get: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 51 | Object.defineProperty @::, 'className', get: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 52 | Object.defineProperty @::, 'classList', get: () -> throw new Error "This DOM method is not implemented." + @debugInfo() 53 | Object.defineProperty @::, 'attributes', get: () -> 54 | if not @attributeMap or not @attributeMap.nodes 55 | @attributeMap = new XMLNamedNodeMap @attribs 56 | return @attributeMap 57 | 58 | 59 | # Creates and returns a deep clone of `this` 60 | # 61 | clone: () -> 62 | clonedSelf = Object.create @ 63 | 64 | # remove document element 65 | if clonedSelf.isRoot 66 | clonedSelf.documentObject = null 67 | 68 | # clone attributes 69 | clonedSelf.attribs = {} 70 | for own attName, att of @attribs 71 | clonedSelf.attribs[attName] = att.clone() 72 | 73 | # clone child nodes 74 | clonedSelf.children = [] 75 | @children.forEach (child) -> 76 | clonedChild = child.clone() 77 | clonedChild.parent = clonedSelf 78 | clonedSelf.children.push clonedChild 79 | 80 | return clonedSelf 81 | 82 | 83 | # Adds or modifies an attribute 84 | # 85 | # `name` attribute name 86 | # `value` attribute value 87 | attribute: (name, value) -> 88 | name = getValue(name) if name? 89 | 90 | if isObject name # expand if object 91 | for own attName, attValue of name 92 | @attribute attName, attValue 93 | else 94 | value = value.apply() if isFunction value 95 | if @options.keepNullAttributes and not value? 96 | @attribs[name] = new XMLAttribute @, name, "" 97 | else if value? 98 | @attribs[name] = new XMLAttribute @, name, value 99 | 100 | return @ 101 | 102 | 103 | # Removes an attribute 104 | # 105 | # `name` attribute name 106 | removeAttribute: (name) -> 107 | # Also defined in DOM level 1 108 | # removeAttribute(name) removes an attribute by name. 109 | if not name? 110 | throw new Error "Missing attribute name. " + @debugInfo() 111 | name = getValue name 112 | 113 | if Array.isArray name # expand if array 114 | for attName in name 115 | delete @attribs[attName] 116 | else 117 | delete @attribs[name] 118 | 119 | return @ 120 | 121 | 122 | # Converts the XML fragment to string 123 | # 124 | # `options.pretty` pretty prints the result 125 | # `options.indent` indentation for pretty print 126 | # `options.offset` how many indentations to add to every line for pretty print 127 | # `options.newline` newline sequence for pretty print 128 | # `options.allowEmpty` do not self close empty element tags 129 | toString: (options) -> 130 | @options.writer.element @, @options.writer.filterOptions(options) 131 | 132 | 133 | # Aliases 134 | att: (name, value) -> @attribute name, value 135 | a: (name, value) -> @attribute name, value 136 | 137 | 138 | # DOM Level 1 139 | getAttribute: (name) -> if @attribs.hasOwnProperty(name) then @attribs[name].value else null 140 | setAttribute: (name, value) -> throw new Error "This DOM method is not implemented." + @debugInfo() 141 | getAttributeNode: (name) -> if @attribs.hasOwnProperty(name) then @attribs[name] else null 142 | setAttributeNode: (newAttr) -> throw new Error "This DOM method is not implemented." + @debugInfo() 143 | removeAttributeNode: (oldAttr) -> throw new Error "This DOM method is not implemented." + @debugInfo() 144 | getElementsByTagName: (name) -> throw new Error "This DOM method is not implemented." + @debugInfo() 145 | 146 | # DOM Level 2 147 | getAttributeNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 148 | setAttributeNS: (namespaceURI, qualifiedName, value) -> throw new Error "This DOM method is not implemented." + @debugInfo() 149 | removeAttributeNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 150 | getAttributeNodeNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 151 | setAttributeNodeNS: (newAttr) -> throw new Error "This DOM method is not implemented." + @debugInfo() 152 | getElementsByTagNameNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 153 | hasAttribute: (name) -> @attribs.hasOwnProperty(name) 154 | hasAttributeNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 155 | 156 | # DOM Level 3 157 | setIdAttribute: (name, isId) -> if @attribs.hasOwnProperty(name) then @attribs[name].isId else isId 158 | setIdAttributeNS: (namespaceURI, localName, isId) -> throw new Error "This DOM method is not implemented." + @debugInfo() 159 | setIdAttributeNode: (idAttr, isId) -> throw new Error "This DOM method is not implemented." + @debugInfo() 160 | 161 | 162 | # DOM Level 4 163 | getElementsByTagName: (tagname) -> throw new Error "This DOM method is not implemented." + @debugInfo() 164 | getElementsByTagNameNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." + @debugInfo() 165 | getElementsByClassName: (classNames) -> throw new Error "This DOM method is not implemented." + @debugInfo() 166 | 167 | 168 | isEqualNode: (node) -> 169 | if not super.isEqualNode(node) then return false 170 | if node.namespaceURI isnt @namespaceURI then return false 171 | if node.prefix isnt @prefix then return false 172 | if node.localName isnt @localName then return false 173 | 174 | if node.attribs.length isnt @attribs.length then return false 175 | for i in [0..@attribs.length - 1] 176 | if not @attribs[i].isEqualNode(node.attribs[i]) then return false 177 | 178 | return true -------------------------------------------------------------------------------- /src/XMLNamedNodeMap.coffee: -------------------------------------------------------------------------------- 1 | # Represents a map of nodes accessed by a string key 2 | module.exports = class XMLNamedNodeMap 3 | 4 | 5 | # Initializes a new instance of `XMLNamedNodeMap` 6 | # This is just a wrapper around an ordinary 7 | # JS object. 8 | # 9 | # `nodes` the object containing nodes. 10 | constructor: (@nodes) -> 11 | 12 | 13 | # DOM level 1 14 | Object.defineProperty @::, 'length', get: () -> Object.keys(@nodes).length or 0 15 | 16 | 17 | # Creates and returns a deep clone of `this` 18 | # 19 | clone: () -> 20 | # this class should not be cloned since it wraps 21 | # around a given object. The calling function should check 22 | # whether the wrapped object is null and supply a new object 23 | # (from the clone). 24 | @nodes = null 25 | 26 | 27 | # DOM Level 1 28 | getNamedItem: (name) -> @nodes[name] 29 | setNamedItem: (node) -> 30 | oldNode = @nodes[node.nodeName] 31 | @nodes[node.nodeName] = node 32 | return oldNode or null 33 | removeNamedItem: (name) -> 34 | oldNode = @nodes[name] 35 | delete @nodes[name] 36 | return oldNode or null 37 | item: (index) -> @nodes[Object.keys(@nodes)[index]] or null 38 | 39 | # DOM level 2 functions to be implemented later 40 | getNamedItemNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." 41 | setNamedItemNS: (node) -> throw new Error "This DOM method is not implemented." 42 | removeNamedItemNS: (namespaceURI, localName) -> throw new Error "This DOM method is not implemented." 43 | -------------------------------------------------------------------------------- /src/XMLNodeFilter.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Represents a node filter 3 | module.exports = class XMLNodeFilter 4 | 5 | FilterAccept : 1 6 | FilterReject : 2 7 | FilterSkip : 3 8 | 9 | ShowAll : 0xffffffff 10 | ShowElement : 0x1 11 | ShowAttribute : 0x2 12 | ShowText : 0x4 13 | ShowCDataSection : 0x8 14 | ShowEntityReference : 0x10 15 | ShowEntity : 0x20 16 | ShowProcessingInstruction : 0x40 17 | ShowComment : 0x80 18 | ShowDocument : 0x100 19 | ShowDocumentType : 0x200 20 | ShowDocumentFragment : 0x400 21 | ShowNotation : 0x800 22 | 23 | 24 | # DOM level 4 functions to be implemented later 25 | acceptNode: (node) -> throw new Error "This DOM method is not implemented." 26 | -------------------------------------------------------------------------------- /src/XMLNodeList.coffee: -------------------------------------------------------------------------------- 1 | # Represents a list of nodes 2 | module.exports = class XMLNodeList 3 | 4 | 5 | # Initializes a new instance of `XMLNodeList` 6 | # This is just a wrapper around an ordinary 7 | # JS array. 8 | # 9 | # `nodes` the array containing nodes. 10 | constructor: (@nodes) -> 11 | 12 | 13 | # DOM level 1 14 | Object.defineProperty @::, 'length', get: () -> @nodes.length or 0 15 | 16 | 17 | # Creates and returns a deep clone of `this` 18 | # 19 | clone: () -> 20 | # this class should not be cloned since it wraps 21 | # around a given array. The calling function should check 22 | # whether the wrapped array is null and supply a new array 23 | # (from the clone). 24 | @nodes = null 25 | 26 | 27 | # DOM Level 1 28 | item: (index) -> @nodes[index] or null -------------------------------------------------------------------------------- /src/XMLProcessingInstruction.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLCharacterData = require './XMLCharacterData' 3 | 4 | # Represents a processing instruction 5 | module.exports = class XMLProcessingInstruction extends XMLCharacterData 6 | 7 | 8 | # Initializes a new instance of `XMLProcessingInstruction` 9 | # 10 | # `parent` the parent node 11 | # `target` instruction target 12 | # `value` instruction value 13 | constructor: (parent, target, value) -> 14 | super parent 15 | 16 | if not target? 17 | throw new Error "Missing instruction target. " + @debugInfo() 18 | 19 | @type = NodeType.ProcessingInstruction 20 | @target = @stringify.insTarget target 21 | @name = @target 22 | @value = @stringify.insValue value if value 23 | 24 | 25 | # Creates and returns a deep clone of `this` 26 | clone: () -> 27 | Object.create @ 28 | 29 | 30 | # Converts the XML fragment to string 31 | # 32 | # `options.pretty` pretty prints the result 33 | # `options.indent` indentation for pretty print 34 | # `options.offset` how many indentations to add to every line for pretty print 35 | # `options.newline` newline sequence for pretty print 36 | toString: (options) -> 37 | @options.writer.processingInstruction @, @options.writer.filterOptions(options) 38 | 39 | 40 | isEqualNode: (node) -> 41 | if not super.isEqualNode(node) then return false 42 | if node.target isnt @target then return false 43 | 44 | return true -------------------------------------------------------------------------------- /src/XMLRaw.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLNode = require './XMLNode' 3 | 4 | # Represents a raw node 5 | module.exports = class XMLRaw extends XMLNode 6 | 7 | 8 | # Initializes a new instance of `XMLRaw` 9 | # 10 | # `text` raw text 11 | constructor: (parent, text) -> 12 | super parent 13 | 14 | if not text? 15 | throw new Error "Missing raw text. " + @debugInfo() 16 | 17 | @type = NodeType.Raw 18 | @value = @stringify.raw text 19 | 20 | 21 | # Creates and returns a deep clone of `this` 22 | clone: () -> 23 | Object.create @ 24 | 25 | 26 | # Converts the XML fragment to string 27 | # 28 | # `options.pretty` pretty prints the result 29 | # `options.indent` indentation for pretty print 30 | # `options.offset` how many indentations to add to every line for pretty print 31 | # `options.newline` newline sequence for pretty print 32 | toString: (options) -> 33 | @options.writer.raw @, @options.writer.filterOptions(options) 34 | 35 | -------------------------------------------------------------------------------- /src/XMLStreamWriter.coffee: -------------------------------------------------------------------------------- 1 | NodeType = require './NodeType' 2 | XMLWriterBase = require './XMLWriterBase' 3 | WriterState = require './WriterState' 4 | 5 | # Prints XML nodes to a stream 6 | module.exports = class XMLStreamWriter extends XMLWriterBase 7 | 8 | 9 | # Initializes a new instance of `XMLStreamWriter` 10 | # 11 | # `stream` output stream 12 | # `options.pretty` pretty prints the result 13 | # `options.indent` indentation string 14 | # `options.newline` newline sequence 15 | # `options.offset` a fixed number of indentations to add to every line 16 | # `options.allowEmpty` do not self close empty element tags 17 | # 'options.dontPrettyTextNodes' if any text is present in node, don't indent or LF 18 | # `options.spaceBeforeSlash` add a space before the closing slash of empty elements 19 | constructor: (@stream, options) -> 20 | super(options) 21 | 22 | endline: (node, options, level) -> 23 | if node.isLastRootNode and options.state is WriterState.CloseTag 24 | return '' 25 | else 26 | super(node, options, level) 27 | 28 | document: (doc, options) -> 29 | # set a flag so that we don't insert a newline after the last root level node 30 | for child, i in doc.children 31 | child.isLastRootNode = (i is doc.children.length - 1) 32 | 33 | options = @filterOptions options 34 | 35 | for child in doc.children 36 | @writeChildNode child, options, 0 37 | 38 | cdata: (node, options, level) -> 39 | @stream.write super(node, options, level) 40 | 41 | comment: (node, options, level) -> 42 | @stream.write super(node, options, level) 43 | 44 | declaration: (node, options, level) -> 45 | @stream.write super(node, options, level) 46 | 47 | docType: (node, options, level) -> 48 | level or= 0 49 | 50 | @openNode(node, options, level) 51 | options.state = WriterState.OpenTag 52 | @stream.write @indent(node, options, level) 53 | @stream.write ' 0 63 | @stream.write ' [' 64 | @stream.write @endline(node, options, level) 65 | options.state = WriterState.InsideTag 66 | for child in node.children 67 | @writeChildNode child, options, level + 1 68 | options.state = WriterState.CloseTag 69 | @stream.write ']' 70 | 71 | # close tag 72 | options.state = WriterState.CloseTag 73 | @stream.write options.spaceBeforeSlash + '>' 74 | @stream.write @endline(node, options, level) 75 | options.state = WriterState.None 76 | @closeNode(node, options, level) 77 | 78 | element: (node, options, level) -> 79 | level or= 0 80 | 81 | # open tag 82 | @openNode(node, options, level) 83 | options.state = WriterState.OpenTag 84 | r = @indent(node, options, level) + '<' + node.name 85 | 86 | # attributes 87 | if options.pretty and options.width > 0 88 | len = r.length 89 | for own name, att of node.attribs 90 | ratt = @attribute att, options, level 91 | attLen = ratt.length 92 | if len + attLen > options.width 93 | rline = @indent(node, options, level + 1) + ratt 94 | r += @endline(node, options, level) + rline 95 | len = rline.length 96 | else 97 | rline = ' ' + ratt 98 | r += rline 99 | len += rline.length 100 | else 101 | for own name, att of node.attribs 102 | r += @attribute att, options, level 103 | 104 | @stream.write r 105 | 106 | childNodeCount = node.children.length 107 | firstChildNode = if childNodeCount is 0 then null else node.children[0] 108 | if childNodeCount == 0 or node.children.every((e) -> (e.type is NodeType.Text or e.type is NodeType.Raw or e.type is NodeType.CData) and e.value == '') 109 | # empty element 110 | if options.allowEmpty 111 | @stream.write '>' 112 | options.state = WriterState.CloseTag 113 | @stream.write '' 114 | else 115 | options.state = WriterState.CloseTag 116 | @stream.write options.spaceBeforeSlash + '/>' 117 | else if options.pretty and childNodeCount == 1 and (firstChildNode.type is NodeType.Text or firstChildNode.type is NodeType.Raw or firstChildNode.type is NodeType.CData) and firstChildNode.value? 118 | # do not indent text-only nodes 119 | @stream.write '>' 120 | options.state = WriterState.InsideTag 121 | options.suppressPrettyCount++ 122 | prettySuppressed = true 123 | @writeChildNode firstChildNode, options, level + 1 124 | options.suppressPrettyCount-- 125 | prettySuppressed = false 126 | options.state = WriterState.CloseTag 127 | @stream.write '' 128 | else 129 | @stream.write '>' + @endline(node, options, level) 130 | options.state = WriterState.InsideTag 131 | # inner tags 132 | for child in node.children 133 | @writeChildNode child, options, level + 1 134 | # close tag 135 | options.state = WriterState.CloseTag 136 | @stream.write @indent(node, options, level) + '' 137 | 138 | @stream.write @endline(node, options, level) 139 | options.state = WriterState.None 140 | @closeNode(node, options, level) 141 | 142 | processingInstruction: (node, options, level) -> 143 | @stream.write super(node, options, level) 144 | 145 | raw: (node, options, level) -> 146 | @stream.write super(node, options, level) 147 | 148 | text: (node, options, level) -> 149 | @stream.write super(node, options, level) 150 | 151 | dtdAttList: (node, options, level) -> 152 | @stream.write super(node, options, level) 153 | 154 | dtdElement: (node, options, level) -> 155 | @stream.write super(node, options, level) 156 | 157 | dtdEntity: (node, options, level) -> 158 | @stream.write super(node, options, level) 159 | 160 | dtdNotation: (node, options, level) -> 161 | @stream.write super(node, options, level) 162 | -------------------------------------------------------------------------------- /src/XMLStringWriter.coffee: -------------------------------------------------------------------------------- 1 | XMLWriterBase = require './XMLWriterBase' 2 | 3 | # Prints XML nodes as plain text 4 | module.exports = class XMLStringWriter extends XMLWriterBase 5 | 6 | 7 | # Initializes a new instance of `XMLStringWriter` 8 | # 9 | # `options.pretty` pretty prints the result 10 | # `options.indent` indentation string 11 | # `options.newline` newline sequence 12 | # `options.offset` a fixed number of indentations to add to every line 13 | # `options.allowEmpty` do not self close empty element tags 14 | # 'options.dontPrettyTextNodes' if any text is present in node, don't indent or LF 15 | # `options.spaceBeforeSlash` add a space before the closing slash of empty elements 16 | constructor: (options) -> 17 | super(options) 18 | 19 | document: (doc, options) -> 20 | options = @filterOptions options 21 | 22 | r = '' 23 | for child in doc.children 24 | r += @writeChildNode child, options, 0 25 | 26 | # remove trailing newline 27 | if options.pretty and r.slice(-options.newline.length) == options.newline 28 | r = r.slice(0, -options.newline.length) 29 | 30 | return r -------------------------------------------------------------------------------- /src/XMLStringifier.coffee: -------------------------------------------------------------------------------- 1 | # Converts values to strings 2 | module.exports = class XMLStringifier 3 | 4 | 5 | # Initializes a new instance of `XMLStringifier` 6 | # 7 | # `options.version` The version number string of the XML spec to validate against, e.g. 1.0 8 | # `options.noDoubleEncoding` whether existing html entities are encoded: true or false 9 | # `options.stringify` a set of functions to use for converting values to strings 10 | # `options.noValidation` whether values will be validated and escaped or returned as is 11 | # `options.invalidCharReplacement` a character to replace invalid characters and disable character validation 12 | constructor: (options) -> 13 | options or= {} 14 | @options = options 15 | @options.version = '1.0' if not @options.version 16 | 17 | for own key, value of options.stringify or {} 18 | @[key] = value 19 | 20 | 21 | # Defaults 22 | name: (val) -> 23 | if @options.noValidation then return val 24 | @assertLegalName '' + val or '' 25 | text: (val) -> 26 | if @options.noValidation then return val 27 | @assertLegalChar @textEscape('' + val or '') 28 | cdata: (val) -> 29 | if @options.noValidation then return val 30 | val = '' + val or '' 31 | val = val.replace(']]>', ']]]]>') 32 | @assertLegalChar val 33 | comment: (val) -> 34 | if @options.noValidation then return val 35 | val = '' + val or '' 36 | if val.match /--/ 37 | throw new Error "Comment text cannot contain double-hypen: " + val 38 | @assertLegalChar val 39 | raw: (val) -> 40 | if @options.noValidation then return val 41 | '' + val or '' 42 | attValue: (val) -> 43 | if @options.noValidation then return val 44 | @assertLegalChar @attEscape(val = '' + val or '') 45 | insTarget: (val) -> 46 | if @options.noValidation then return val 47 | @assertLegalChar '' + val or '' 48 | insValue: (val) -> 49 | if @options.noValidation then return val 50 | val = '' + val or '' 51 | if val.match /\?>/ 52 | throw new Error "Invalid processing instruction value: " + val 53 | @assertLegalChar val 54 | xmlVersion: (val) -> 55 | if @options.noValidation then return val 56 | val = '' + val or '' 57 | if not val.match /1\.[0-9]+/ 58 | throw new Error "Invalid version number: " + val 59 | val 60 | xmlEncoding: (val) -> 61 | if @options.noValidation then return val 62 | val = '' + val or '' 63 | if not val.match /^[A-Za-z](?:[A-Za-z0-9._-])*$/ 64 | throw new Error "Invalid encoding: " + val 65 | @assertLegalChar val 66 | xmlStandalone: (val) -> 67 | if @options.noValidation then return val 68 | if val then "yes" else "no" 69 | dtdPubID: (val) -> 70 | if @options.noValidation then return val 71 | @assertLegalChar '' + val or '' 72 | dtdSysID: (val) -> 73 | if @options.noValidation then return val 74 | @assertLegalChar '' + val or '' 75 | dtdElementValue: (val) -> 76 | if @options.noValidation then return val 77 | @assertLegalChar '' + val or '' 78 | dtdAttType: (val) -> 79 | if @options.noValidation then return val 80 | @assertLegalChar '' + val or '' 81 | dtdAttDefault: (val) -> 82 | if @options.noValidation then return val 83 | @assertLegalChar '' + val or '' 84 | dtdEntityValue: (val) -> 85 | if @options.noValidation then return val 86 | @assertLegalChar '' + val or '' 87 | dtdNData: (val) -> 88 | if @options.noValidation then return val 89 | @assertLegalChar '' + val or '' 90 | 91 | 92 | # strings to match while converting from JS objects 93 | convertAttKey: '@' 94 | convertPIKey: '?' 95 | convertTextKey: '#text' 96 | convertCDataKey: '#cdata' 97 | convertCommentKey: '#comment' 98 | convertRawKey: '#raw' 99 | 100 | 101 | # Checks whether the given string contains legal characters 102 | # Fails with an exception on error 103 | # 104 | # `str` the string to check 105 | assertLegalChar: (str) => 106 | if @options.noValidation then return str 107 | 108 | if @options.version is '1.0' 109 | # Valid characters from https://www.w3.org/TR/xml/#charsets 110 | # any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. 111 | # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 112 | # This ES5 compatible Regexp has been generated using the "regenerate" NPM module: 113 | # let xml_10_InvalidChars = regenerate() 114 | # .addRange(0x0000, 0x0008) 115 | # .add(0x000B, 0x000C) 116 | # .addRange(0x000E, 0x001F) 117 | # .addRange(0xD800, 0xDFFF) 118 | # .addRange(0xFFFE, 0xFFFF) 119 | regex = /[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g 120 | if @options.invalidCharReplacement isnt undefined 121 | str = str.replace regex, @options.invalidCharReplacement 122 | else if res = str.match(regex) 123 | throw new Error "Invalid character in string: #{str} at index #{res.index}" 124 | else if @options.version is '1.1' 125 | # Valid characters from https://www.w3.org/TR/xml11/#charsets 126 | # any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. 127 | # [#x1-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 128 | # This ES5 compatible Regexp has been generated using the "regenerate" NPM module: 129 | # let xml_11_InvalidChars = regenerate() 130 | # .add(0x0000) 131 | # .addRange(0xD800, 0xDFFF) 132 | # .addRange(0xFFFE, 0xFFFF) 133 | regex = /[\0\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g 134 | if @options.invalidCharReplacement isnt undefined 135 | str = str.replace regex, @options.invalidCharReplacement 136 | else if res = str.match(regex) 137 | throw new Error "Invalid character in string: #{str} at index #{res.index}" 138 | 139 | return str 140 | 141 | 142 | # Checks whether the given string contains legal characters for a name 143 | # Fails with an exception on error 144 | # 145 | # `str` the string to check 146 | assertLegalName: (str) => 147 | if @options.noValidation then return str 148 | 149 | str = @assertLegalChar str 150 | 151 | regex = /^([:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])([\x2D\.0-:A-Z_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/ 152 | if not str.match(regex) 153 | throw new Error "Invalid character in name: #{str}" 154 | 155 | return str 156 | 157 | 158 | # Escapes special characters in text 159 | # 160 | # See http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping 161 | # 162 | # `str` the string to escape 163 | textEscape: (str) -> 164 | if @options.noValidation then return str 165 | ampregex = if @options.noDoubleEncoding then /(?!&(lt|gt|amp|apos|quot);)&/g else /&/g 166 | str.replace(ampregex, '&') 167 | .replace(//g, '>') 169 | .replace(/\r/g, ' ') 170 | 171 | # Escapes special characters in attribute values 172 | # 173 | # See http://www.w3.org/TR/2000/WD-xml-c14n-20000119.html#charescaping 174 | # 175 | # `str` the string to escape 176 | attEscape: (str) -> 177 | if @options.noValidation then return str 178 | ampregex = if @options.noDoubleEncoding then /(?!&(lt|gt|amp|apos|quot);)&/g else /&/g 179 | str.replace(ampregex, '&') 180 | .replace(/ 12 | super parent 13 | 14 | if not text? 15 | throw new Error "Missing element text. " + @debugInfo() 16 | 17 | @name = "#text" 18 | @type = NodeType.Text 19 | @value = @stringify.text text 20 | 21 | 22 | # DOM level 3 23 | Object.defineProperty @::, 'isElementContentWhitespace', 24 | get: () -> 25 | throw new Error "This DOM method is not implemented." + @debugInfo() 26 | Object.defineProperty @::, 'wholeText', 27 | get: () -> 28 | str = '' 29 | 30 | prev = @previousSibling 31 | while prev 32 | str = prev.data + str 33 | prev = prev.previousSibling 34 | 35 | str += @data 36 | 37 | next = @nextSibling 38 | while next 39 | str = str + next.data 40 | next = next.nextSibling 41 | 42 | return str 43 | 44 | 45 | # Creates and returns a deep clone of `this` 46 | clone: () -> 47 | Object.create @ 48 | 49 | 50 | # Converts the XML fragment to string 51 | # 52 | # `options.pretty` pretty prints the result 53 | # `options.indent` indentation for pretty print 54 | # `options.offset` how many indentations to add to every line for pretty print 55 | # `options.newline` newline sequence for pretty print 56 | toString: (options) -> 57 | @options.writer.text @, @options.writer.filterOptions(options) 58 | 59 | 60 | # DOM level 1 functions to be implemented later 61 | splitText: (offset) -> throw new Error "This DOM method is not implemented." + @debugInfo() 62 | 63 | # DOM level 3 functions to be implemented later 64 | replaceWholeText: (content) -> throw new Error "This DOM method is not implemented." + @debugInfo() -------------------------------------------------------------------------------- /src/XMLTypeInfo.coffee: -------------------------------------------------------------------------------- 1 | Derivation = require './Derivation' 2 | 3 | # Represents a type referenced from element or attribute nodes. 4 | module.exports = class XMLTypeInfo 5 | 6 | 7 | # Initializes a new instance of `XMLTypeInfo` 8 | # 9 | constructor: (@typeName, @typeNamespace) -> 10 | 11 | 12 | # DOM level 3 functions to be implemented later 13 | isDerivedFrom: (typeNamespaceArg, typeNameArg, derivationMethod) -> 14 | throw new Error "This DOM method is not implemented." 15 | -------------------------------------------------------------------------------- /src/XMLUserDataHandler.coffee: -------------------------------------------------------------------------------- 1 | OperationType = require './OperationType' 2 | 3 | # Represents a handler that gets called when its associated 4 | # node object is being cloned, imported, or renamed. 5 | module.exports = class XMLUserDataHandler 6 | 7 | 8 | # Initializes a new instance of `XMLUserDataHandler` 9 | # 10 | constructor: () -> 11 | 12 | # Called whenever the node for which this handler is 13 | # registered is imported or cloned. 14 | # 15 | # `operation` type of operation that is being performed on the node 16 | # `key` the key for which this handler is being called 17 | # `data` the data for which this handler is being called 18 | # `src` the node being cloned, adopted, imported, or renamed 19 | # This is null when the node is being deleted. 20 | # `dst` the node newly created if any, or null 21 | handle: (operation, key, data, src, dst) -> 22 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | { assign, isFunction } = require './Utility' 2 | 3 | XMLDOMImplementation = require './XMLDOMImplementation' 4 | XMLDocument = require './XMLDocument' 5 | XMLDocumentCB = require './XMLDocumentCB' 6 | XMLStringWriter = require './XMLStringWriter' 7 | XMLStreamWriter = require './XMLStreamWriter' 8 | 9 | NodeType = require './NodeType' 10 | WriterState = require './WriterState' 11 | 12 | # Creates a new document and returns the root node for 13 | # chain-building the document tree 14 | # 15 | # `name` name of the root element 16 | # 17 | # `xmldec.version` A version number string, e.g. 1.0 18 | # `xmldec.encoding` Encoding declaration, e.g. UTF-8 19 | # `xmldec.standalone` standalone document declaration: true or false 20 | # 21 | # `doctype.pubID` public identifier of the external subset 22 | # `doctype.sysID` system identifier of the external subset 23 | # 24 | # `options.headless` whether XML declaration and doctype will be included: 25 | # true or false 26 | # `options.keepNullNodes` whether nodes with null values will be kept 27 | # or ignored: true or false 28 | # `options.keepNullAttributes` whether attributes with null values will be 29 | # kept or ignored: true or false 30 | # `options.ignoreDecorators` whether decorator strings will be ignored when 31 | # converting JS objects: true or false 32 | # `options.separateArrayItems` whether array items are created as separate 33 | # nodes when passed as an object value: true or false 34 | # `options.noDoubleEncoding` whether existing html entities are encoded: 35 | # true or false 36 | # `options.stringify` a set of functions to use for converting values to 37 | # strings 38 | # `options.writer` the default XML writer to use for converting nodes to 39 | # string. If the default writer is not set, the built-in XMLStringWriter 40 | # will be used instead. 41 | module.exports.create = (name, xmldec, doctype, options) -> 42 | if not name? 43 | throw new Error "Root element needs a name." 44 | 45 | options = assign { }, xmldec, doctype, options 46 | 47 | # create the document node 48 | doc = new XMLDocument(options) 49 | # add the root node 50 | root = doc.element name 51 | 52 | # prolog 53 | if not options.headless 54 | doc.declaration options 55 | 56 | if options.pubID? or options.sysID? 57 | doc.dtd options 58 | 59 | return root 60 | 61 | # Creates a new document and returns the document node for 62 | # chain-building the document tree 63 | # 64 | # `options.keepNullNodes` whether nodes with null values will be kept 65 | # or ignored: true or false 66 | # `options.keepNullAttributes` whether attributes with null values will be 67 | # kept or ignored: true or false 68 | # `options.ignoreDecorators` whether decorator strings will be ignored when 69 | # converting JS objects: true or false 70 | # `options.separateArrayItems` whether array items are created as separate 71 | # nodes when passed as an object value: true or false 72 | # `options.noDoubleEncoding` whether existing html entities are encoded: 73 | # true or false 74 | # `options.stringify` a set of functions to use for converting values to 75 | # strings 76 | # `options.writer` the default XML writer to use for converting nodes to 77 | # string. If the default writer is not set, the built-in XMLStringWriter 78 | # will be used instead. 79 | # 80 | # `onData` the function to be called when a new chunk of XML is output. The 81 | # string containing the XML chunk is passed to `onData` as its single 82 | # argument. 83 | # `onEnd` the function to be called when the XML document is completed with 84 | # `end`. `onEnd` does not receive any arguments. 85 | module.exports.begin = (options, onData, onEnd) -> 86 | if isFunction(options) 87 | [onData, onEnd] = [options, onData] 88 | options = {} 89 | 90 | if onData 91 | new XMLDocumentCB(options, onData, onEnd) 92 | else 93 | new XMLDocument(options) 94 | 95 | module.exports.stringWriter = (options) -> 96 | new XMLStringWriter(options) 97 | 98 | module.exports.streamWriter = (stream, options) -> 99 | new XMLStreamWriter(stream, options) 100 | 101 | module.exports.implementation = new XMLDOMImplementation() 102 | 103 | module.exports.nodeType = NodeType 104 | module.exports.writerState = WriterState 105 | -------------------------------------------------------------------------------- /test/basic/attributes.coffee: -------------------------------------------------------------------------------- 1 | suite 'Attributes:', -> 2 | test 'Add attribute (single with object argument)', -> 3 | eq( 4 | xml('test4', { headless: true }) 5 | .ele('node', 'element', {"first":"1", "second":"2"}) 6 | .att("third", "3") 7 | .end() 8 | 'element' 9 | ) 10 | 11 | test 'Add attribute (multiple with object argument)', -> 12 | eq( 13 | xml('test4', { headless: true }) 14 | .ele('node').att({"first":"1", "second":"2"}) 15 | .end() 16 | '' 17 | ) 18 | 19 | test 'Remove attribute', -> 20 | eq( 21 | xml('test4', { headless: true }) 22 | .ele('node', 'element', {"first":"1", "second":"2", "third":"3"}) 23 | .removeAttribute("second") 24 | .end() 25 | 'element' 26 | ) 27 | 28 | test 'Remove multiple attributes', -> 29 | eq( 30 | xml('test4', { headless: true }) 31 | .ele('node', 'element', {"first":"1", "second":"2", "third":"3"}) 32 | .removeAttribute(["second", "third"]) 33 | .end() 34 | 'element' 35 | ) 36 | 37 | test 'Empty attribute', -> 38 | eq( 39 | xml('test', { headless: true }) 40 | .ele('node', 'element', {"first":"", "second":"2", "third":""}) 41 | .end() 42 | 'element' 43 | ) 44 | 45 | test 'Skip if null attribute (ele)', -> 46 | eq( 47 | xml('test4', { headless: true }) 48 | .ele('node', 'element', {"first": null, "second": '2'}) 49 | .end() 50 | 'element' 51 | ) 52 | 53 | test 'Skip if null attribute (att)', -> 54 | eq( 55 | xml('test4', { headless: true }) 56 | .ele('node').att("first") 57 | .end() 58 | '' 59 | ) 60 | 61 | test 'Skip if null attribute (JSON)', -> 62 | eq( 63 | xml('test4', { headless: true }) 64 | .ele({'@first': null, '@second': '2'}) 65 | .end() 66 | '' 67 | ) 68 | 69 | test 'Keep null attribute (ele)', -> 70 | eq( 71 | xml('test4', { headless: true, keepNullAttributes: true }) 72 | .ele('node', 'element', {"first": null, "second": '2'}) 73 | .end() 74 | 'element' 75 | ) 76 | 77 | test 'Keep null attribute (att)', -> 78 | eq( 79 | xml('test4', { headless: true, keepNullAttributes: true }) 80 | .ele('node').att("first") 81 | .end() 82 | '' 83 | ) 84 | 85 | test 'Keep null attribute (JSON)', -> 86 | eq( 87 | xml('test4', { headless: true, keepNullAttributes: true }) 88 | .ele({'@first': null, '@second': '2'}) 89 | .end() 90 | '' 91 | ) -------------------------------------------------------------------------------- /test/basic/begin.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML:', -> 2 | test 'begin()', -> 3 | eq( 4 | doc({ headless: true }).ele('root', { att: 'val' }).ele('test').end() 5 | '' 6 | ) 7 | 8 | test 'begin() with prolog', -> 9 | eq( 10 | doc().dec().dtd().up().ele('root').end() 11 | '' 12 | ) 13 | -------------------------------------------------------------------------------- /test/basic/cdata.coffee: -------------------------------------------------------------------------------- 1 | suite 'CDATA', -> 2 | test 'Nested CDATA', -> 3 | eq( 4 | xml('test', {}, {}, { headless: true }) 5 | .cdata('foo and bar]]> bar') 6 | .end() 7 | 'bar]]]]> bar]]>' 8 | ) 9 | -------------------------------------------------------------------------------- /test/basic/clone.coffee: -------------------------------------------------------------------------------- 1 | xmloriginal = xml('test', { headless: true}) 2 | .att('att', 'val') 3 | .ele('nodes') 4 | .ele('node', '1').up() 5 | .ele('node', '2') 6 | .att('att2', 'val2') 7 | .root() 8 | 9 | xmlcloned = xmloriginal.root().clone() 10 | xmlcloned.ele('added', '3') 11 | newxml = xml('test2', { headless: true}).importDocument(xmlcloned) 12 | 13 | suite 'Clone:', -> 14 | test 'Original should remain unchanged', -> 15 | eq( 16 | xmloriginal.end() 17 | '12' 18 | ) 19 | 20 | test 'Cloned should contain all nodes including added node', -> 21 | eq( 22 | newxml.end() 23 | '123' 24 | ) 25 | 26 | test 'Clone each node type', -> 27 | org = xml('test', { headless: true}) 28 | .cdata('val1') 29 | .raw('val2') 30 | .ele('node') 31 | .ins('pi', 'target') 32 | .com('comment') 33 | eq( 34 | xml('test2', { headless: true}).importDocument(org.root().clone()).end() 35 | 'val2' 36 | ) 37 | 38 | -------------------------------------------------------------------------------- /test/basic/comment.coffee: -------------------------------------------------------------------------------- 1 | suite 'Comments', -> 2 | test 'Nothing gets escaped', -> 3 | eq( 4 | xml('comment', { headless: true }) 5 | .comment('<>\'"&\t\n\r').end() 6 | '' 7 | ) 8 | 9 | test 'Comments before and after root', -> 10 | eq( 11 | xml('comment', { headless: true }) 12 | .commentBefore('pre').commentAfter('post').end() 13 | '' 14 | ) 15 | -------------------------------------------------------------------------------- /test/basic/createxml.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML:', -> 2 | test 'Long form', -> 3 | eq( 4 | xml('root') 5 | .ele('xmlbuilder') 6 | .att('for', 'node-js') 7 | .com('CoffeeScript is awesome.') 8 | .nod('repo') 9 | .att('type', 'git') 10 | .txt('git://github.com/oozcitak/xmlbuilder-js.git') 11 | .up() 12 | .up() 13 | .ele('cdata') 14 | .cdata('this is a test\nSecond line') 15 | .up() 16 | .ele('raw') 17 | .raw('&<>&') 18 | .up() 19 | .ele('atttest', { 'att': 'val' }, 'text') 20 | .up() 21 | .ele('atttest', 'text') 22 | .end() 23 | 24 | '' + 25 | '' + 26 | '' + 27 | '' + 28 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 29 | '' + 30 | 'this is a test\nSecond line]]>' + 31 | '&<>&' + 32 | 'text' + 33 | 'text' + 34 | '' 35 | ) 36 | 37 | test 'Long form with attributes', -> 38 | eq( 39 | xml('root') 40 | .ele('xmlbuilder', {'for': 'node-js' }) 41 | .com('CoffeeScript is awesome.') 42 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 43 | .up() 44 | .up() 45 | .ele('cdata') 46 | .cdata('this is a test\nSecond line') 47 | .up() 48 | .ele('raw') 49 | .raw('&<>&') 50 | .up() 51 | .ele('atttest', { 'att': 'val' }, 'text') 52 | .up() 53 | .ele('atttest', 'text') 54 | .att('att', () -> 'val') 55 | .end() 56 | 57 | '' + 58 | '' + 59 | '' + 60 | '' + 61 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 62 | '' + 63 | 'this is a test\nSecond line]]>' + 64 | '&<>&' + 65 | 'text' + 66 | 'text' + 67 | '' 68 | ) 69 | 70 | test 'Pretty printing', -> 71 | eq( 72 | xml('root') 73 | .ele('xmlbuilder', {'for': 'node-js' }) 74 | .com('CoffeeScript is awesome.') 75 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 76 | .up() 77 | .up() 78 | .ele('cdata') 79 | .cdata('this is a test\nSecond line') 80 | .up() 81 | .ele('raw') 82 | .raw('&<>&') 83 | .up() 84 | .ele('atttest', { 'att': 'val' }, 'text') 85 | .up() 86 | .ele('atttest', 'text') 87 | .att('att', () -> 'val') 88 | .end({ pretty: true, indent: ' ' }) 89 | 90 | """ 91 | 92 | 93 | 94 | 95 | git://github.com/oozcitak/xmlbuilder-js.git 96 | 97 | this is a test 98 | Second line]]> 99 | &<>& 100 | text 101 | text 102 | 103 | """ 104 | ) 105 | 106 | test 'Pretty printing with offset', -> 107 | eq( 108 | xml('root') 109 | .ele('xmlbuilder', {'for': 'node-js' }) 110 | .com('CoffeeScript is awesome.') 111 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 112 | .up() 113 | .up() 114 | .ele('cdata') 115 | .cdata('this is a test\nSecond line') 116 | .up() 117 | .ele('raw') 118 | .raw('&<>&') 119 | .up() 120 | .ele('atttest', { 'att': 'val' }, 'text') 121 | .up() 122 | .ele('atttest', 'text') 123 | .att('att', () -> 'val') 124 | .end({ pretty: true, indent: ' ', offset : 1 }) 125 | 126 | """ 127 | TEMPORARY_INDENT 128 | 129 | 130 | 131 | 132 | git://github.com/oozcitak/xmlbuilder-js.git 133 | 134 | this is a test 135 | Second line]]> 136 | &<>& 137 | text 138 | text 139 | 140 | """.replace( 141 | /// 142 | TEMPORARY_INDENT\n 143 | /// 144 | '' 145 | ) #Heredoc format indenting is based on the first non-whitespace character, so we add extra, then replace it 146 | ) 147 | 148 | test 'Pretty printing with empty indent', -> 149 | eq( 150 | xml('root') 151 | .ele('xmlbuilder', {'for': 'node-js' }) 152 | .com('CoffeeScript is awesome.') 153 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 154 | .up() 155 | .up() 156 | .ele('cdata') 157 | .cdata('this is a test\nSecond line') 158 | .up() 159 | .ele('raw') 160 | .raw('&<>&') 161 | .up() 162 | .ele('atttest', { 'att': 'val' }, 'text') 163 | .up() 164 | .ele('atttest', 'text') 165 | .att('att', () -> 'val') 166 | .end({ pretty: true, indent: '' }) 167 | 168 | """ 169 | 170 | 171 | 172 | 173 | git://github.com/oozcitak/xmlbuilder-js.git 174 | 175 | this is a test 176 | Second line]]> 177 | &<>& 178 | text 179 | text 180 | 181 | """ 182 | ) 183 | 184 | test 'Short form with attributes', -> 185 | eq( 186 | xml('root') 187 | .e('xmlbuilder', {'for': 'node-js' }) 188 | .c('CoffeeScript is awesome.') 189 | .n('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 190 | .u() 191 | .u() 192 | .e('cdata') 193 | .d('this is a test\nSecond line') 194 | .u() 195 | .e('raw') 196 | .r('&<>&') 197 | .u() 198 | .e('atttest', { 'att': 'val' }, 'text') 199 | .u() 200 | .e('atttest') 201 | .a('att2', 'val2') 202 | .i('pi', 'pival') 203 | .t('text2') 204 | .end() 205 | 206 | '' + 207 | '' + 208 | '' + 209 | '' + 210 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 211 | '' + 212 | 'this is a test\nSecond line]]>' + 213 | '&<>&' + 214 | 'text' + 215 | 'text2' + 216 | '' 217 | ) 218 | 219 | test 'create() without arguments', -> 220 | eq( 221 | xml('test14').ele('node').txt('test').end() 222 | 'test' 223 | ) 224 | 225 | test 'create() with arguments', -> 226 | eq( 227 | xml('test14', { 'version': '1.1' }).ele('node').txt('test').end() 228 | 'test' 229 | ) 230 | 231 | test 'create() with merged arguments', -> 232 | eq( 233 | xml('test14', { version: '1.1', encoding: 'UTF-8', standalone: true, sysID: 'hello.dtd' }) 234 | .ele('node').txt('test').end() 235 | '' + 236 | 'test' 237 | ) 238 | 239 | eq( 240 | xml('test14', { headless: true, version: '1.1', encoding: 'UTF-8', standalone: true, sysID: 'hello.dtd' }) 241 | .ele('node').txt('test').end() 242 | 'test' 243 | ) 244 | 245 | 246 | test 'create() allowing empty elements', -> 247 | eq( 248 | xml('test15', { headless: true }) 249 | .ele('node').end({ allowEmpty: true }) 250 | '' 251 | ) 252 | -------------------------------------------------------------------------------- /test/basic/doc.coffee: -------------------------------------------------------------------------------- 1 | suite 'Navigation:', -> 2 | test 'Doc', -> 3 | eq( 4 | xml('test7', {}, {}, { headless: true }) 5 | .ele('nodes') 6 | .ele('node', '1').up() 7 | .ele('node', '2').up() 8 | .ele('node', '3') 9 | .end() 10 | '123' 11 | ) 12 | -------------------------------------------------------------------------------- /test/basic/doctype.coffee: -------------------------------------------------------------------------------- 1 | suite 'Document Type Declaration:', -> 2 | test 'SYSTEM dtd from create()', -> 3 | eq( 4 | xml('root', { sysID: 'hello.dtd' }).ele('node').txt('test').end() 5 | 'test' 6 | ) 7 | 8 | test 'Public dtd from create()', -> 9 | eq( 10 | xml('HTML', { 11 | pubID: '-//W3C//DTD HTML 4.01//EN' 12 | sysID: 'http://www.w3.org/TR/html4/strict.dtd' 13 | }).end() 14 | '' + 15 | '' + 17 | '' 18 | ) 19 | 20 | test 'Empty dtd from create()', -> 21 | eq( 22 | xml('root', { sysID: '' }).ele('node').txt('test').end() 23 | 'test' 24 | ) 25 | 26 | test 'Replace dtd', -> 27 | eq( 28 | xml('root').dtd('hello.dtd').up().ele('node').txt('test').dtd('bye.dtd').end() 29 | 'test' 30 | ) 31 | 32 | test 'Internal and external dtd', -> 33 | eq( 34 | xml('root') 35 | .dtd('hello.dtd') 36 | .ins('pub_border', 'thin') 37 | .ele('img', 'EMPTY') 38 | .com('Image attributes follow') 39 | .att('img', 'height', 'CDATA', '#REQUIRED') 40 | .att('img', 'visible', '(yes|no)', '#DEFAULT', "yes") 41 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 42 | .not('fs-nt', { pubID: 'FS Network Reader 1.0' }) 43 | .not('fs-nt', { pubID: 'FS Network Reader 1.0', sysID: 'http://my.fs.com/reader' }) 44 | .att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED') 45 | .dat('John') 46 | .ele('node') 47 | .ent('ent', 'my val') 48 | .ent('ent', { sysID: 'http://www.myspec.com/ent' }) 49 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 50 | .ent('ent', { sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 51 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 52 | .pent('ent', 'my val') 53 | .pent('ent', { sysID: 'http://www.myspec.com/ent' }) 54 | .pent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 55 | .ele('nodearr', ['a', 'b']) 56 | .root() 57 | .ele('node').txt('test') 58 | .end() 59 | 60 | '' + 61 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '' + 68 | '' + 69 | '' + 70 | '' + 71 | 'John]]>' + 72 | '' + 73 | '' + 74 | '' + 75 | '' + 76 | '' + 77 | '' + 78 | '' + 79 | '' + 80 | '' + 81 | '' + 82 | ']>' + 83 | 'test' 84 | ) 85 | 86 | test 'Internal and external dtd (pretty print)', -> 87 | eq( 88 | xml('root') 89 | .dtd('hello.dtd') 90 | .ins('pub_border', 'thin') 91 | .ele('img', 'EMPTY') 92 | .com('Image attributes follow') 93 | .att('img', 'height', 'CDATA', '#REQUIRED') 94 | .att('img', 'visible', '(yes|no)', '#DEFAULT', "yes") 95 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 96 | .not('fs-nt', { pubID: 'FS Network Reader 1.0', sysID: 'http://my.fs.com/reader' }) 97 | .att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED') 98 | .dat('John') 99 | .ele('node') 100 | .ent('ent', 'my val') 101 | .ent('ent', { sysID: 'http://www.myspec.com/ent' }) 102 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 103 | .ent('ent', { sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 104 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 105 | .pent('ent', 'my val') 106 | .pent('ent', { sysID: 'http://www.myspec.com/ent' }) 107 | .pent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 108 | .root() 109 | .ele('node').txt('test') 110 | .end({ pretty: true }) 111 | 112 | """ 113 | 114 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | John]]> 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ]> 134 | 135 | test 136 | 137 | """ 138 | ) 139 | 140 | -------------------------------------------------------------------------------- /test/basic/escaping.coffee: -------------------------------------------------------------------------------- 1 | suite 'Text Processing', -> 2 | test 'Escaping element value', -> 3 | eq( 4 | xml('root', { headless: true }) 5 | .ele('e', 'escaped <>\'"&\t\n').up() 6 | .ele('e', 'escaped <>\'"&\t\r\n').up() 7 | .ele('e', 'escaped <>\'"&\t\n\r').up() 8 | .ele('e') 9 | .att('a1', 'escaped <>\'"&\t\n') 10 | .att('a2', 'escaped <>\'"&\t\r') 11 | .att('a3', 'escaped <>\'"&\t\n\r') 12 | .att('a4', 'escaped <>\'"&\t\r\n') 13 | .up() 14 | .end() 15 | 16 | '' + 17 | 'escaped <>\'"&\t\n' + 18 | 'escaped <>\'"&\t \n' + 19 | 'escaped <>\'"&\t\n ' + 20 | '' + 26 | '' 27 | ) 28 | 29 | -------------------------------------------------------------------------------- /test/basic/headless.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML', -> 2 | test 'Headless', -> 3 | eq( 4 | xml('root', {}, {}, { headless: true }) 5 | .ele('xmlbuilder', { 'for': 'node-js' }) 6 | .ele('repo', { 'type': 'git' }, 'git://github.com/oozcitak/xmlbuilder-js.git') 7 | .end() 8 | 9 | '' + 10 | '' + 11 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 12 | '' + 13 | '' 14 | ) 15 | 16 | -------------------------------------------------------------------------------- /test/basic/import.coffee: -------------------------------------------------------------------------------- 1 | suite 'Edit:', -> 2 | test 'Import', -> 3 | test13imported = xml('test13imported', { headless: true }) 4 | .ele('node', 'imported') 5 | 6 | eq( 7 | xml('test13', { headless: true }) 8 | .importDocument(test13imported.doc()) 9 | .end() 10 | 'imported' 11 | ) 12 | 13 | eq( 14 | xml('test13', { headless: true }) 15 | .importXMLBuilder(test13imported.doc()) 16 | .end() 17 | 'imported' 18 | ) 19 | 20 | test 'Import in to empty document', -> 21 | importedDoc = xml('imported', { headless: true }) 22 | .ele('node', 'imported node') 23 | 24 | eq( 25 | doc({ headless: true }) 26 | .importDocument(importedDoc.doc()) 27 | .end() 28 | 'imported node' 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /test/basic/insert.coffee: -------------------------------------------------------------------------------- 1 | suite 'Editing:', -> 2 | test 'Insert', -> 3 | eq( 4 | xml('test6', {}, {}, { headless: true }) 5 | .e('node','last') 6 | .insertBefore('node','1') 7 | .insertAfter('node','2') 8 | .end() 9 | '12last' 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /test/basic/instructions.coffee: -------------------------------------------------------------------------------- 1 | suite 'Processing Instructions:', -> 2 | test 'Simple', -> 3 | eq( 4 | xml('test17', { headless: true }).ins('pi', 'mypi').end() 5 | '' 6 | ) 7 | 8 | test 'From object', -> 9 | eq( 10 | xml('test17', { headless: true }).ins({'pi': 'mypi', 'pi2': 'mypi2', 'pi3': null}).end() 11 | '' 12 | ) 13 | 14 | test 'From array', -> 15 | eq( 16 | xml('test17', { headless: true }).ins(['pi', 'pi2']).end() 17 | '' 18 | ) 19 | 20 | test 'Complex', -> 21 | eq( 22 | xml('test18', { headless: true }) 23 | .ins('renderCache.subset', '"Verdana" 0 0 ISO-8859-1 4 268 67 "#(),-./') 24 | .ins('pitarget', () -> 'pivalue') 25 | .end() 26 | '' 27 | ) 28 | 29 | test 'Instructions before and after root', -> 30 | eq( 31 | xml('ins', { headless: true }) 32 | .instructionBefore('pre' ,'val1').instructionAfter('post', 'val2').end() 33 | '' 34 | ) 35 | -------------------------------------------------------------------------------- /test/basic/multipleinstances.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML:', -> 2 | test 'Multiple Instances', -> 3 | 4 | xml1 = xml('first').ele('node1', { 'testatt1': 'testattval1' }, 'text1') 5 | eq( 6 | xml1.end() 7 | 'text1' 8 | ) 9 | 10 | xml2 = xml('second').ele('node2', { 'testatt2': 'testattval2' }, 'text2') 11 | eq( 12 | xml2.end() 13 | 'text2' 14 | ) 15 | 16 | # First instance should remain unchanged 17 | eq( 18 | xml1.end() 19 | 'text1' 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /test/basic/nodetype.coffee: -------------------------------------------------------------------------------- 1 | suite 'Check node type:', -> 2 | test 'Document node types', -> 3 | obj = 4 | root: 5 | '@age': 35 6 | '#raw': '' 7 | '#text': '' 8 | '#cdata': '' 9 | '#comment': '' 10 | '?pi': '' 11 | 12 | doc = xml(obj, { sysID: 'hello.dtd' }).doc() 13 | root = doc.root() 14 | 15 | eq(doc.type, builder.nodeType.Document) 16 | eq(doc.children[0].type, builder.nodeType.Declaration) 17 | eq(doc.children[1].type, builder.nodeType.DocType) 18 | eq(root.type, builder.nodeType.Element) 19 | eq(root.children[0].type, builder.nodeType.Raw) 20 | eq(root.children[1].type, builder.nodeType.Text) 21 | eq(root.children[2].type, builder.nodeType.CData) 22 | eq(root.children[3].type, builder.nodeType.Comment) 23 | eq(root.children[4].type, builder.nodeType.ProcessingInstruction) 24 | 25 | test 'DTD node types', -> 26 | dtd = xml('root', { headless: true }).dtd() 27 | .att('img', 'height', 'CDATA', '#REQUIRED') 28 | .ele('img', 'EMPTY') 29 | .ent('ent', 'my val') 30 | .pent('ent', 'my val') 31 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 32 | 33 | eq(dtd.type, builder.nodeType.DocType) 34 | eq(dtd.children[0].type, builder.nodeType.AttributeDeclaration) 35 | eq(dtd.children[1].type, builder.nodeType.ElementDeclaration) 36 | eq(dtd.children[2].type, builder.nodeType.EntityDeclaration) 37 | eq(dtd.children[3].type, builder.nodeType.EntityDeclaration) 38 | eq(dtd.children[4].type, builder.nodeType.NotationDeclaration) 39 | -------------------------------------------------------------------------------- /test/basic/object.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML:', -> 2 | test 'From JS object (simple)', -> 3 | obj = 4 | root: 5 | xmlbuilder: 6 | '@for': 'node-js' 7 | repo: 8 | '@type': 'git' 9 | '#text': 'git://github.com/oozcitak/xmlbuilder-js.git' 10 | 11 | eq( 12 | xml(obj).end() 13 | '' + 14 | '' + 15 | '' + 16 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 17 | '' + 18 | '' 19 | ) 20 | 21 | test 'From JS object (functions)', -> 22 | obj = 23 | squares: 24 | '#comment': 'f(x) = x^2' 25 | 'data': () -> 26 | ret = for i in [1..5] 27 | { '@x': i, '@y': i * i } 28 | 29 | eq( 30 | xml(obj).end() 31 | '' + 32 | '' + 33 | '' + 34 | '' + 35 | '' + 36 | '' + 37 | '' + 38 | '' + 39 | '' 40 | ) 41 | 42 | test 'From JS object (decorators)', -> 43 | obj = 44 | ele: "simple element" 45 | person: 46 | name: "John" 47 | '@age': 35 48 | '?pi': 'mypi' 49 | '#comment': 'Good guy' 50 | '#cdata': 'well formed!' 51 | unescaped: 52 | '#raw': '&<>&' 53 | address: 54 | city: "Istanbul" 55 | street: "End of long and winding road" 56 | contact: 57 | phone: [ "555-1234", "555-1235" ] 58 | id: () -> return 42 59 | details: 60 | '#text': 'classified' 61 | 62 | eq( 63 | xml('root', { headless: true }) 64 | .ele(obj).up() 65 | .ele('added') 66 | .end() 67 | 68 | '' + 69 | 'simple element' + 70 | '' + 71 | 'John' + 72 | '' + 73 | '' + 74 | '' + 75 | '&<>&' + 76 | '
' + 77 | 'Istanbul' + 78 | 'End of long and winding road' + 79 | '
' + 80 | '' + 81 | '555-1234' + 82 | '555-1235' + 83 | '' + 84 | '42' + 85 | '
classified
' + 86 | '
' + 87 | '' + 88 | '
' 89 | ) 90 | 91 | test 'From JS object (ignore decorators)', -> 92 | obj = 93 | ele: "simple element" 94 | person: 95 | name: "John" 96 | '@age': 35 97 | '?pi': 'mypi' 98 | '#comment': 'Good guy' 99 | '#cdata': 'well formed!' 100 | unescaped: 101 | '#raw': '&<>&' 102 | address: 103 | city: "Istanbul" 104 | street: "End of long and winding road" 105 | phone: [ 106 | "555-1234" 107 | "555-1235" 108 | ] 109 | id: () -> return 42 110 | details: 111 | '#text': 'classified' 112 | 113 | eq( 114 | xml('root', { headless: true, ignoreDecorators: true, noValidation: true }) 115 | .ele(obj).up() 116 | .ele('added') 117 | .end() 118 | 119 | '' + 120 | 'simple element' + 121 | '' + 122 | 'John' + 123 | '<@age>35' + 124 | 'mypi' + 125 | '<#comment>Good guy' + 126 | '<#cdata>well formed!' + 127 | '<#raw>&<>&' + 128 | '
' + 129 | 'Istanbul' + 130 | 'End of long and winding road' + 131 | '
' + 132 | '555-1234' + 133 | '555-1235' + 134 | '42' + 135 | '
<#text>classified
' + 136 | '
' + 137 | '' + 138 | '
' 139 | ) 140 | 141 | test 'From JS object (deep nesting)', -> 142 | obj = 143 | one: 144 | '@val': 1 145 | two: 146 | '@val': 2 147 | three: 148 | '@val': 3 149 | four: 150 | '@val': 4 151 | five: 152 | '@val': 5 153 | six: 154 | '@val': 6 155 | ends: 'here' 156 | 157 | eq( 158 | xml('root', { headless: true }).ele(obj).end() 159 | '' + 160 | '' + 161 | '' + 162 | '' + 163 | '' + 164 | '' + 165 | '' + 166 | 'here' + 167 | '' + 168 | '' + 169 | '' + 170 | '' + 171 | '' + 172 | '' + 173 | '' 174 | ) 175 | 176 | test 'From JS object (root level)', -> 177 | obj = 178 | myroot: 179 | ele: "simple element" 180 | person: 181 | name: "John" 182 | '@age': 35 183 | address: 184 | city: "Istanbul" 185 | street: "End of long and winding road" 186 | phone: [ 187 | { '#text': "555-1234", '@type': 'home' } 188 | { '#text': "555-1235", '@type': 'mobile' } 189 | ] 190 | id: () -> return 42 191 | 192 | eq( 193 | xml(obj, { headless: true }).ele('added').end() 194 | '' + 195 | 'simple element' + 196 | '' + 197 | 'John' + 198 | '
' + 199 | 'Istanbul' + 200 | 'End of long and winding road' + 201 | '
' + 202 | '555-1234' + 203 | '555-1235' + 204 | '42' + 205 | '
' + 206 | '' + 207 | '
' 208 | ) 209 | 210 | test 'From JS object (simple array)', -> 211 | obj = [ 212 | "one" 213 | "two" 214 | () -> return "three" 215 | ] 216 | 217 | eq( 218 | xml('root', { headless: true }).ele(obj).end() 219 | '' + 220 | '' + 221 | '' + 222 | '' + 223 | '' 224 | ) 225 | 226 | test 'From JS object (empty array should produce no nodes)', -> 227 | eq( 228 | xml('root', { headless: true }).ele({ item: [] }).end() 229 | '' 230 | ) 231 | 232 | test 'From JS object (empty array should produce one node if separateArrayItems is set)', -> 233 | eq( 234 | xml('root', { headless: true, separateArrayItems: true }).ele({ item: [] }).end() 235 | '' 236 | ) 237 | 238 | test 'From JS object (empty object should produce one node)', -> 239 | eq( 240 | xml('root', { headless: true }).ele({ item: {} }).end() 241 | '' 242 | ) 243 | 244 | test 'From JS object (empty array with empty object should produce one node)', -> 245 | eq( 246 | xml('root', { headless: true }).ele({ item: [{}] }).end() 247 | '' 248 | ) 249 | 250 | test 'From JS object (null should produce no nodes)', -> 251 | eq( 252 | xml('root', { headless: true }).ele({ item: null }).end() 253 | '' 254 | ) 255 | -------------------------------------------------------------------------------- /test/basic/objectconsistency.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML:', -> 2 | test 'Object builder consistency', -> 3 | obj = 4 | ele: "simple element" 5 | person: 6 | name: "John" 7 | '@age': 35 8 | '?pi': 'mypi' 9 | '#comment': 'Good guy' 10 | '#cdata': 'well formed!' 11 | unescaped: 12 | '#raw': '&<>&' 13 | address: 14 | city: "Istanbul" 15 | street: "End of long and winding road" 16 | contact: 17 | phone: [ 18 | "555-1234" 19 | "555-1235" 20 | ] 21 | id: () -> return 42 22 | details: 23 | '#text': 'classified' 24 | nullval: null 25 | 26 | t1 = xml('root', { headless: true }).ele(obj) 27 | 28 | t2 = xml('root', { headless: true }) 29 | .ele('ele', 'simple element') 30 | .up() 31 | .ele('person') 32 | .ele('name', 'John') 33 | .up() 34 | .att('age', 35) 35 | .ins('pi', 'mypi') 36 | .com('Good guy') 37 | .dat('well formed!') 38 | .ele('unescaped') 39 | .raw('&<>&') 40 | .up() 41 | .ele('address') 42 | .ele('city', 'Istanbul') 43 | .up() 44 | .ele('street', 'End of long and winding road') 45 | .up() 46 | .up() 47 | .ele('contact') 48 | .ele('phone', '555-1234') 49 | .up() 50 | .ele('phone', '555-1235') 51 | .up() 52 | .up() 53 | .ele('id', 42) 54 | .up() 55 | .ele('details') 56 | .text('classified') 57 | .up() 58 | .ele('nullval', null) 59 | 60 | eq( 61 | t1.end() 62 | t2.end() 63 | ) 64 | 65 | -------------------------------------------------------------------------------- /test/basic/prettyattributes.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML with string writer:', -> 2 | test 'Pretty print attributes - 1', -> 3 | eq( 4 | xml('test', { headless: true }) 5 | .ele('node', {"first":"1", "second":"2"}) 6 | .end({ pretty: true, width: 20 }) 7 | """ 8 | 9 | 11 | 12 | """ 13 | ) 14 | 15 | test 'Pretty print attributes - 2', -> 16 | eq( 17 | xml('test', { headless: true }) 18 | .ele('node', {"first":"1", "second":"2", "third":"33333333333333333333", "fourth": 4}) 19 | .end({ pretty: true, width: 10 }) 20 | """ 21 | 22 | 27 | 28 | """ 29 | ) 30 | 31 | test 'Pretty print attributes - 3', -> 32 | eq( 33 | xml('test', { headless: true }) 34 | .ele('node', {"first":"1", "second":"2", "third":"33333333333333333333", "fourth": 4}) 35 | .end({ pretty: true, width: 1 }) 36 | """ 37 | 38 | 43 | 44 | """ 45 | ) 46 | 47 | test 'Pretty print attributes - 4', -> 48 | eq( 49 | xml('test', { headless: true }) 50 | .ele('node', {"first":"1", "second":"2"}).ele('child') 51 | .end({ pretty: true, width: 10 }) 52 | """ 53 | 54 | 57 | 58 | 59 | 60 | """ 61 | ) 62 | -------------------------------------------------------------------------------- /test/basic/prettyattributesstream.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML with stream writer:', -> 2 | hook = null 3 | setup 'hook stdout.write', -> 4 | hook = captureStream(process.stdout) 5 | return 6 | teardown 'unhook stdout.write', -> 7 | hook.unhook() 8 | return 9 | 10 | test 'Pretty print attributes - 1', -> 11 | xml('test', { headless: true }) 12 | .ele('node', {"first":"1", "second":"2"}) 13 | .end(builder.streamWriter(process.stdout, { pretty: true, width: 20 })) 14 | eq( 15 | hook.captured() 16 | """ 17 | 18 | 20 | 21 | """ 22 | ) 23 | 24 | test 'Pretty print attributes - 2', -> 25 | xml('test', { headless: true }) 26 | .ele('node', {"first":"1", "second":"2", "third":"33333333333333333333", "fourth": 4}) 27 | .end(builder.streamWriter(process.stdout, { pretty: true, width: 10 })) 28 | eq( 29 | hook.captured() 30 | """ 31 | 32 | 37 | 38 | """ 39 | ) 40 | 41 | test 'Pretty print attributes - 3', -> 42 | xml('test', { headless: true }) 43 | .ele('node', {"first":"1", "second":"2", "third":"33333333333333333333", "fourth": 4}) 44 | .end(builder.streamWriter(process.stdout, { pretty: true, width: 1 })) 45 | eq( 46 | hook.captured() 47 | """ 48 | 49 | 54 | 55 | """ 56 | ) 57 | 58 | test 'Pretty print attributes - 4', -> 59 | xml('test', { headless: true }) 60 | .ele('node', {"first":"1", "second":"2"}).ele('child') 61 | .end(builder.streamWriter(process.stdout, { pretty: true, width: 10 })) 62 | eq( 63 | hook.captured() 64 | """ 65 | 66 | 69 | 70 | 71 | 72 | """ 73 | ) 74 | -------------------------------------------------------------------------------- /test/basic/prevnextroot.coffee: -------------------------------------------------------------------------------- 1 | suite 'Navigation:', -> 2 | test 'Prev/next/root', -> 3 | eq( 4 | xml('test5', { headless: true }) 5 | .e('node','1') 6 | .up() 7 | .e('node','element') 8 | .up() 9 | .e('node','2') 10 | .prev() 11 | .prev() 12 | .att('prev','yes') 13 | .next() 14 | .next() 15 | .att('next','yes') 16 | .root() 17 | .att('root', 'yes') 18 | .end() 19 | '1element2' 20 | ) 21 | 22 | test 'up() in dtd', -> 23 | eq( 24 | xml('test', { headless: true }).dtd().com('internal subset').up().ele('node').end() 25 | ']>' + 26 | '' 27 | ) 28 | 29 | test 'doc() in element', -> 30 | isan( 31 | xml('test').ele('node').doc() 32 | 'XMLDocument' 33 | ) 34 | 35 | test 'doc() in dtd', -> 36 | isan( 37 | xml('test').dtd().doc() 38 | 'XMLDocument' 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /test/basic/primitives.coffee: -------------------------------------------------------------------------------- 1 | suite 'Implicit Conversion to Primitives:', -> 2 | test 'String', -> 3 | eq( 4 | xml('test', {}, {}, { headless: true }) 5 | .ele('node', 'hello') 6 | .nod('node', 'hello') 7 | .ins('node', 'hello') 8 | .end() 9 | xml('test', {}, {}, { headless: true }) 10 | .ele('node', new String('hello')) 11 | .nod('node', new String('hello')) 12 | .ins('node', new String('hello')) 13 | .end() 14 | ) 15 | 16 | test 'Boolean', -> 17 | eq( 18 | xml('test', {}, {}, { headless: true }) 19 | .ele('node', true) 20 | .end() 21 | xml('test', {}, {}, { headless: true }) 22 | .ele('node', new Boolean(true)) 23 | .end() 24 | ) 25 | 26 | test 'Number', -> 27 | eq( 28 | xml('test', {}, {}, { headless: true }) 29 | .ele('node', 123) 30 | .end() 31 | xml('test', {}, {}, { headless: true }) 32 | .ele('node', new Number(123)) 33 | .end() 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /test/basic/removeitem.coffee: -------------------------------------------------------------------------------- 1 | suite 'Editing:', -> 2 | test 'Remove item', -> 3 | eq( 4 | xml('test3', {}, {}, { headless: true }) 5 | .e('node', 'first instance') 6 | .u() 7 | .e('node', 'second instance') 8 | .remove() 9 | .e('node', 'third instance') 10 | .end() 11 | 'first instancethird instance' 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /test/basic/skipnullelements.coffee: -------------------------------------------------------------------------------- 1 | suite 'Element:', -> 2 | 3 | test 'Skip if null (ele)', -> 4 | eq( 5 | xml('test', { headless: true }) 6 | .ele('node1', 'val1').up() 7 | .ele('node2', null).up() 8 | .ele('node3', undefined).up() 9 | .ele('node4', 'val4').up() 10 | .ele('node5', '').up() 11 | .ele('node6', {att: 'val'}).up() 12 | .end() 13 | '' + 14 | 'val1' + 15 | '' + 16 | 'val4' + 17 | '' + 18 | '' + 19 | '' 20 | ) 21 | 22 | test 'Skip if null or undefined (JSON)', -> 23 | eq( 24 | xml('test', { headless: true }) 25 | .ele( { node1: 'val1', node2: null, node3: undefined, node4: 'val4', node5: '' }) 26 | .end() 27 | '' + 28 | 'val1' + 29 | 'val4' + 30 | '' + 31 | '' 32 | ) 33 | 34 | test 'Keep if null (ele) with keepNullNodes', -> 35 | eq( 36 | xml('test', { headless: true, keepNullNodes: true }) 37 | .ele('node1', 'val1').up() 38 | .ele('node2', null).up() 39 | .ele('node3', undefined).up() 40 | .ele('node4', 'val4').up() 41 | .ele('node5', '').up() 42 | .end() 43 | '' + 44 | 'val1' + 45 | '' + 46 | '' + 47 | 'val4' + 48 | '' + 49 | '' 50 | ) 51 | 52 | test 'Keep if null (JSON) with keepNullNodes', -> 53 | eq( 54 | xml('test', { headless: true, keepNullNodes: true }) 55 | .ele( { node1: 'val1', node2: null, node3: undefined, node4: 'val4', node5: '' }) 56 | .end() 57 | '' + 58 | 'val1' + 59 | '' + 60 | '' + 61 | 'val4' + 62 | '' + 63 | '' 64 | ) -------------------------------------------------------------------------------- /test/basic/string.coffee: -------------------------------------------------------------------------------- 1 | suite 'Convert to String:', -> 2 | test 'end() method', -> 3 | eq( 4 | xml('test16', { 'version': '1.1' } ).ele('node').txt('test').end() 5 | 'test' 6 | ) 7 | 8 | test 'end() method of builder', -> 9 | eq( 10 | xml('test').doc().end() 11 | '' 12 | ) 13 | -------------------------------------------------------------------------------- /test/basic/stringify.coffee: -------------------------------------------------------------------------------- 1 | suite 'Stringify:', -> 2 | test 'Custom function', -> 3 | addns = (val) -> 'my:' + val 4 | eq( 5 | xml('test7', { headless: true, stringify: { name: addns } }) 6 | .ele('nodes') 7 | .ele('node', '1').up() 8 | .ele('node', '2').up() 9 | .ele('node', '3') 10 | .end() 11 | '123' 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /test/basic/text.coffee: -------------------------------------------------------------------------------- 1 | suite 'Text Processing:', -> 2 | test 'Escape "', -> 3 | eq( 4 | xml('test8', {}, {}, { headless: true }).ele('node', '"').end() 5 | '"' 6 | ) 7 | 8 | test 'Text node with empty string', -> 9 | eq( 10 | xml('test9', {}, {}, { headless: true }).text('').end() 11 | '' 12 | ) 13 | 14 | test 'Text node with empty string (pretty print)', -> 15 | eq( 16 | xml('test10', {}, {}, { headless: true }).text('').end() 17 | '' 18 | ) 19 | -------------------------------------------------------------------------------- /test/basic/tostring.coffee: -------------------------------------------------------------------------------- 1 | suite 'Test toString() function with built-in XML writer:', -> 2 | test 'Nodes', -> 3 | eq xml('root').doc().toString(), '' 4 | eq xml('root').toString(), '' 5 | eq xml('root').att('att', 'val').attribs['att'].toString(), ' att="val"' 6 | eq xml('root').dat('val').children[0].toString(), '' 7 | eq xml('root').com('val').children[0].toString(), '' 8 | eq xml('root').ins('target', 'val').children[0].toString(), '' 9 | eq xml('root').raw('val').children[0].toString(), 'val' 10 | eq xml('root').text('val').children[0].toString(), 'val' 11 | 12 | test 'DTD', -> 13 | eq( 14 | xml('root').dtd({pubID: 'pub', sysID: 'sys'}).toString() 15 | '' 16 | ) 17 | eq( 18 | xml('root').dtd().att('img', 'visible', '(yes|no)', '#DEFAULT', "yes").children[0].toString() 19 | '' 20 | ) 21 | eq( 22 | xml('root').dtd().ele('img', 'EMPTY').children[0].toString() 23 | '' 24 | ) 25 | eq( 26 | xml('root').dtd().ent('ent', 'my val').children[0].toString() 27 | '' 28 | ) 29 | eq( 30 | xml('root').dtd().not('fs', { sysID: 'http://my.fs.com/reader' }).children[0].toString() 31 | '' 32 | ) 33 | 34 | test 'XML Declaration', -> 35 | eq( 36 | xml('root').dec().doc().children[0].toString() 37 | '' 38 | ) 39 | -------------------------------------------------------------------------------- /test/basic/validatechar.coffee: -------------------------------------------------------------------------------- 1 | suite 'Validate Input Chars:', -> 2 | 3 | test 'Invalid chars in XML 1.0', -> 4 | err () -> xml('test').txt('invalid char \u{0000}') 5 | err () -> xml('test').txt('invalid char \u{0001}') 6 | err () -> xml('test').txt('invalid char \u{0002}') 7 | err () -> xml('test').txt('invalid char \u{0003}') 8 | err () -> xml('test').txt('invalid char \u{0004}') 9 | err () -> xml('test').txt('invalid char \u{0005}') 10 | err () -> xml('test').txt('invalid char \u{0006}') 11 | err () -> xml('test').txt('invalid char \u{0007}') 12 | err () -> xml('test').txt('invalid char \u{0008}') 13 | err () -> xml('test').txt('invalid char \u{000B}') 14 | err () -> xml('test').txt('invalid char \u{000C}') 15 | err () -> xml('test').txt('invalid char \u{000E}') 16 | err () -> xml('test').txt('invalid char \u{000F}') 17 | err () -> xml('test').txt('invalid char \u{0010}') 18 | err () -> xml('test').txt('invalid char \u{0011}') 19 | err () -> xml('test').txt('invalid char \u{0012}') 20 | err () -> xml('test').txt('invalid char \u{0013}') 21 | err () -> xml('test').txt('invalid char \u{0014}') 22 | err () -> xml('test').txt('invalid char \u{0015}') 23 | err () -> xml('test').txt('invalid char \u{0016}') 24 | err () -> xml('test').txt('invalid char \u{0017}') 25 | err () -> xml('test').txt('invalid char \u{0018}') 26 | err () -> xml('test').txt('invalid char \u{001A}') 27 | err () -> xml('test').txt('invalid char \u{001B}') 28 | err () -> xml('test').txt('invalid char \u{001C}') 29 | err () -> xml('test').txt('invalid char \u{001D}') 30 | err () -> xml('test').txt('invalid char \u{001E}') 31 | err () -> xml('test').txt('invalid char \u{001F}') 32 | err () -> xml('test').txt('invalid char \u{D800}') 33 | err () -> xml('test').txt('invalid char \u{DFFF}') 34 | err () -> xml('test').txt('invalid char \u{FFFE}') 35 | err () -> xml('test').txt('invalid char \u{FFFF}') 36 | 37 | test 'Invalid chars in XML 1.1', -> 38 | err () -> xml('test', { version: '1.1' }).txt('invalid char \u{0000}') 39 | err () -> xml('test', { version: '1.1' }).txt('invalid char \u{D800}') 40 | err () -> xml('test', { version: '1.1' }).txt('invalid char \u{DFFF}') 41 | err () -> xml('test', { version: '1.1' }).txt('invalid char \u{FFFE}') 42 | err () -> xml('test', { version: '1.1' }).txt('invalid char \u{FFFF}') 43 | eq( 44 | xml('root', { headless: true, version: '1.1' }).txt('char_\u{0008}_valid_in_XML_1.1').end() 45 | 'char_\u{0008}_valid_in_XML_1.1' 46 | ) 47 | 48 | test 'Invalid names', -> 49 | err () -> xml('.test') 50 | err () -> xml('_?test') -------------------------------------------------------------------------------- /test/basic/xmldeclaration.coffee: -------------------------------------------------------------------------------- 1 | suite 'XML Declaration:', -> 2 | test 'From create() without arguments', -> 3 | eq( 4 | xml('test').end() 5 | '' 6 | ) 7 | 8 | test 'From create() with arguments', -> 9 | eq( 10 | xml('test', { version: '1.1', encoding: 'UTF-8', standalone: true }).end() 11 | '' 12 | ) 13 | 14 | test 'From dec() without arguments', -> 15 | eq( 16 | xml('test', { headless: true }).dec().ele('node').end() 17 | '' 18 | ) 19 | 20 | test 'From dec() with arguments', -> 21 | eq( 22 | xml('test').dec({ version: '1.1', encoding: 'UTF-8', standalone: true }).ele('node').end() 23 | '' 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /test/callback/attributes.coffee: -------------------------------------------------------------------------------- 1 | suite 'Attributes:', -> 2 | 3 | result = '' 4 | data = (chunk, level) -> 5 | result += chunk 6 | 7 | test 'All forms of att() usage', -> 8 | result = '' 9 | doc(data) 10 | .node('test4') 11 | .ele('node', {"first":"1", "second":"2"}) 12 | .att("third", "3") 13 | .up() 14 | .ele('node').att({"first":"1", "second":"2"}).up() 15 | .up() 16 | .end() 17 | 18 | eq( 19 | result 20 | 21 | '' + 22 | '' + 23 | '' + 24 | '' 25 | ) 26 | 27 | test 'Skip null attributes', -> 28 | result = '' 29 | doc(data) 30 | .node('test') 31 | .ele('node', {"first": null}) 32 | .att("third", null) 33 | .up() 34 | .ele('node').att({"first": null}).up() 35 | .up() 36 | .end() 37 | 38 | eq( 39 | result 40 | 41 | '' + 42 | '' + 43 | '' + 44 | '' 45 | ) 46 | 47 | test 'Keep null attributes', -> 48 | result = '' 49 | doc({ keepNullAttributes: true }, data) 50 | .node('test') 51 | .ele('node', {"first": null}) 52 | .att("second", null) 53 | .up() 54 | .ele('node').att({"first": null}).up() 55 | .up() 56 | .end() 57 | 58 | eq( 59 | result 60 | 61 | '' + 62 | '' + 63 | '' + 64 | '' 65 | ) 66 | -------------------------------------------------------------------------------- /test/callback/beginwithcallback.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML using begin() callbacks:', -> 2 | test 'Long form', -> 3 | 4 | result = '' 5 | data = (chunk, level) -> 6 | result += chunk 7 | 8 | doc(data) 9 | .dec() 10 | .dtd('root', 'hello.dtd') 11 | .ins('pub_border', 'thin') 12 | .ele('img', 'EMPTY') 13 | .com('Image attributes follow') 14 | .att('img', 'height', 'CDATA', '#REQUIRED') 15 | .att('img', 'visible', '(yes|no)', '#DEFAULT', "yes") 16 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 17 | .not('fs-nt', { pubID: 'FS Network Reader 1.0' }) 18 | .not('fs-nt', { pubID: 'FS Network Reader 1.0', sysID: 'http://my.fs.com/reader' }) 19 | .att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED') 20 | .dat('John') 21 | .ele('node') 22 | .ent('ent', 'my val') 23 | .ent('ent', { sysID: 'http://www.myspec.com/ent' }) 24 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 25 | .ent('ent', { sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 26 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 27 | .pent('ent', 'my val') 28 | .pent('ent', { sysID: 'http://www.myspec.com/ent' }) 29 | .pent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 30 | .ele('nodearr', ['a', 'b']) 31 | .up() 32 | .ele('root') 33 | .ele('xmlbuilder', {'for': 'node-js' }) 34 | .com('CoffeeScript is awesome.') 35 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git').up() 36 | .nod('repo', 'git://github.com/oozcitak/xmlbuilder-js.git', {'type': 'git'}).up() 37 | .up() 38 | .ele('cdata') 39 | .att('att', 'val') 40 | .dat('this is a test\nSecond line') 41 | .txt('text node') 42 | .ins('target', 'value') 43 | .up() 44 | .ele('raw') 45 | .raw('&<>&') 46 | .up() 47 | .ele('atttest', { 'att': 'val' }, 'text') 48 | .end() 49 | 50 | eq( 51 | result 52 | 53 | '' + 54 | '' + 56 | '' + 57 | '' + 58 | '' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | 'John]]>' + 65 | '' + 66 | '' + 67 | '' + 68 | '' + 69 | '' + 70 | '' + 71 | '' + 72 | '' + 73 | '' + 74 | '' + 75 | ']>' + 76 | '' + 77 | '' + 78 | '' + 79 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 80 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 81 | '' + 82 | '' + 83 | 'this is a test\nSecond line]]>' + 84 | 'text node' + 85 | '' + 86 | '' + 87 | '&<>&' + 88 | 'text' + 89 | '' 90 | ) 91 | 92 | test 'Short form', -> 93 | 94 | result = '' 95 | data = (chunk) -> 96 | result += chunk 97 | 98 | doc(data) 99 | .dec() 100 | .dtd('root', 'hello.dtd') 101 | .ins('pub_border', 'thin') 102 | .ele('img', 'EMPTY') 103 | .com('Image attributes follow') 104 | .a('img', 'height', 'CDATA', '#REQUIRED') 105 | .att('img', 'visible', '(yes|no)', '#DEFAULT', "yes") 106 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 107 | .not('fs-nt', { pubID: 'FS Network Reader 1.0' }) 108 | .not('fs-nt', { pubID: 'FS Network Reader 1.0', sysID: 'http://my.fs.com/reader' }) 109 | .att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED') 110 | .dat('John') 111 | .ele('node') 112 | .ent('ent', 'my val') 113 | .ent('ent', { sysID: 'http://www.myspec.com/ent' }) 114 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 115 | .ent('ent', { sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 116 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 117 | .pent('ent', 'my val') 118 | .pent('ent', { sysID: 'http://www.myspec.com/ent' }) 119 | .pent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 120 | .ele('nodearr', ['a', 'b']) 121 | .up() 122 | .e('root') 123 | .e('xmlbuilder', {'for': 'node-js' }) 124 | .c('CoffeeScript is awesome.') 125 | .n('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git') 126 | .up() 127 | .up() 128 | .e('cdata') 129 | .a('att', 'val') 130 | .d('this is a test\nSecond line') 131 | .t('text node') 132 | .i('target', 'value') 133 | .up() 134 | .e('raw') 135 | .r('&<>&') 136 | .up() 137 | .e('atttest', { 'att': 'val' }, 'text') 138 | .end() 139 | 140 | eq( 141 | result 142 | 143 | '' + 144 | '' + 146 | '' + 147 | '' + 148 | '' + 149 | '' + 150 | '' + 151 | '' + 152 | '' + 153 | '' + 154 | 'John]]>' + 155 | '' + 156 | '' + 157 | '' + 158 | '' + 159 | '' + 160 | '' + 161 | '' + 162 | '' + 163 | '' + 164 | '' + 165 | ']>' + 166 | '' + 167 | '' + 168 | '' + 169 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 170 | '' + 171 | '' + 172 | 'this is a test\nSecond line]]>' + 173 | 'text node' + 174 | '' + 175 | '' + 176 | '&<>&' + 177 | 'text' + 178 | '' 179 | ) 180 | 181 | test 'Test public DTD', -> 182 | 183 | result = '' 184 | data = (chunk) -> 185 | result += chunk 186 | 187 | doc(data) 188 | .dtd('root', 'hello.dtd', 'value') 189 | .up() 190 | .ele('root') 191 | .end() 192 | 193 | eq( 194 | result 195 | 196 | '' + 197 | '' 198 | ) 199 | 200 | test 'Pretty printing', -> 201 | 202 | result = '' 203 | data = (chunk) -> 204 | result += chunk 205 | 206 | doc({ writer: { pretty: true, indent: ' ' } }, data) 207 | .dec() 208 | .ele('root') 209 | .ele('node') 210 | .end() 211 | 212 | eq( 213 | result 214 | 215 | """ 216 | 217 | 218 | 219 | 220 | 221 | """ 222 | ) 223 | -------------------------------------------------------------------------------- /test/callback/indentation.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML using begin() callbacks:', -> 2 | test 'Indentation', -> 3 | 4 | result = '' 5 | data = (chunk, level) -> 6 | result += new Array(level).join('----') + chunk + '\n' 7 | end = () -> 8 | # remove trailing newline 9 | result = result.slice(0, -1) 10 | 11 | doc(data, end) 12 | .dec() 13 | .dtd('root', 'hello.dtd') 14 | .ins('pub_border', 'thin') 15 | .ele('img', 'EMPTY') 16 | .com('Image attributes follow') 17 | .att('img', 'height', 'CDATA', '#REQUIRED') 18 | .att('img', 'visible', '(yes|no)', '#DEFAULT', "yes") 19 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 20 | .not('fs-nt', { pubID: 'FS Network Reader 1.0' }) 21 | .not('fs-nt', { pubID: 'FS Network Reader 1.0', sysID: 'http://my.fs.com/reader' }) 22 | .att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED') 23 | .dat('John') 24 | .ele('node') 25 | .ent('ent', 'my val') 26 | .ent('ent', { sysID: 'http://www.myspec.com/ent' }) 27 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 28 | .ent('ent', { sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 29 | .ent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent', nData: 'entprg' }) 30 | .pent('ent', 'my val') 31 | .pent('ent', { sysID: 'http://www.myspec.com/ent' }) 32 | .pent('ent', { pubID: '-//MY//SPEC ENT//EN', sysID: 'http://www.myspec.com/ent' }) 33 | .ele('nodearr', ['a', 'b']) 34 | .up() 35 | .ele('root') 36 | .ele('xmlbuilder', {'for': 'node-js' }) 37 | .com('CoffeeScript is awesome.') 38 | .nod('repo', {'type': 'git'}, 'git://github.com/oozcitak/xmlbuilder-js.git').up() 39 | .nod('repo', 'git://github.com/oozcitak/xmlbuilder-js.git', {'type': 'git'}).up() 40 | .up() 41 | .ele('cdata') 42 | .att('att', 'val') 43 | .dat('this is a test\nSecond line') 44 | .txt('text node') 45 | .ins('target', 'value') 46 | .up() 47 | .ele('raw') 48 | .raw('&<>&') 49 | .up() 50 | .ele('atttest', { 'att': 'val' }, 'text') 51 | .end() 52 | 53 | eq( 54 | result 55 | """ 56 | 57 | 59 | ---- 60 | ---- 61 | ---- 62 | ---- 63 | ---- 64 | ---- 65 | ---- 66 | ---- 67 | ----John]]> 68 | ---- 69 | ---- 70 | ---- 71 | ---- 72 | ---- 73 | ---- 74 | ---- 75 | ---- 76 | ---- 77 | ---- 78 | ]> 79 | 80 | ---- 81 | -------- 82 | -------- 83 | ------------git://github.com/oozcitak/xmlbuilder-js.git 84 | -------- 85 | -------- 86 | ------------git://github.com/oozcitak/xmlbuilder-js.git 87 | -------- 88 | ---- 89 | ---- 90 | --------this is a test\nSecond line]]> 91 | --------text node 92 | -------- 93 | ---- 94 | ---- 95 | --------&<>& 96 | ---- 97 | ---- 98 | --------text 99 | ---- 100 | 101 | """ 102 | ) 103 | 104 | -------------------------------------------------------------------------------- /test/callback/instructions.coffee: -------------------------------------------------------------------------------- 1 | suite 'Processing Instructions:', -> 2 | 3 | result = '' 4 | data = (chunk, level) -> 5 | result += chunk 6 | 7 | doc(data) 8 | .ins('pre' ,'val1') 9 | .node('test17') 10 | .ins('pi', 'mypi') 11 | . ins({'pi': 'mypi', 'pi2': 'mypi2', 'pi3': null}) 12 | .ins(['pi', 'pi2']) 13 | .up() 14 | .ins('post', 'val2') 15 | .end() 16 | 17 | test 'All forms of ins() usage', -> 18 | eq( 19 | result 20 | 21 | '' + 22 | '' + 23 | '' + 24 | '' + 25 | '' + 26 | '' + 27 | '' 28 | ) 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/callback/object.coffee: -------------------------------------------------------------------------------- 1 | suite 'Creating XML using begin() callbacks:', -> 2 | 3 | result = '' 4 | data = (chunk, level) -> 5 | result += chunk 6 | 7 | test 'From JS object (simple)', -> 8 | obj = 9 | root: 10 | xmlbuilder: 11 | '@for': 'node-js' 12 | repo: 13 | '@type': 'git' 14 | '#text': 'git://github.com/oozcitak/xmlbuilder-js.git' 15 | 16 | result = '' 17 | doc(data).dec().ele(obj).end() 18 | 19 | eq( 20 | result 21 | 22 | '' + 23 | '' + 24 | '' + 25 | 'git://github.com/oozcitak/xmlbuilder-js.git' + 26 | '' + 27 | '' 28 | ) 29 | 30 | test 'From JS object (functions)', -> 31 | obj = 32 | squares: 33 | '#comment': 'f(x) = x^2' 34 | 'data': () -> 35 | ret = for i in [1..5] 36 | { '@x': i, '@y': i * i } 37 | 38 | result = '' 39 | doc(data).dec().ele(obj).end() 40 | 41 | eq( 42 | result 43 | 44 | '' + 45 | '' + 46 | '' + 47 | '' + 48 | '' + 49 | '' + 50 | '' + 51 | '' + 52 | '' 53 | ) 54 | 55 | test 'From JS object (decorators)', -> 56 | obj = 57 | root: 58 | ele: "simple element" 59 | person: 60 | name: "John" 61 | '@age': 35 62 | '?pi': 'mypi' 63 | '#comment': 'Good guy' 64 | '#cdata': 'well formed!' 65 | unescaped: 66 | '#raw': '&<>&' 67 | address: 68 | city: "Istanbul" 69 | street: "End of long and winding road" 70 | contact: 71 | phone: [ "555-1234", "555-1235" ] 72 | id: () -> return 42 73 | details: 74 | '#text': 'classified' 75 | 76 | result = '' 77 | doc(data).ele(obj).end() 78 | 79 | eq( 80 | result 81 | 82 | '' + 83 | 'simple element' + 84 | '' + 85 | 'John' + 86 | '' + 87 | '' + 88 | '' + 89 | '&<>&' + 90 | '
' + 91 | 'Istanbul' + 92 | 'End of long and winding road' + 93 | '
' + 94 | '' + 95 | '555-1234' + 96 | '555-1235' + 97 | '' + 98 | '42' + 99 | '
classified
' + 100 | '
' + 101 | '
' 102 | ) 103 | 104 | test 'From JS object (ignore decorators)', -> 105 | obj = 106 | root: 107 | ele: "simple element" 108 | person: 109 | name: "John" 110 | '@age': 35 111 | '?pi': 'mypi' 112 | '#comment': 'Good guy' 113 | '#cdata': 'well formed!' 114 | unescaped: 115 | '#raw': '&<>&' 116 | address: 117 | city: "Istanbul" 118 | street: "End of long and winding road" 119 | phone: [ 120 | "555-1234" 121 | "555-1235" 122 | ] 123 | id: () -> return 42 124 | details: 125 | '#text': 'classified' 126 | 127 | result = '' 128 | doc({ ignoreDecorators: true, noValidation: true }, data).ele(obj).end() 129 | 130 | eq( 131 | result 132 | 133 | '' + 134 | 'simple element' + 135 | '' + 136 | 'John' + 137 | '<@age>35' + 138 | 'mypi' + 139 | '<#comment>Good guy' + 140 | '<#cdata>well formed!' + 141 | '<#raw>&<>&' + 142 | '
' + 143 | 'Istanbul' + 144 | 'End of long and winding road' + 145 | '
' + 146 | '555-1234' + 147 | '555-1235' + 148 | '42' + 149 | '
<#text>classified
' + 150 | '
' + 151 | '
' 152 | ) 153 | 154 | test 'From JS object (deep nesting)', -> 155 | obj = 156 | root: 157 | one: 158 | '@val': 1 159 | two: 160 | '@val': 2 161 | three: 162 | '@val': 3 163 | four: 164 | '@val': 4 165 | five: 166 | '@val': 5 167 | six: 168 | '@val': 6 169 | ends: 'here' 170 | 171 | result = '' 172 | doc(data).ele(obj).end() 173 | 174 | eq( 175 | result 176 | 177 | '' + 178 | '' + 179 | '' + 180 | '' + 181 | '' + 182 | '' + 183 | '' + 184 | 'here' + 185 | '' + 186 | '' + 187 | '' + 188 | '' + 189 | '' + 190 | '' + 191 | '' 192 | ) 193 | 194 | test 'From JS object (root level)', -> 195 | obj = 196 | myroot: 197 | ele: "simple element" 198 | person: 199 | name: "John" 200 | '@age': 35 201 | address: 202 | city: "Istanbul" 203 | street: "End of long and winding road" 204 | phone: [ 205 | { '#text': "555-1234", '@type': 'home' } 206 | { '#text': "555-1235", '@type': 'mobile' } 207 | ] 208 | id: () -> return 42 209 | 210 | result = '' 211 | doc(data).ele(obj).end() 212 | 213 | eq( 214 | result 215 | 216 | '' + 217 | 'simple element' + 218 | '' + 219 | 'John' + 220 | '
' + 221 | 'Istanbul' + 222 | 'End of long and winding road' + 223 | '
' + 224 | '555-1234' + 225 | '555-1235' + 226 | '42' + 227 | '
' + 228 | '
' 229 | ) 230 | 231 | test 'From JS object (simple array)', -> 232 | obj = [ 233 | "one" 234 | "two" 235 | () -> return "three" 236 | ] 237 | 238 | result = '' 239 | doc(data).ele('root').ele(obj).end() 240 | 241 | eq( 242 | result 243 | 244 | '' + 245 | '' + 246 | '' + 247 | '' + 248 | '' 249 | ) 250 | 251 | test 'From JS object (empty array should produce no nodes)', -> 252 | result = '' 253 | doc(data).ele('root').ele({ item: [] }).end() 254 | 255 | eq( 256 | result 257 | '' 258 | ) 259 | 260 | test 'From JS object (empty array should produce one node if separateArrayItems is set)', -> 261 | result = '' 262 | doc({ separateArrayItems: true }, data).ele('root').ele({ item: [] }).end() 263 | 264 | eq( 265 | result 266 | '' 267 | ) 268 | 269 | test 'From JS object (empty object should produce one node)', -> 270 | result = '' 271 | doc(data).ele('root').ele({ item: {} }).end() 272 | 273 | eq( 274 | result 275 | '' 276 | ) 277 | 278 | test 'From JS object (empty array with empty object should produce one node)', -> 279 | result = '' 280 | doc(data).ele('root').ele({ item: [{}] }).end() 281 | 282 | eq( 283 | result 284 | '' 285 | ) 286 | 287 | test 'From JS object (null should produce no nodes)', -> 288 | result = '' 289 | doc(data).ele('root').ele({ item: null }).end() 290 | 291 | eq( 292 | result 293 | '' 294 | ) 295 | -------------------------------------------------------------------------------- /test/common.coffee: -------------------------------------------------------------------------------- 1 | global.builder = require('../src/index') 2 | global.assert = require('assert') 3 | 4 | global.xml = builder.create 5 | global.doc = builder.begin 6 | global.writer = builder.stringWriter 7 | 8 | global.xpath = require('xpath') 9 | 10 | global.ok = assert.ok 11 | global.eq = assert.strictEqual 12 | global.err = assert.throws 13 | global.noterr = assert.doesNotThrow 14 | global.isan = (obj, type) -> 15 | clas = obj.constructor.name 16 | eq(clas, type) 17 | 18 | global.captureStream = (stream) -> 19 | oldWrite = stream.write 20 | buf = '' 21 | stream.write = (chunk, encoding, callback) -> 22 | buf += chunk.toString() 23 | # oldWrite.apply(stream, arguments) 24 | 25 | return { 26 | unhook: -> 27 | stream.write = oldWrite 28 | captured: -> 29 | buf 30 | } 31 | 32 | global.xmleleend = (arg) -> 33 | builder.create('root', { headless: true }).ele(arg).end() 34 | -------------------------------------------------------------------------------- /test/dom/level1.coffee: -------------------------------------------------------------------------------- 1 | suite 'DOM Level 1:', -> 2 | test 'DOMImplementation', -> 3 | ok( builder.implementation.hasFeature "XML", "1.0" ) 4 | 5 | test 'Document', -> 6 | doc = xml('root').dtd('pubID', 'sysID').doc() 7 | 8 | eq( doc.doctype.type, builder.nodeType.DocType ) 9 | ok( doc.implementation.hasFeature "XML", "1.0" ) 10 | eq( doc.documentElement.type, builder.nodeType.Element ) 11 | eq( doc.documentElement.nodeName, 'root' ) 12 | 13 | test 'Node Types', -> 14 | # we check integer values here to ensure 15 | # strict compliance with the spec 16 | eq( xml('root').nodeType, 1 ) # Element : 1 17 | eq( xml('root').att('att', 'val').attribs['att'].nodeType, 2 ) # Attribute : 2 18 | eq( xml('root').txt('text').children[0].nodeType, 3 ) # Text : 3 19 | eq( xml('root').dat('text').children[0].nodeType, 4 ) # CData : 4 20 | # EntityReference : 5 21 | eq( xml('root').dtd().ent('text', 'val').children[0].nodeType, 6 ) # EntityDeclaration : 6 22 | eq( xml('root').dtd().pent('text', 'val').children[0].nodeType, 6 ) # EntityDeclaration : 6 23 | eq( xml('root').ins('text').children[0].nodeType, 7 ) # ProcessingInstruction : 7 24 | eq( xml('root').com('text').children[0].nodeType, 8 ) # Comment : 8 25 | eq( xml('root').doc().nodeType, 9 ) # Document : 9 26 | eq( xml('root').dtd().nodeType, 10 ) # DocType : 10 27 | # DocumentFragment : 11 28 | eq( xml('root').dtd().not('text', { pubID: 'test' } ).children[0].nodeType, 12 ) # NotationDeclaration : 12 29 | eq( xml('root').doc().children[0].nodeType, 201 ) # Declaration : 201 30 | eq( xml('root').raw('text').children[0].nodeType, 202 ) # Raw : 202 31 | eq( xml('root').dtd().att('img', 'src', 'NOTATION (fs|fs-nt)', '#REQUIRED').children[0].nodeType, 203 ) # AttributeDeclaration : 203 32 | eq( xml('root').dtd().ele('text').children[0].nodeType, 204 ) # ElementDeclaration : 204 33 | # Dummy : 205 34 | 35 | test 'Node', -> 36 | node = xml('root').ele('node') 37 | node.att('att1', 'val1').att('att2', 'val2') 38 | node.ele('child1').up().ele('child2').up().ele('child3') 39 | child = node.children[1] 40 | 41 | eq( node.nodeName, 'node' ) 42 | eq( node.nodeType, builder.nodeType.Element ) 43 | eq( node.parentNode.nodeName, 'root' ) 44 | eq( node.hasChildNodes(), true ) 45 | eq( node.childNodes.length, 3 ) 46 | eq( node.childNodes.item(0).nodeName, 'child1' ) 47 | eq( node.childNodes.item(1).nodeName, 'child2' ) 48 | eq( node.firstChild.nodeName, 'child1' ) 49 | eq( node.lastChild.nodeName, 'child3' ) 50 | eq( child.previousSibling.nodeName, 'child1' ) 51 | eq( child.nextSibling.nodeName, 'child3' ) 52 | eq( child.previousSibling.previousSibling, null ) 53 | eq( child.nextSibling.nextSibling, null ) 54 | eq( node.attributes.length, 2 ) 55 | eq( node.attributes.item(0).name, 'att1' ) 56 | eq( node.attributes.item(1).value, 'val2' ) 57 | eq( node.ownerDocument.type, builder.nodeType.Document ) 58 | 59 | test 'NodeList', -> 60 | node = xml('root') 61 | node.ele('child1').up().ele('child2').up().ele('child3') 62 | 63 | eq( node.childNodes.length, 3 ) 64 | eq( node.childNodes.item(0).nodeName, 'child1' ) 65 | eq( node.childNodes.item(1).nodeName, 'child2' ) 66 | eq( node.childNodes.item(2).nodeName, 'child3' ) 67 | eq( node.childNodes.item(3), null ) 68 | 69 | test 'NamedNodeMap', -> 70 | node = xml('root') 71 | node.att('att1', 'val1').att('att2', 'val2').att('att3', 'val3') 72 | 73 | eq( node.attributes.length, 3 ) 74 | eq( node.attributes.item(0).name, 'att1' ) 75 | eq( node.attributes.item(1).name, 'att2' ) 76 | eq( node.attributes.item(2).name, 'att3' ) 77 | eq( node.attributes.item(0).value, 'val1' ) 78 | eq( node.attributes.item(1).value, 'val2' ) 79 | eq( node.attributes.item(2).value, 'val3' ) 80 | eq( node.attributes.item(3), null ) 81 | 82 | test 'CharacterData', -> 83 | node = xml('root').txt('text').children[0] 84 | 85 | eq( node.data, 'text' ) 86 | eq( node.length, 4 ) 87 | 88 | test 'Attr', -> 89 | node = xml('root') 90 | node.att('att1', 'val1') 91 | 92 | eq( node.attributes.item(0).name, 'att1' ) 93 | eq( node.attributes.item(0).value, 'val1' ) 94 | eq( node.attributes.item(0).specified, true ) 95 | 96 | test 'Element', -> 97 | node = xml('root') 98 | node.att('att1', 'val1') 99 | 100 | eq( node.tagName, 'root' ) 101 | eq( node.getAttribute('att1'), 'val1' ) 102 | eq( node.getAttributeNode('att1').value, 'val1' ) 103 | 104 | test 'DocumentType', -> 105 | dtd = xml('root').dtd() 106 | .ent('text1', 'val1') 107 | .pent('text2', 'val2') 108 | .not('text1', { pubID: 'test1' } ) 109 | .not('text2', { sysID: 'test2' } ) 110 | .not('text3', { sysID: 'test3' } ) 111 | 112 | eq( dtd.name, 'root' ) 113 | eq( dtd.entities.length, 1 ) 114 | eq( dtd.entities.item(0).nodeType, builder.nodeType.EntityDeclaration ) 115 | eq( dtd.entities.item(0).nodeName, 'text1' ) 116 | eq( dtd.entities.item(1), null ) 117 | eq( dtd.notations.length, 3 ) 118 | eq( dtd.notations.item(0).nodeType, builder.nodeType.NotationDeclaration ) 119 | eq( dtd.notations.item(0).nodeName, 'text1' ) 120 | eq( dtd.notations.item(1).nodeType, builder.nodeType.NotationDeclaration ) 121 | eq( dtd.notations.item(1).nodeName, 'text2' ) 122 | eq( dtd.notations.item(3), null ) 123 | 124 | test 'Notation', -> 125 | node = xml('root').dtd().not('text1', { pubID: 'pub', sysID: 'sys' } ).children[0] 126 | eq( node.nodeType, builder.nodeType.NotationDeclaration ) 127 | eq( node.publicId, 'pub' ) 128 | eq( node.systemId, 'sys' ) 129 | 130 | test 'Entity', -> 131 | ent = xml('root').dtd().ent('ent', { pubID: 'pub', sysID: 'sys', nData: 'entprg' }).children[0] 132 | pent = xml('root').dtd().pent('ent', { pubID: 'pub', sysID: 'sys' }).children[0] 133 | eq( ent.nodeType, builder.nodeType.EntityDeclaration ) 134 | eq( ent.publicId, 'pub' ) 135 | eq( ent.systemId, 'sys' ) 136 | eq( ent.notationName, 'entprg' ) 137 | eq( pent.nodeType, builder.nodeType.EntityDeclaration ) 138 | eq( pent.publicId, 'pub' ) 139 | eq( pent.systemId, 'sys' ) 140 | eq( pent.notationName, null ) 141 | 142 | test 'ProcessingInstruction', -> 143 | node = xml('root').ins('target', 'value').children[0] 144 | 145 | eq( node.target, 'target' ) 146 | eq( node.data, 'value' ) 147 | eq( node.value, 'value' ) 148 | -------------------------------------------------------------------------------- /test/dom/level2.coffee: -------------------------------------------------------------------------------- 1 | suite 'DOM Level 2:', -> 2 | test 'DOMImplementation', -> 3 | ok( builder.implementation.hasFeature "Core", "2.0" ) 4 | ok( builder.implementation.hasFeature "XML", "2.0" ) 5 | 6 | test 'Attr', -> 7 | node = xml('root') 8 | node.att('att1', 'val1') 9 | 10 | eq( node.attributes.item(0).ownerElement.nodeName, 'root' ) 11 | 12 | test 'Node', -> 13 | node = xml('root').ele('node') 14 | node.att('att1', 'val1').att('att2', 'val2') 15 | node.ele('child1').up().ele('child2').up().ele('child3') 16 | child = node.children[1] 17 | 18 | ok( node.isSupported("XML", "2.0") ) 19 | eq( node.hasAttributes(), true ) 20 | eq( node.namespaceURI, '' ) 21 | eq( node.prefix, '' ) 22 | eq( node.localName, 'node' ) 23 | 24 | test 'Element', -> 25 | node = xml('root') 26 | node.att('att1', 'val1') 27 | 28 | eq( node.tagName, 'root' ) 29 | eq( node.hasAttribute('att1'), true ) 30 | 31 | test 'DocumentType', -> 32 | dtd = xml('root').dtd('pub', 'sys') 33 | 34 | eq( dtd.publicId, 'pub' ) 35 | eq( dtd.systemId, 'sys' ) 36 | -------------------------------------------------------------------------------- /test/dom/level3.coffee: -------------------------------------------------------------------------------- 1 | suite 'DOM Level 3:', -> 2 | test 'DOMImplementation', -> 3 | ok( builder.implementation.hasFeature "XML", "3.0" ) 4 | 5 | test 'Document', -> 6 | node = xml('root', { version: '1.1', encoding: 'UTF-8', standalone: true }) 7 | 8 | eq( node.doc().xmlEncoding, 'UTF-8' ) 9 | eq( node.doc().xmlVersion, '1.1' ) 10 | eq( node.doc().xmlStandalone, true ) 11 | 12 | test 'Node', -> 13 | node = xml('root').ele('node').txt('text1').txt('text2') 14 | 15 | eq( node.textContent, 'text1text2' ) 16 | 17 | test 'Text', -> 18 | node = xml('root').ele('node').txt('text1').txt('text2') 19 | 20 | eq( node.children[0].wholeText, 'text1text2' ) 21 | -------------------------------------------------------------------------------- /test/guards/attributes.coffee: -------------------------------------------------------------------------------- 1 | suite 'attribute() Guards:', -> 2 | test 'Throw if null attribute (att)', -> 3 | err( 4 | () -> xml('test4', { headless: true }).ele('node').att(null, '') 5 | Error 6 | "Missing attribute name. parent: " 7 | ) 8 | -------------------------------------------------------------------------------- /test/guards/beginwithcallback.coffee: -------------------------------------------------------------------------------- 1 | suite 'begin() with callbacks Guards:', -> 2 | test 'node', -> 3 | testCases = [ 4 | () -> doc(() ->).node() 5 | () -> doc(() ->).node('root1').up().node('root2') 6 | () -> doc(() ->).node('root').ele('node').up().att('att', 'val') 7 | () -> doc(() ->).node('root').dec() 8 | () -> doc(() ->).dtd() 9 | () -> doc(() ->).node('root').dtd('root') 10 | () -> doc(() ->).node('root').up().up() 11 | ] 12 | 13 | results = [ 14 | "Missing node name." 15 | "Document can only have one root node. node: " 16 | "att() can only be used immediately after an ele() call in callback mode. node: " 17 | "declaration() must be the first node." 18 | "Missing root node name." 19 | "dtd() must come before the root node." 20 | "The document node has no parent." 21 | ] 22 | 23 | err( 24 | testCases[i] 25 | Error 26 | results[i] 27 | ) for i in [0..testCases.length-1] 28 | -------------------------------------------------------------------------------- /test/guards/builder.coffee: -------------------------------------------------------------------------------- 1 | suite 'begin() Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml() 5 | Error 6 | "Root element needs a name." 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/cdata.coffee: -------------------------------------------------------------------------------- 1 | suite 'CDATA Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').cdata() 5 | Error 6 | "Missing CDATA text. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/comment.coffee: -------------------------------------------------------------------------------- 1 | suite 'Comment Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').com() 5 | Error 6 | "Missing comment text. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/dtdattlist.coffee: -------------------------------------------------------------------------------- 1 | suite 'DTDAttList Guards:', -> 2 | test 'constructor', -> 3 | testCases = [ 4 | () -> xml('test').dtd().att() 5 | () -> xml('test').dtd().att('ele') 6 | () -> xml('test').dtd().att('ele', 'att') 7 | () -> xml('test').dtd().att('ele', 'att', 'type') 8 | () -> xml('test').dtd().att('ele', 'att', 'type', 'INVALID') 9 | () -> xml('test').dtd().att('ele', 'att', 'type', 'REQUIRED', 'def') 10 | ] 11 | 12 | results = [ 13 | "Missing DTD element name. parent: " 14 | "Missing DTD attribute name. node: , parent: " 15 | "Missing DTD attribute type. node: , parent: " 16 | "Missing DTD attribute default. node: , parent: " 17 | "Invalid default value type; expected: #REQUIRED, #IMPLIED, #FIXED or #DEFAULT. node: , parent: " 18 | "Default value only applies to #FIXED or #DEFAULT. node: , parent: " 19 | ] 20 | 21 | err( 22 | testCases[i] 23 | Error 24 | results[i] 25 | ) for i in [0..testCases.length-1] 26 | 27 | -------------------------------------------------------------------------------- /test/guards/dtdelement.coffee: -------------------------------------------------------------------------------- 1 | suite 'DTDElement Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').dtd().ele() 5 | Error 6 | "Missing DTD element name. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/dtdentity.coffee: -------------------------------------------------------------------------------- 1 | suite 'DTDEntity Guards:', -> 2 | test 'constructor', -> 3 | testCases = [ 4 | () -> xml('test').dtd().ent() 5 | () -> xml('test').dtd().ent('name') 6 | () -> xml('test').dtd().ent('name', { invalid: "obj" }) 7 | () -> xml('test').dtd().ent('name', { pubID: "obj" }) 8 | () -> xml('test').dtd().pent('name', { sysID: "obj", nData: "val" }) 9 | ] 10 | 11 | results = [ 12 | "Missing DTD entity name. parent: " 13 | "Missing DTD entity value. node: , parent: " 14 | "Public and/or system identifiers are required for an external entity. node: , parent: " 15 | "System identifier is required for a public external entity. node: , parent: " 16 | "Notation declaration is not allowed in a parameter entity. node: , parent: " 17 | ] 18 | 19 | err( 20 | testCases[i] 21 | Error 22 | results[i] 23 | ) for i in [0..testCases.length-1] 24 | 25 | -------------------------------------------------------------------------------- /test/guards/dtdnotation.coffee: -------------------------------------------------------------------------------- 1 | suite 'DTDNotation Guards:', -> 2 | test 'constructor', -> 3 | testCases = [ 4 | () -> xml('test').dtd().not() 5 | () -> xml('test').dtd().not('name', { invalid: "obj" }) 6 | ] 7 | 8 | results = [ 9 | "Missing DTD notation name. parent: " 10 | "Public or system identifiers are required for an external entity. node: , parent: " 11 | ] 12 | 13 | err( 14 | testCases[i] 15 | Error 16 | results[i] 17 | ) for i in [0..testCases.length-1] 18 | 19 | -------------------------------------------------------------------------------- /test/guards/element.coffee: -------------------------------------------------------------------------------- 1 | suite 'XMLElement Guards:', -> 2 | test 'constructor and removeAttribute', -> 3 | testCases = [ 4 | () -> xml('test').nod() 5 | () -> xml('test').ele('node').removeAttribute() 6 | ] 7 | 8 | results = [ 9 | "Missing element name. parent: " 10 | "Missing attribute name. parent: " 11 | ] 12 | 13 | err( 14 | testCases[i] 15 | Error 16 | results[i] 17 | ) for i in [0..testCases.length-1] 18 | 19 | -------------------------------------------------------------------------------- /test/guards/instruction.coffee: -------------------------------------------------------------------------------- 1 | suite 'CDATA Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').ins() 5 | Error 6 | "Missing instruction target. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/node.coffee: -------------------------------------------------------------------------------- 1 | suite 'Node Guards:', -> 2 | test 'Invalid operations', -> 3 | testCases = [ 4 | () -> xml('test').insertBefore() 5 | () -> xml('test').insertAfter() 6 | () -> xml('test').remove() 7 | () -> xml('test').up() 8 | () -> xml('test').ele('first').prev() 9 | () -> xml('test').ele('first').up().ele('last').next() 10 | () -> xml('root').ele([]) 11 | ] 12 | 13 | results = [ 14 | "Cannot insert elements at root level. parent: " 15 | "Cannot insert elements at root level. parent: " 16 | "Cannot remove the root element. parent: " 17 | "The root node has no parent. Use doc() if you need to get the document object." 18 | "Already at the first node. parent: " 19 | "Already at the last node. parent: " 20 | "Could not create any elements with: . parent: " 21 | ] 22 | 23 | err( 24 | testCases[i] 25 | Error 26 | results[i] 27 | ) for i in [0..testCases.length-1] 28 | 29 | -------------------------------------------------------------------------------- /test/guards/raw.coffee: -------------------------------------------------------------------------------- 1 | suite 'CDATA Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').raw() 5 | Error 6 | "Missing raw text. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/guards/stringify.coffee: -------------------------------------------------------------------------------- 1 | suite 'Stringify Guards:', -> 2 | test 'defaults', -> 3 | testCases = [ 4 | () -> xml('test').com('--') 5 | () -> xml('test').ins('pi', '?>') 6 | () -> xml('test', { encoding: "A#" }) 7 | () -> xml('test', { version: "A.B" }) 8 | ] 9 | 10 | results = [ 11 | "Comment text cannot contain double-hypen: --" 12 | "Invalid processing instruction value: ?>" 13 | "Invalid encoding: A" 14 | "Invalid version number: A.B" 15 | ] 16 | 17 | err( 18 | testCases[i] 19 | Error 20 | results[i] 21 | ) for i in [0..testCases.length-1] 22 | 23 | -------------------------------------------------------------------------------- /test/guards/text.coffee: -------------------------------------------------------------------------------- 1 | suite 'Text Guards:', -> 2 | test 'constructor', -> 3 | err( 4 | () -> xml('test').txt() 5 | Error 6 | "Missing element text. parent: " 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /test/issues/117.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #117 (0 as element value) in create()', -> 3 | eq( 4 | xml('root').ele('test', 0).end() 5 | '0' 6 | ) 7 | 8 | test 'Issue #117 (0 as element value) in begin()', -> 9 | r = '' 10 | doc((c) -> r += c).ele('root').ele('test', 0).end() 11 | 12 | eq( 13 | r 14 | '0' 15 | ) 16 | 17 | -------------------------------------------------------------------------------- /test/issues/120.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'toString or end with arguments sets writer forever. Issue 157', -> 3 | str1 = xml('root').ins('a', 'b').ele('xmlbuilder').end() 4 | xml('root').ins('a', 'b').ele('xmlbuilder').end({ pretty: true, spacebeforeslash: ' ' }) 5 | str2 = xml('root').ins('a', 'b').ele('xmlbuilder').end() 6 | eq(str1, str2) 7 | -------------------------------------------------------------------------------- /test/issues/122.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Get node elements by name. Issue 122', -> 3 | 4 | doc = xml('clients') 5 | .ele('client').att('city', '').up() 6 | .ele('client').att('city', 'CA').up() 7 | .ele('client').att('city', 'FL').up() 8 | .ele('client').up() 9 | .doc() 10 | 11 | nodes = xpath.select('//client', doc) 12 | nodes.forEach (node) -> 13 | node.att("city","NY") 14 | 15 | eq(nodes[0].attributes.item(0).value, "NY") 16 | eq(nodes[1].attributes.item(0).value, "NY") 17 | eq(nodes[2].attributes.item(0).value, "NY") 18 | eq(nodes[3].attributes.item(0).value, "NY") 19 | 20 | -------------------------------------------------------------------------------- /test/issues/147.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #147 (unicode characters)', -> 3 | eq( 4 | xml('root').ele('test', '💩💩💩💩').end() 5 | '💩💩💩💩' 6 | ) 7 | 8 | test 'Issue #147 (invalid character replacement) - 1', -> 9 | eq( 10 | xml('root', { invalidCharReplacement: '' }).ele('hello\u0000').txt('\u0001world').end() 11 | 'world' 12 | ) 13 | 14 | test 'Issue #147 (invalid character replacement) - 2', -> 15 | eq( 16 | xml('root', { invalidCharReplacement: 'x' }).ele('hello\u0000').txt('\u0001world').end() 17 | 'xworld' 18 | ) 19 | 20 | test 'Issue #147 (invalid character replacement) - 3', -> 21 | obj = 22 | root: 23 | 'node\x00': 'text\x08content' 24 | 25 | eq( 26 | xml(obj, { invalidCharReplacement: (c) -> if c is '\x00' then '' else '_' }).end() 27 | 'text_content' 28 | ) 29 | -------------------------------------------------------------------------------- /test/issues/157.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Option to add space before closing tag. Issue 157', -> 3 | eq( 4 | xml('root') 5 | .dtd('hello.dtd') 6 | .ins('pub_border', 'thin') 7 | .ele('img', 'EMPTY') 8 | .att('img', 'height', 'CDATA', '#REQUIRED') 9 | .not('fs', { sysID: 'http://my.fs.com/reader' }) 10 | .ent('ent', 'my val') 11 | .pent('ent', 'my val') 12 | .root() 13 | .ins('a', 'b') 14 | .ele('xmlbuilder') 15 | .end({ pretty: true, spacebeforeslash: ' ' }) 16 | 17 | """ 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | ] > 27 | 28 | 29 | 30 | 31 | """ 32 | ) 33 | 34 | test 'Fragment', -> 35 | eq( 36 | xml('root').toString({ spacebeforeslash: true }) 37 | 38 | '' 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /test/issues/159.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'multiple elements with different values. Issue 159', -> 3 | obj = { 4 | 'mdui:UIInfo': { 5 | "mdui:DisplayName": [ 6 | { "@xml:lang": "de", "#text": "AAA" } 7 | { "@xml:lang": "en", "#text": "BBB" } 8 | ] 9 | } 10 | } 11 | 12 | eq( 13 | xml(obj, { headless: true }).end() 14 | 15 | '' + 16 | 'AAA' + 17 | 'BBB' + 18 | '' 19 | ) 20 | 21 | 22 | test 'nested array. Issue 159', -> 23 | obj = { 24 | "category": { 25 | "@category-id": "twe-root", 26 | "display-name": { 27 | "#text": "Root", 28 | "@xml:lang": "x-default" 29 | }, 30 | "description": { 31 | "#text": "Master Catalogue for Treasury Wines", 32 | "@xml:lang": "x-default" 33 | }, 34 | "online-flag": { 35 | "#text": true 36 | }, 37 | "attribute-groups": [{ 38 | "attribute-group": { 39 | "@group-id": "wine", 40 | "display-name": { 41 | "#text": "Wine Attributes", 42 | "@xml:lang": "x-default" 43 | }, 44 | "attribute": [{ 45 | "@attribute-id": "wineContentChannels", 46 | "@system": false 47 | }, 48 | { 49 | "@attribute-id": "wineTastingNotesPDF", 50 | "@system": false 51 | }, 52 | { 53 | "@attribute-id": "wineCOGS", 54 | "@system": false 55 | }, 56 | { 57 | "@attribute-id": "wineCollection", 58 | "@system": false 59 | }, 60 | { 61 | "@attribute-id": "wineType", 62 | "@system": false 63 | }, 64 | { 65 | "@attribute-id": "wineVariety", 66 | "@system": false 67 | }, 68 | { 69 | "@attribute-id": "wineBottleType", 70 | "@system": false 71 | }, 72 | { 73 | "@attribute-id": "wineVintage", 74 | "@system": false 75 | } 76 | ] 77 | } 78 | }, 79 | { 80 | "attribute-group": { 81 | "@group-id": "coreProduct", 82 | "display-name": { 83 | "#text": "Core Product Attributes", 84 | "@xml:lang": "x-default" 85 | }, 86 | "attribute": [{ 87 | "@attribute-id": "csrOnly", 88 | "@system": false 89 | }] 90 | } 91 | } 92 | ] 93 | } 94 | } 95 | 96 | eq( 97 | xml(obj, { headless: true }).end({ pretty: true }) 98 | 99 | """ 100 | 101 | Root 102 | Master Catalogue for Treasury Wines 103 | true 104 | 105 | 106 | Wine Attributes 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Core Product Attributes 120 | 121 | 122 | 123 | 124 | """ 125 | ) 126 | 127 | test 'multiple paragraphs. Issue 159', -> 128 | obj = { 129 | 'text': { 130 | '#text': [ 131 | { 'paragraph': 'Symbolic text number 1' } 132 | { 'table': '' } 133 | { 'paragraph': '** Symbolic text number 2 **' } 134 | ] 135 | } 136 | } 137 | 138 | eq( 139 | xml(obj, { headless: true }).end({ pretty: true }) 140 | 141 | """ 142 | 143 | Symbolic text number 1 144 | 145 | ** Symbolic text number 2 ** 146 | 147 | """ 148 | ) -------------------------------------------------------------------------------- /test/issues/171.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Inline elements within Text Decorator throws error. Issue 171', -> 3 | obj = { 4 | pc: { 5 | '#text': [ 6 | 'Hello ', 7 | { mrk: { '#text': 'World', '@id': 'm1', '@type': 'term' } }, 8 | '!', 9 | ], 10 | '@id': 1, 11 | }, 12 | } 13 | 14 | eq( 15 | xml(obj, { headless: true }).end() 16 | 17 | '' + 18 | 'Hello ' + 19 | 'World' + 20 | '!' + 21 | '' 22 | ) 23 | 24 | test 'Nested mixed-content', -> 25 | obj = { 26 | pc: { 27 | '#text': [ 28 | 'Hello ', 29 | { nested: { nestedagain: { '#text': 'World' }, '#text': '!' } } 30 | '!', 31 | ], 32 | '@id': 1, 33 | }, 34 | } 35 | 36 | eq( 37 | xml(obj, { headless: true }).end() 38 | 39 | '' + 40 | 'Hello ' + 41 | 'World!' + 42 | '!' + 43 | '' 44 | ) -------------------------------------------------------------------------------- /test/issues/175.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'importDocument overwrites parent declaration: Issue 175', -> 3 | 4 | doc = xml({ factura: {'@id':'comprabante','@version':'2.1.0' }}, {encoding: 'UTF-8'}) 5 | .ele('identificacionComprador', '1303963712') 6 | doc.ele('node') 7 | 8 | for i in [1..3] 9 | person = xml('person').att('id', i) 10 | doc.importDocument(person) 11 | 12 | eq( 13 | doc.end() 14 | '' + 15 | '' + 16 | '' + 17 | '1303963712' + 18 | '' + 19 | '' + 20 | '' + 21 | '' + 22 | '' + 23 | '' 24 | ) 25 | 26 | -------------------------------------------------------------------------------- /test/issues/176.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #176 (valueOf fails for Object.Create(null)', -> 3 | obj = Object.create(null) 4 | obj.root = 'node' 5 | 6 | eq( 7 | xml(obj).end() 8 | '' + 9 | 'node' 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /test/issues/187.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'skipNullNodes returning null node as undefined node (JSON): Issue 187', -> 3 | 4 | dataJson = [ 5 | { user: 'barney', age: 36, active: true, city: null }, 6 | { user: 'fred', age: 40, active: false, city: '' }, 7 | { user: 'pebbles', age: 1, active: true, city: 'Dubai' }, 8 | ] 9 | 10 | doc = xml(dataJson, { headless: true, skipNullNodes: true, separateArrayItems: true }) 11 | 12 | eq( 13 | doc.end() 14 | 'barney' + 15 | '36' + 16 | 'true' + 17 | 'fred' + 18 | '40' + 19 | 'false' + 20 | '' + 21 | 'pebbles' + 22 | '1' + 23 | 'true' + 24 | 'Dubai' 25 | ) 26 | 27 | 28 | test 'skipNullNodes returning null node as undefined node (JSON with root): Issue 187', -> 29 | 30 | dataJson = root: [ 31 | { user: 'barney', age: 36, active: true, city: null }, 32 | { user: 'fred', age: 40, active: false, city: '' }, 33 | { user: 'pebbles', age: 1, active: true, city: 'Dubai' }, 34 | ] 35 | 36 | doc = xml(dataJson, { headless: true, skipNullNodes: true, separateArrayItems: true }) 37 | 38 | eq( 39 | doc.end() 40 | '' + 41 | 'barney' + 42 | '36' + 43 | 'true' + 44 | 'fred' + 45 | '40' + 46 | 'false' + 47 | '' + 48 | 'pebbles' + 49 | '1' + 50 | 'true' + 51 | 'Dubai' + 52 | '' 53 | ) 54 | 55 | 56 | test 'skipNullNodes returning null node as undefined node: Issue 187', -> 57 | 58 | dataJson = [ 59 | { user: 'barney', age: 36, active: true, city: null }, 60 | { user: 'fred', age: 40, active: false, city: '' }, 61 | { user: 'pebbles', age: 1, active: true, city: 'Dubai' }, 62 | ] 63 | 64 | doc = xml('root', { headless: true, skipNullNodes: true, separateArrayItem: true }) 65 | .ele('user', 'barney').up() 66 | .ele('age', 36).up() 67 | .ele('active', true).up() 68 | .ele('city', null).up() 69 | .ele('user', 'fred').up() 70 | .ele('age', 40).up() 71 | .ele('active', false).up() 72 | .ele('city', '').up() 73 | .ele('user', 'pebbles').up() 74 | .ele('age', 1).up() 75 | .ele('active', true).up() 76 | .ele('city', 'Dubai').up() 77 | 78 | eq( 79 | doc.end() 80 | '' + 81 | 'barney' + 82 | '36' + 83 | 'true' + 84 | 'fred' + 85 | '40' + 86 | 'false' + 87 | '' + 88 | 'pebbles' + 89 | '1' + 90 | 'true' + 91 | 'Dubai' + 92 | '' 93 | ) 94 | 95 | -------------------------------------------------------------------------------- /test/issues/190.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Empty Array from JSON generates Element. Issue 190. `item: []` should produce no nodes.', -> 3 | eq( 4 | xmleleend { item: [] } 5 | '' 6 | ) 7 | 8 | test 'Empty Array from JSON generates Element. Issue 190. `item: {}` should produce one node.', -> 9 | eq( 10 | xmleleend { item: {} } 11 | '' 12 | ) 13 | 14 | test 'Empty Array from JSON generates Element. Issue 190. `item: [{}]` should produce one node.', -> 15 | eq( 16 | xmleleend { item: [{}] } 17 | '' 18 | ) 19 | -------------------------------------------------------------------------------- /test/issues/193.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'use of writer modification in .end(). Issue 193', -> 3 | 4 | newIndent = (node, options, level) -> 5 | if (node.parent?.name is "p" and options.state is builder.writerState.OpenTag) or (node.name is "p" and options.state is builder.writerState.CloseTag) 6 | return '' 7 | else 8 | return @_indent node, options, level 9 | 10 | newEndline = (node, options, level) -> 11 | if (node.parent?.name is "p" and options.state is builder.writerState.CloseTag) or (node.name is "p" and options.state is builder.writerState.OpenTag) 12 | return '' 13 | else 14 | return @_endline node, options, level 15 | 16 | eq( 17 | xml('html', { headless: true }) 18 | .ele('p', { 'style': 'S1' }) 19 | .ele('span', { 'style': 'S1' }).txt(1) 20 | .end(builder.stringWriter({ writer: { indent: newIndent, endline: newEndline }, pretty: true } )) 21 | 22 | """ 23 | 24 |

1

25 | 26 | """ 27 | ) 28 | 29 | test 'use of writer modification in .end() with openNode and closeNode. Issue 193', -> 30 | 31 | newOpenNode = (node, options, level) -> 32 | if (node.name is "p") 33 | options.user.oldPretty = options.pretty 34 | options.pretty = false 35 | 36 | @_openNode node, options, level 37 | 38 | newCloseNode = (node, options, level) -> 39 | if (node.name is "p") 40 | options.pretty = options.user.oldPretty 41 | 42 | @_closeNode node, options, level 43 | 44 | eq( 45 | xml('p', { headless: true }) 46 | .ele('span') 47 | .ele('span', 'sometext').up() 48 | .ele('span', 'sometext2') 49 | .end(builder.stringWriter({ writer: { openNode: newOpenNode, closeNode: newCloseNode }, pretty: true } )) 50 | 51 | """ 52 |

sometextsometext2

53 | """ 54 | ) -------------------------------------------------------------------------------- /test/issues/194.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Proper line breaks on Windows? (\r\n in text nodes). Issue 194', -> 3 | 4 | escapeFunc = (str) -> 5 | str.replace(/&/g, '&') 6 | .replace(//g, '>') 8 | .replace(/\r/g, ' ') 9 | .replace(/\r?\n/gm, '\r\n') # normalize newlines 10 | 11 | eq( 12 | xml('root', { headless: true, stringify: { textEscape: escapeFunc } }) 13 | .ele("foo", "line\nbreak") 14 | .end( { pretty: true, newline: "\r\n" } ) 15 | 16 | '\r\n' + 17 | ' line\r\nbreak\r\n' + 18 | '' 19 | ) 20 | -------------------------------------------------------------------------------- /test/issues/195.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Missing callbacks if {pretty: true} and only one method called on element?. Issue 195', -> 3 | 4 | debugStr = '' 5 | root = xml('root') 6 | root.ele('textDirect', null, '[1]') 7 | root.ele('textSingle').txt('[2]') 8 | root.ele('rawSingle').raw('[3]') 9 | root.ele('textDirectDummy', null, '[4]').dummy() 10 | root.ele('textDummy').txt('[5]').dummy() 11 | root.ele('rawDummy').raw('[6]').dummy() 12 | root.ele('twoTextNodes').txt('[7]').txt('[7]') 13 | root.ele('twoRaw').raw('[8]').raw('[8]') 14 | root.ele('rawAndTextNode').raw('[9]').txt('[9]') 15 | root.end(builder.stringWriter( 16 | pretty: true 17 | writer: 18 | raw: (node, options, level) -> 19 | debugStr += "#{ options.indent.repeat(level) }RAW #{ node.value }\n" 20 | @_raw node, options, level 21 | text: (node, options, level) -> 22 | debugStr += "#{ options.indent.repeat(level) }TEXT #{ node.value }\n" 23 | @_text node, options, level 24 | element: (node, options, level) -> 25 | debugStr += "#{ options.indent.repeat(level) }ELEMENT #{ node.name }\n" 26 | @_element node, options, level 27 | )) 28 | 29 | # trim last newline 30 | debugStr = debugStr.slice(0, -1) 31 | 32 | eq( 33 | debugStr 34 | 35 | """ 36 | ELEMENT root 37 | ELEMENT textDirect 38 | TEXT [1] 39 | ELEMENT textSingle 40 | TEXT [2] 41 | ELEMENT rawSingle 42 | RAW [3] 43 | ELEMENT textDirectDummy 44 | TEXT [4] 45 | ELEMENT textDummy 46 | TEXT [5] 47 | ELEMENT rawDummy 48 | RAW [6] 49 | ELEMENT twoTextNodes 50 | TEXT [7] 51 | TEXT [7] 52 | ELEMENT twoRaw 53 | RAW [8] 54 | RAW [8] 55 | ELEMENT rawAndTextNode 56 | RAW [9] 57 | TEXT [9] 58 | """ 59 | ) 60 | -------------------------------------------------------------------------------- /test/issues/196.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Are attributes really nodes and should therefore invoke openNode() and closeNode() callbacks?. Issue 196', -> 3 | 4 | newAttribute = (att, options, level) -> 5 | r = @_attribute att, options, level 6 | if options.user.openflag then r = "BEGINATT:" + r 7 | if options.user.closeflag then r = r + ":ENDATT" 8 | return r 9 | 10 | newOpenAttribute = (att, options, level) -> 11 | options.user.openflag = att.name is "att1" 12 | 13 | newCloseAttribute = (att, options, level) -> 14 | options.user.closeflag = att.name is "att1" 15 | 16 | eq( 17 | xml('root', { headless: true }) 18 | .ele('item', { 'att1': 'val1', 'att2': 'val2' }) 19 | .end(builder.stringWriter({ writer: { attribute: newAttribute, openAttribute: newOpenAttribute, closeAttribute: newCloseAttribute }, pretty: true } )) 20 | 21 | """ 22 | 23 | 24 | 25 | """ 26 | ) 27 | -------------------------------------------------------------------------------- /test/issues/208.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | 4 | suite 'Tests specific to issues:', -> 5 | test 'Writer events not triggered: Issue 208', -> 6 | 7 | errorReportObj = error: 8 | code: 500 9 | message: "failed" 10 | xmlPath = path.join __dirname, '208.xml' 11 | stream = fs.createWriteStream(xmlPath, { flags : 'w' }) 12 | writer = builder.streamWriter(stream, { pretty: true, allowEmpty: false}) 13 | builder.create(errorReportObj, { 14 | encoding: 'utf-8', 15 | }).end(writer) 16 | stream.end() 17 | 18 | stream.on('end', () -> 19 | eq( 20 | fs.readFileSync(xmlPath, 'utf8') 21 | 22 | """ 23 | 24 | 25 | 500 26 | failed 27 | 28 | """ 29 | ) 30 | ) 31 | 32 | -------------------------------------------------------------------------------- /test/issues/208.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 500 4 | failed 5 | -------------------------------------------------------------------------------- /test/issues/213.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #213: importDocument created from begin()', -> 3 | boldDoc = xml('b').text('Hello World') 4 | emptyDoc = doc().importDocument(boldDoc) 5 | main = xml('p', { headless: true }).importDocument(emptyDoc) 6 | 7 | eq( 8 | main.end() 9 | '

Hello World

' 10 | ) 11 | 12 | -------------------------------------------------------------------------------- /test/issues/222.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #222: Cant remove attribute heired from root', -> 3 | 4 | uuid = "24ff5e22-09af-42cc-aaf6-b475137e6304" 5 | xml = builder.begin({ encoding: 'utf-8' }).ele({ 6 | "AuthnRequest": { 7 | "@ID": uuid, 8 | "@Version": "2.0", 9 | "@IssueInstant": "2019-07-28T18:02:19.511Z", 10 | "@Destination": "https://autenticacao.gov.pt/fa/Default.aspx", 11 | "@ProtocolBinding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", 12 | "@AssertionConsumerServiceURL": "http://clav-auth.di.uminho.pt/assertion", 13 | "@ProviderName": "CLAV", 14 | "@xmlns":"urn:oasis:names:tc:SAML:2.0:protocol", 15 | "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", 16 | "@xmlns:xsd":"http://www.w3.org/2001/XMLSchema", 17 | "@Consent":"urn:oasis:names:tc:SAML:2.0:consent:unspecified" 18 | } 19 | }).ele( 20 | "Issuer", { 21 | "xmlns": "urn:oasis:names:tc:SAML:2.0:assertion" 22 | }, "http://clav-auth.di.uminho.pt" 23 | ).up().ele( 24 | "Extensions" 25 | ).ele( 26 | "fa:RequestedAttributes", { 27 | "xmlns:fa": "http://autenticacao.cartaodecidadao.pt/atributos" 28 | } 29 | ).ele( 30 | "fa:RequestedAttribute", { 31 | "Name": "http://interop.gov.pt/MDC/Cidadao/NIC", "NameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" 32 | } 33 | ) 34 | .end({ pretty: true }) 35 | 36 | eq( 37 | xml 38 | 39 | """ 40 | 41 | http://clav-auth.di.uminho.pt 42 | 43 | 44 | 45 | 46 | 47 | 48 | """ 49 | ) 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/issues/235.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | 4 | suite 'Tests specific to issues:', -> 5 | test 'Issue #235: Maximum call stack size exceeded while create XML', -> 6 | 7 | jsonPath = path.join __dirname, '235.json' 8 | jsonText = fs.readFileSync jsonPath, { encoding: 'utf8' } 9 | replacedText = jsonText.replace /\$ref/g, 'ref' 10 | 11 | err( 12 | () => builder.create(JSON.parse(jsonText)) 13 | ) 14 | 15 | noterr( 16 | () => builder.create(JSON.parse(replacedText)) 17 | ) 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/issues/239.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | 4 | suite 'Tests specific to issues:', -> 5 | test 'Issue #239: Not possible to build a list of non-unique elements non-contiguously with JSON', -> 6 | 7 | obj = root: 8 | '#text1': [ 9 | { node: 'value1' } 10 | { node: 'value2' } 11 | ] 12 | '#comment': 'comment node' 13 | '#text2': [ 14 | { node: 'value3' } 15 | { node: 'value4' } 16 | ] 17 | 18 | doc = xml(obj, { headless: true }) 19 | 20 | eq( 21 | doc.end({ pretty: true }) 22 | """ 23 | 24 | value1 25 | value2 26 | 27 | value3 28 | value4 29 | 30 | """ 31 | ) -------------------------------------------------------------------------------- /test/issues/249.coffee: -------------------------------------------------------------------------------- 1 | path = require('path') 2 | fs = require('fs') 3 | 4 | suite 'Tests specific to issues:', -> 5 | test 'Issue #249: separateArrayItems ', -> 6 | 7 | obj = p: 8 | "@someattr": "something" 9 | '#text': [ 10 | { span: { "@someattr2": "something2", "#text": "line1" } } 11 | { br: "" } 12 | { span: { "@someattr2": "something2", "#text": "line2" } } 13 | ] 14 | 15 | doc = xml(obj, { headless: true }) 16 | 17 | eq( 18 | doc.end({ pretty: true }) 19 | """ 20 |

21 | line1 22 |
23 | line2 24 |

25 | """ 26 | ) -------------------------------------------------------------------------------- /test/issues/93.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #93 (Auto-generating arrays)', -> 3 | obj = 4 | root: 5 | Title: 6 | '@lang': 'eng' 7 | '#text': 'Show Title' 8 | Cast: [ 9 | { 10 | '@role': 'Host' 11 | '#text': 'Interview Person' 12 | } 13 | { 14 | '@role': 'Guest' 15 | '#text': 'Guest Person' 16 | } 17 | ] 18 | Category: [ 19 | { 20 | '@lang': 'eng' 21 | '@type': 'B23' 22 | '#text': 'Game Show' 23 | } 24 | { 25 | '@lang': 'eng' 26 | '@type': 'C34' 27 | '#text': 'Family' 28 | } 29 | { 30 | '@lang': 'eng' 31 | '@type': 'B23' 32 | '#text': 'Topical' 33 | } 34 | ] 35 | 36 | eq( 37 | xml(obj).end() 38 | '' + 39 | '' + 40 | 'Show Title' + 41 | 'Interview Person' + 42 | 'Guest Person' + 43 | 'Game Show' + 44 | 'Family' + 45 | 'Topical' + 46 | '' 47 | ) 48 | 49 | -------------------------------------------------------------------------------- /test/issues/96.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Issue #96 (Seperate array items)', -> 3 | obj = 4 | data: 5 | article_data: [ 6 | { article: 'somedata' }, 7 | { article: 'somedata' } 8 | ] 9 | 10 | eq( 11 | xml(obj, { separateArrayItems: true }).end() 12 | '' + 13 | '' + 14 | '' + 15 | '
somedata
' + 16 | '
somedata
' + 17 | '
' + 18 | '
' 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /test/issues/97.coffee: -------------------------------------------------------------------------------- 1 | obj = 2 | root: 3 | '@att': 'attribute value with & and &' 4 | '#text': 'XML entities for ampersand are & and &.' 5 | 6 | suite 'Tests specific to issues:', -> 7 | test 'Issue #97 (No double encoding)', -> 8 | eq( 9 | xml(obj, { noDoubleEncoding: true }).end() 10 | '' + 11 | '' + 12 | 'XML entities for ampersand are & and &#38;.' + 13 | '' 14 | ) 15 | 16 | test 'Issue #97 (Double encoding - default behavior)', -> 17 | eq( 18 | xml(obj).end() 19 | '' + 20 | '' + 21 | 'XML entities for ampersand are &amp; and &#38;.' + 22 | '' 23 | ) 24 | 25 | -------------------------------------------------------------------------------- /test/issues/encoding.coffee: -------------------------------------------------------------------------------- 1 | suite 'Tests specific to issues:', -> 2 | test 'Encoding regex', -> 3 | eq( 4 | xml('root', {encoding: 'UTF-8'}).end() 5 | '' 6 | ) 7 | 8 | err( 9 | () -> xml('root', {encoding: 'A------------------------------------!'}) 10 | /Invalid encoding: A------------------------------------!/ 11 | ) 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/xpath/simple.coffee: -------------------------------------------------------------------------------- 1 | suite 'XPath:', -> 2 | test 'Evaluate', -> 3 | doc = xml('book').ele('title').txt('Harry Potter').doc() 4 | nodes = xpath.evaluate('//title', doc, null, xpath.XPathResult.ANY_TYPE, null).nodes 5 | 6 | eq(nodes[0].localName, "title") 7 | eq(nodes[0].firstChild.data, "Harry Potter") 8 | eq(nodes[0].toString(), "Harry Potter") 9 | 10 | test 'Select', -> 11 | doc = builder.begin() 12 | .ins('book', 'title="Harry Potter"') 13 | .ins('series', 'title="Harry Potter"') 14 | .ins('series', 'books="7"') 15 | .ele('root') 16 | .com('This is a great book') 17 | .ele('title') 18 | .txt('Harry Potter') 19 | .document() 20 | 21 | nodes = xpath.select('//title', doc) 22 | nodes2 = xpath.select('//node()', doc) 23 | pis = xpath.select("/processing-instruction('series')", doc) 24 | 25 | eq(nodes[0].localName, 'title') 26 | eq(nodes[0].firstChild.data, 'Harry Potter') 27 | eq(nodes[0].toString(), 'Harry Potter') 28 | 29 | eq(nodes2.length, 7) 30 | 31 | eq(pis.length, 2) 32 | eq(pis[1].data, 'books="7"') 33 | 34 | test 'Select single node', -> 35 | doc = xml('book').ele('title').txt('Harry Potter').doc() 36 | eq(xpath.select('//title[1]', doc)[0].localName, 'title') 37 | 38 | test 'Select text node', -> 39 | doc = xml('book').ele('title').txt('Harry').up().ele('title').txt('Potter').doc() 40 | eq(xpath.select('local-name(/book)', doc), 'book') 41 | eq(xpath.select('//title/text()', doc).toString(), 'Harry,Potter') 42 | 43 | test 'Select number value', -> 44 | doc = xml('book').ele('title').txt('Harry').up().ele('title').txt('Potter').doc() 45 | eq(xpath.select('count(//title)', doc), 2) 46 | 47 | test 'Select attribute', -> 48 | doc = xml('author').att('name', 'J. K. Rowling').doc() 49 | author = xpath.select1('/author/@name', doc).value 50 | eq(author, 'J. K. Rowling'); 51 | 52 | test 'Select with multiple predicates', -> 53 | doc = xml('characters') 54 | .ele('character', { name: "Snape", sex: "M", age: "50" }).up() 55 | .ele('character', { name: "McGonnagal", sex: "F", age: "65" }).up() 56 | .doc() 57 | 58 | characters = xpath.select('/*/character[@sex = "M"][@age > 40]/@name', doc) 59 | 60 | eq(characters.length, 1) 61 | eq(characters[0].textContent, 'Snape') 62 | --------------------------------------------------------------------------------