Skip to content

Commit

Permalink
Apply namesapces from ns attributes from JS objects. Fixes #13
Browse files Browse the repository at this point in the history
  • Loading branch information
oozcitak committed Mar 30, 2020
1 parent 73272b3 commit 999e688
Show file tree
Hide file tree
Showing 12 changed files with 139 additions and 52 deletions.
55 changes: 48 additions & 7 deletions src/builder/XMLBuilderImpl.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {
XMLBuilderOptions, XMLBuilder, AttributesObject, ExpandObject,
WriterOptions, XMLSerializedValue, DTDOptions,
DefaultBuilderOptions, PIObject, DocumentWithSettings, XMLWriterOptions,
DefaultBuilderOptions, PIObject, DocumentWithSettings, XMLWriterOptions,
JSONWriterOptions, ObjectWriterOptions, MapWriterOptions
} from "../interfaces"
import {
applyDefaults, isObject, isString, isFunction, isMap, isArray, isEmpty,
getValue, forEachObject, forEachArray, isSet
} from "@oozcitak/util"
import { XMLWriter, MapWriter, ObjectWriter, JSONWriter } from "../writers"
import { Document, Node, Element } from "@oozcitak/dom/lib/dom/interfaces"
import { createParser, throwIfParserError } from "./dom"
import { Document, Node, Element, Attr } from "@oozcitak/dom/lib/dom/interfaces"
import { Guard } from "@oozcitak/dom/lib/util"
import { namespace_extractQName, tree_index } from "@oozcitak/dom/lib/algorithm"

import {
namespace_extractQName, tree_index, create_element
} from "@oozcitak/dom/lib/algorithm"
import { createParser, throwIfParserError } from "./dom"
import { namespace as infraNamespace } from "@oozcitak/infra"
/**
* Represents a wrapper that extends XML nodes to implement easy to use and
* chainable document builder methods.
Expand Down Expand Up @@ -184,7 +186,7 @@ export class XMLBuilderImpl implements XMLBuilder {
}

// create a child element node
const childNode = (namespace !== undefined && namespace !== null?
const childNode = (namespace !== undefined && namespace !== null ?
this._doc.createElementNS(namespace, name) :
this._doc.createElement(name)
)
Expand Down Expand Up @@ -259,8 +261,47 @@ export class XMLBuilderImpl implements XMLBuilder {
return this
}

const ele = this.node as Element
if (!Guard.isElementNode(this.node)) {
throw new Error("An attribute can only be assigned to an element node.")
}
let ele = this.node as Element
[namespace, name] = this._extractNamespace(namespace, name, false)
const [prefix, localName] = namespace_extractQName(name)
const [elePrefix, eleLocalName] = namespace_extractQName(ele.prefix ? ele.prefix + ':' + ele.localName : ele.localName)

// check if this is a namespace declaration attribute
// assign a new element namespace if it wasn't previously assigned
let eleNamespace: string | null = null
if (prefix === "xmlns") {
namespace = infraNamespace.XMLNS
if (ele.namespaceURI === null && elePrefix === localName) {
eleNamespace = value
}
} else if (prefix === null && localName === "xmlns" && elePrefix === null) {
namespace = infraNamespace.XMLNS
eleNamespace = value
}

// re-create the element node if its namespace changed
// we can't simply change the namespaceURI since its read-only
if (eleNamespace !== null) {
const newEle = create_element(this._doc, eleLocalName,
eleNamespace, elePrefix)
for (const attr of ele.attributes) {
newEle.setAttributeNodeNS(attr.cloneNode() as Attr)
}
for (const childNode of ele.childNodes) {
newEle.appendChild(childNode.cloneNode())
}
const parent = ele.parentNode
/* istanbul ignore next */
if (parent === null) {
throw new Error("Parent node is null." + this._debugInfo())
}
parent.replaceChild(newEle, ele)
this._domNode = newEle
ele = newEle
}

if (namespace !== undefined) {
ele.setAttributeNS(namespace, name, value)
Expand Down
9 changes: 3 additions & 6 deletions src/callback/XMLBuilderCBImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
private _currentElementSerialized = false
private _openTags: Array<[string, string | null, NamespacePrefixMap, boolean]> = []

private _namespace: string | null
private _prefixMap: NamespacePrefixMap
private _prefixIndex: PrefixIndex

Expand Down Expand Up @@ -89,7 +88,6 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
this.on("error", this._options.error)
}

this._namespace = null
this._prefixMap = new NamespacePrefixMap()
this._prefixMap.set("xml", infraNamespace.XML)
this._prefixIndex = { value: 1 }
Expand Down Expand Up @@ -366,8 +364,9 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
let map = this._prefixMap.copy()
let localPrefixesMap: { [key: string]: string } = {}
let localDefaultNamespace = this._recordNamespaceInformation(node, map, localPrefixesMap)
let inheritedNS = this._namespace
let inheritedNS = this._openTags.length === 0 ? null : this._openTags[this._openTags.length - 1][1]
let ns = node.namespaceURI
if (ns === null) ns = inheritedNS

if (inheritedNS === ns) {
if (localDefaultNamespace !== null) {
Expand Down Expand Up @@ -463,14 +462,13 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
* Save qualified name, original inherited ns, original prefix map, and
* hasChildren flag.
*/
this._openTags.push([qualifiedName, this._namespace, this._prefixMap, hasChildren])
this._openTags.push([qualifiedName, inheritedNS, this._prefixMap, hasChildren])

/**
* New values of inherited namespace and prefix map will be used while
* serializing child nodes. They will be returned to their original values
* when this node is closed using the _openTags array item we saved above.
*/
this._namespace = inheritedNS
if (this._isPrefixMapModified(this._prefixMap, map)) {
this._prefixMap = map
}
Expand All @@ -497,7 +495,6 @@ export class XMLBuilderCBImpl extends EventEmitter implements XMLBuilderCB {
/**
* Restore original values of inherited namespace and prefix map.
*/
this._namespace = ns
this._prefixMap = map
if (!hasChildren) return

Expand Down
8 changes: 4 additions & 4 deletions src/writers/BaseWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,14 @@ export abstract class BaseWriter<T extends BaseWriterOptions, U extends XMLSeria
try {
this._serializeNodeNS(node, namespace, prefixMap, prefixIndex,
requireWellFormed)
} catch {
throw new InvalidStateError()
} catch (e) {
throw new InvalidStateError(e.message)
}
} else {
try {
this._serializeNode(node, requireWellFormed)
} catch {
throw new InvalidStateError()
} catch (e) {
throw new InvalidStateError(e.message)
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions test/basic/attribute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,9 @@ describe('att()', () => {
`)
})

test('attribute can only be applied to element nodes', () => {
const txt = $$.create().ele('root').txt('hello').first()
expect(() => txt.att('att', 'val')).toThrow()
})

})
57 changes: 38 additions & 19 deletions test/basic/namespace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ describe('namespaces', () => {

expect($$.serialize(doc)).toBe(
'<root xmlns="http://example.com/ns1">' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
'</root>')

expect(doc.documentElement.namespaceURI).toBe(ns1)
Expand All @@ -32,11 +32,11 @@ describe('namespaces', () => {

expect($$.serialize(doc)).toBe(
'<root xmlns="http://example.com/ns1"' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://example.com/n1 schema.xsd">' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://example.com/n1 schema.xsd">' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
'</root>')
})

Expand All @@ -56,11 +56,11 @@ describe('namespaces', () => {

expect($$.serialize(doc)).toBe(
'<root xmlns="http://example.com/ns1"' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://example.com/n1 schema.xsd">' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation="http://example.com/n1 schema.xsd">' +
'<foo>' +
'<bar>foobar</bar>' +
'</foo>' +
'</root>')
})

Expand All @@ -71,14 +71,14 @@ describe('namespaces', () => {
const doc = $$.create().ele(svgNs, 'svg')
.att('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', xlinkNs)
.ele(svgNs, 'script')
.att('type', 'text/ecmascript')
.att(xlinkNs, 'xlink:href', 'foo.js')
.att('type', 'text/ecmascript')
.att(xlinkNs, 'xlink:href', 'foo.js')
.doc().node as any

expect($$.serialize(doc)).toBe(
'<svg xmlns="http://www.w3.org/2000/svg"' +
' xmlns:xlink="http://www.w3.org/1999/xlink">' +
'<script type="text/ecmascript" xlink:href="foo.js"/>' +
' xmlns:xlink="http://www.w3.org/1999/xlink">' +
'<script type="text/ecmascript" xlink:href="foo.js"/>' +
'</svg>')
})

Expand Down Expand Up @@ -126,7 +126,7 @@ describe('namespaces', () => {
})

test('built-in namespace alias with JS object', () => {
const doc1 = $$.create().ele({ 'root@@xml': { '@att@@xml': 'val' }})
const doc1 = $$.create().ele({ 'root@@xml': { '@att@@xml': 'val' } })
expect($$.serialize(doc1.node)).toBe('<xml:root xml:att="val"/>')
const doc2 = $$.create().ele({ 'root@@svg': {} })
expect($$.serialize(doc2.node)).toBe('<root xmlns="http://www.w3.org/2000/svg"/>')
Expand All @@ -146,7 +146,7 @@ describe('namespaces', () => {
test('default namespace does not apply if was declared in an ancestor', () => {
const doc = $$.create()
.ele('root').att('http://www.w3.org/2000/xmlns/', 'xmlns:x', 'uri1')
.ele('uri1', 'table').att('http://www.w3.org/2000/xmlns/', 'xmlns', 'uri1')
.ele('uri1', 'table').att('http://www.w3.org/2000/xmlns/', 'xmlns', 'uri1')
.doc().node as any
expect($$.serialize(doc)).toBe(
'<root xmlns:x="uri1">' +
Expand All @@ -155,4 +155,23 @@ describe('namespaces', () => {
)
})

test('default namespace cannot be specified later', () => {
// this is just a limitation of the current implementation
// recursively changing child element namespaces is simply
// not implemented
const doc = $$.create()
.ele('root', { att: "val" })
.ele('foo').up()
.ele('bar').up()
.att("xmlns", "ns")
.doc().node as any

expect($$.serialize(doc)).toBe(
'<root att="val" xmlns="ns">' +
'<foo xmlns=""/>' +
'<bar xmlns=""/>' +
'</root>'
)
})

})
6 changes: 3 additions & 3 deletions test/basic/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ describe('object', () => {
const doc = $$.create().ele(obj).doc()

expect($$.printTree(doc.node)).toBe($$.t`
root xmlns="myns"
node
root (ns:myns) xmlns="myns" (ns:http://www.w3.org/2000/xmlns/)
node (ns:myns)
# val
`)
})
Expand All @@ -250,7 +250,7 @@ describe('object', () => {
const doc = $$.create().ele(obj).doc()

expect($$.printTree(doc.node)).toBe($$.t`
ns1:root xmlns:ns1="myns"
ns1:root (ns:myns) xmlns:ns1="myns" (ns:http://www.w3.org/2000/xmlns/)
node
# val
`)
Expand Down
12 changes: 6 additions & 6 deletions test/callback/namespaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('namespaces', () => {

$$.expectCBResult(xmlStream, $$.t`
<r xmlns:x0="ns" xmlns:x2="ns">
<b xmlns:x1="ns" xmlns:ns1="ns" ns1:name="v"/>
<b xmlns:x1="ns" x1:name="v"/>
</r>`, done)
})

Expand All @@ -76,7 +76,7 @@ describe('namespaces', () => {
</r>`, done)
})

test('prefix of an attribute is replaced with another existing prefix mapped to the same namespace URI', (done) => {
test('prefix of an attribute is replaced with another existing prefix mapped to the same namespace URI - 1', (done) => {
const xmlStream = $$.createCB({ prettyPrint: true })

xmlStream.ele('r')
Expand All @@ -85,7 +85,7 @@ describe('namespaces', () => {
.end()

$$.expectCBResult(xmlStream, $$.t`
<r xmlns:xx="uri" xmlns:p="uri" p:name="v"/> `, done)
<r xmlns:xx="uri" xx:name="v"/> `, done)
})

test('prefix of an attribute is replaced with another existing prefix mapped to the same namespace URI - 2', (done) => {
Expand All @@ -98,7 +98,7 @@ describe('namespaces', () => {

$$.expectCBResult(xmlStream, $$.t`
<r xmlns:xx="uri">
<b xmlns:p="uri" p:name="value"/>
<b xx:name="value"/>
</r> `, done)
})

Expand Down Expand Up @@ -264,7 +264,7 @@ describe('namespaces', () => {

$$.expectCBResult(xmlStream, $$.t`
<r xmlns:x0="uri" xmlns:x2="uri">
<b xmlns:x1="uri" xmlns:ns1="uri" ns1:name="v"/>
<b xmlns:x1="uri" x1:name="v"/>
</r>`, done)
})

Expand All @@ -278,7 +278,7 @@ describe('namespaces', () => {

$$.expectCBResult(xmlStream, $$.t`
<el1 xmlns:p="u1" xmlns:q="u1">
<el2 xmlns:q="u2" xmlns:ns1="u1" ns1:name="v"/>
<el2 xmlns:q="u2" q:name="v"/>
</el1>`, done)
})

Expand Down
2 changes: 1 addition & 1 deletion test/issues/issue-001.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Replicate issue', () => {
.doc()
const root = doc.root()
const foo = root.first()
expect(foo.toString()).toBe('<foo a="A1" x="1"/>')
expect(foo.toString()).toBe('<foo xmlns="http://example.com" a="A1" x="1"/>')
expect(root.toString()).toBe('<SomeRootTag xmlns="http://example.com" x="0"><foo a="A1" x="1"/></SomeRootTag>')
expect(doc.end({ headless: true })).toBe($$.t`
<SomeRootTag xmlns="http://example.com" x="0"><foo a="A1" x="1"/></SomeRootTag>
Expand Down
4 changes: 3 additions & 1 deletion test/issues/issue-008.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ describe('Replicate issue', () => {
})

const result = select("//myns:child1", doc.node as any) as any
expect(result.length).toBe(0)
expect(result.length).toBe(2)
expect(result[0].tagName).toBe("myns:child1")
expect(result[1].tagName).toBe("myns:child1")
})

test('#8 - correct usage', () => {
Expand Down
24 changes: 24 additions & 0 deletions test/issues/issue-013.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import $$ from '../TestHelpers'

describe('Replicate issue', () => {

// https://github.com/oozcitak/xmlbuilder2/issues/13
test('#13 - Parsing JS object with default namespace declaration attribute throws error', () => {
const str = $$.convert(
{ feed: { "@xmlns": "http://www.w3.org/2005/Atom" } },
{ headless: true, wellFormed: true }
)

expect(str).toBe(`<feed xmlns="http://www.w3.org/2005/Atom"/>`)
})

test('#13 - with prefix', () => {
const str = $$.convert(
{ "p:feed": { "@xmlns:p": "http://www.w3.org/2005/Atom" } },
{ headless: true, wellFormed: true }
)

expect(str).toBe(`<p:feed xmlns:p="http://www.w3.org/2005/Atom"/>`)
})

})
Loading

0 comments on commit 999e688

Please sign in to comment.