diff --git a/CoreFoundation/Parsing.subproj/CFXMLInterface.c b/CoreFoundation/Parsing.subproj/CFXMLInterface.c index c21e354c9f..c82b5fdb1e 100644 --- a/CoreFoundation/Parsing.subproj/CFXMLInterface.c +++ b/CoreFoundation/Parsing.subproj/CFXMLInterface.c @@ -367,8 +367,26 @@ _CFXMLNodePtr _CFXMLNewComment(const unsigned char* value) { return xmlNewComment(value); } -_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* value) { - return xmlNewProp(node, name, value); +_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* uri, const unsigned char* value) { + xmlNodePtr nodePtr = (xmlNodePtr)node; + xmlChar *prefix = NULL; + xmlChar *localName = xmlSplitQName2(name, &prefix); + + _CFXMLNodePtr result; + if (uri == NULL && localName == NULL) { + result = xmlNewProp(node, name, value); + } else { + xmlNsPtr ns = xmlNewNs(nodePtr, uri, localName ? prefix : NULL); + result = xmlNewNsProp(nodePtr, ns, localName ? localName : name, value); + } + + if (localName) { + xmlFree(localName); + } + if (prefix) { + xmlFree(prefix); + } + return result; } CFStringRef _CFXMLNodeCopyURI(_CFXMLNodePtr node) { @@ -376,7 +394,11 @@ CFStringRef _CFXMLNodeCopyURI(_CFXMLNodePtr node) { switch (nodePtr->type) { case XML_ATTRIBUTE_NODE: case XML_ELEMENT_NODE: - return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8); + if (nodePtr->ns && nodePtr->ns->href) { + return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8); + } else { + return NULL; + } case XML_DOCUMENT_NODE: { @@ -914,8 +936,56 @@ CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node) { return result; } -_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName) { - return xmlHasProp(node, (const xmlChar*)propertyName); +static inline xmlNsPtr _searchNamespace(xmlNodePtr nodePtr, const xmlChar* prefix) { + while (nodePtr != NULL) { + xmlNsPtr ns = nodePtr->ns; + while (ns != NULL) { + if (xmlStrcmp(prefix, ns->prefix) == 0) { + return ns; + } + ns = ns->next; + } + nodePtr = nodePtr->parent; + } + return NULL; +} + +void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node) { + xmlNodePtr propNodePtr = (xmlNodePtr)propertyNode; + xmlNodePtr nodePtr = (xmlNodePtr)node; + if (propNodePtr->type != XML_ATTRIBUTE_NODE || nodePtr->type != XML_ELEMENT_NODE) { + return; + } + if (propNodePtr->ns != NULL + && propNodePtr->ns->href == NULL + && propNodePtr->ns->prefix != NULL) { + xmlNsPtr ns = _searchNamespace(nodePtr, propNodePtr->ns->prefix); + if (ns != NULL && ns->href != NULL) { + propNodePtr->ns->href = xmlStrdup(ns->href); + } + } +} + +_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* uri) { + xmlNodePtr nodePtr = (xmlNodePtr)node; + xmlChar* prefix = NULL; + xmlChar* localName = xmlSplitQName2(propertyName, &prefix); + + if (!uri) { + xmlNsPtr ns = _searchNamespace(nodePtr, prefix); + uri = ns ? ns->href : NULL; + } + _CFXMLNodePtr result; + result = xmlHasNsProp(node, localName ? localName : propertyName, uri); + + if (localName) { + xmlFree(localName); + } + if (prefix) { + xmlFree(prefix); + } + + return result; } _CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options) { diff --git a/CoreFoundation/Parsing.subproj/CFXMLInterface.h b/CoreFoundation/Parsing.subproj/CFXMLInterface.h index ea21dbbf87..2fbb9c27c7 100644 --- a/CoreFoundation/Parsing.subproj/CFXMLInterface.h +++ b/CoreFoundation/Parsing.subproj/CFXMLInterface.h @@ -144,7 +144,7 @@ _CFXMLDocPtr _CFXMLNewDoc(const unsigned char* version); _CFXMLNodePtr _CFXMLNewProcessingInstruction(const unsigned char* name, const unsigned char* value); _CFXMLNodePtr _CFXMLNewTextNode(const unsigned char* value); _CFXMLNodePtr _CFXMLNewComment(const unsigned char* value); -_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* value); +_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* _Nullable uri, const unsigned char* value); CFStringRef _Nullable _CFXMLNodeCopyURI(_CFXMLNodePtr node); void _CFXMLNodeSetURI(_CFXMLNodePtr node, const unsigned char* _Nullable URI); @@ -197,7 +197,8 @@ CFStringRef _CFXMLCopyStringWithOptions(_CFXMLNodePtr node, uint32_t options); CF_RETURNS_RETAINED CFArrayRef _Nullable _CFXMLNodesForXPath(_CFXMLNodePtr node, const unsigned char* xpath); CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node); -_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName); +void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node); +_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* _Nullable uri); _CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options); diff --git a/Docs/Status.md b/Docs/Status.md index 0202929522..6e55c9e83e 100644 --- a/Docs/Status.md +++ b/Docs/Status.md @@ -141,7 +141,7 @@ There is no _Complete_ status for test coverage because there are always additio | `XMLDocument` | Mostly Complete | Substantial | `init()`, `replacementClass(for:)`, and `object(byApplyingXSLT...)` remain unimplemented | | `XMLDTD` | Mostly Complete | Substantial | `init()` remains unimplemented | | `XMLDTDNode` | Complete | Incomplete | | - | `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, `attribute(forLocalName:uri:)`, namespace support, and others remain unimplemented | + | `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, namespace support, and others remain unimplemented | | `XMLNode` | Incomplete | Incomplete | `localName(forName:)`, `prefix(forName:)`, `predefinedNamespace(forPrefix:)`, and others remain unimplemented | | `XMLParser` | Complete | Incomplete | | diff --git a/Foundation/XMLElement.swift b/Foundation/XMLElement.swift index b1b13f1e3e..d866a861cb 100644 --- a/Foundation/XMLElement.swift +++ b/Foundation/XMLElement.swift @@ -90,6 +90,7 @@ open class XMLElement: XMLNode { } removeAttribute(forName: name) + _CFXMLCompletePropURI(attribute._xmlNode, _xmlNode); addChild(attribute) } @@ -98,7 +99,7 @@ open class XMLElement: XMLNode { @abstract Removes an attribute based on its name. */ open func removeAttribute(forName name: String) { - if let prop = _CFXMLNodeHasProp(_xmlNode, name) { + if let prop = _CFXMLNodeHasProp(_xmlNode, name, nil) { let propNode = XMLNode._objectNodeForNode(_CFXMLNodePtr(prop)) _childNodes.remove(propNode) // We can't use `xmlRemoveProp` because someone else may still have a reference to this attribute @@ -170,7 +171,7 @@ open class XMLElement: XMLNode { @abstract Returns an attribute matching this name. */ open func attribute(forName name: String) -> XMLNode? { - guard let attribute = _CFXMLNodeHasProp(_xmlNode, name) else { return nil } + guard let attribute = _CFXMLNodeHasProp(_xmlNode, name, nil) else { return nil } return XMLNode._objectNodeForNode(attribute) } @@ -179,7 +180,8 @@ open class XMLElement: XMLNode { @abstract Returns an attribute matching this localname URI pair. */ open func attribute(forLocalName localName: String, uri URI: String?) -> XMLNode? { - NSUnimplemented() + guard let attribute = _CFXMLNodeHasProp(_xmlNode, localName, URI) else { return nil } + return XMLNode._objectNodeForNode(attribute) } /*! diff --git a/Foundation/XMLNode.swift b/Foundation/XMLNode.swift index cad0ced828..5a423c5dba 100644 --- a/Foundation/XMLNode.swift +++ b/Foundation/XMLNode.swift @@ -122,7 +122,7 @@ open class XMLNode: NSObject, NSCopying { _xmlNode = _CFXMLNewNode(nil, "") case .attribute: - _xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", "")) + _xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", nil, "")) case .DTDKind: _xmlNode = _CFXMLNewDTD(nil, "", "", "") @@ -199,7 +199,7 @@ open class XMLNode: NSObject, NSCopying { @abstract Returns an attribute name="stringValue". */ open class func attribute(withName name: String, stringValue: String) -> Any { - let attribute = _CFXMLNewProperty(nil, name, stringValue) + let attribute = _CFXMLNewProperty(nil, name, nil, stringValue) return XMLNode(ptr: attribute) } @@ -209,10 +209,9 @@ open class XMLNode: NSObject, NSCopying { @abstract Returns an attribute whose full QName is specified. */ open class func attribute(withName name: String, uri: String, stringValue: String) -> Any { - let attribute = XMLNode.attribute(withName: name, stringValue: stringValue) as! XMLNode -// attribute.URI = URI + let attribute = _CFXMLNewProperty(nil, name, uri, stringValue) - return attribute + return XMLNode(ptr: attribute) } /*! diff --git a/TestFoundation/TestXMLDocument.swift b/TestFoundation/TestXMLDocument.swift index 88b43d032e..7e43839dbe 100644 --- a/TestFoundation/TestXMLDocument.swift +++ b/TestFoundation/TestXMLDocument.swift @@ -20,6 +20,7 @@ class TestXMLDocument : LoopbackServerTest { ("test_stringValue", test_stringValue), ("test_objectValue", test_objectValue), ("test_attributes", test_attributes), + ("test_attributesWithNamespace", test_attributesWithNamespace), ("test_comments", test_comments), ("test_processingInstruction", test_processingInstruction), ("test_parseXMLString", test_parseXMLString), @@ -264,6 +265,52 @@ class TestXMLDocument : LoopbackServerTest { XCTAssertEqual(element.attribute(forName:"hello")?.stringValue, "world", "\(element.attribute(forName:"hello")?.stringValue as Optional)") XCTAssertEqual(element.attribute(forName:"foobar")?.stringValue, "buzbaz", "\(element.attributes ?? [])") } + + func test_attributesWithNamespace() { + let uriNs1 = "http://example.com/ns1" + let uriNs2 = "http://example.com/ns2" + + let root = XMLNode.element(withName: "root") as! XMLElement + root.addNamespace(XMLNode.namespace(withName: "ns1", stringValue: uriNs1) as! XMLNode) + + let element = XMLNode.element(withName: "element") as! XMLElement + element.addNamespace(XMLNode.namespace(withName: "ns2", stringValue: uriNs2) as! XMLNode) + root.addChild(element) + + // Add attributes without URI + element.addAttribute(XMLNode.attribute(withName: "name", stringValue: "John") as! XMLNode) + element.addAttribute(XMLNode.attribute(withName: "ns1:name", stringValue: "Tom") as! XMLNode) + + // Add attributes with URI + element.addAttribute(XMLNode.attribute(withName: "ns1:age", uri: uriNs1, stringValue: "44") as! XMLNode) + element.addAttribute(XMLNode.attribute(withName: "ns2:address", uri: uriNs2, stringValue: "Foobar City") as! XMLNode) + + // Retrieve attributes without URI + XCTAssertEqual(element.attribute(forName: "name")?.stringValue, "John", "name==John") + XCTAssertEqual(element.attribute(forName: "ns1:name")?.stringValue, "Tom", "ns1:name==Tom") + XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "44", "ns1:age==44") + XCTAssertEqual(element.attribute(forName: "ns2:address")?.stringValue, "Foobar City", "ns2:addresss==Foobar City") + + // Retrieve attributes with URI + XCTAssertEqual(element.attribute(forLocalName: "name", uri: nil)?.stringValue, "John", "name==John") + XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tom", "name==Tom") + XCTAssertEqual(element.attribute(forLocalName: "age", uri: uriNs1)?.stringValue, "44", "age==44") + XCTAssertNil(element.attribute(forLocalName: "address", uri: uriNs1), "address==nil") + XCTAssertEqual(element.attribute(forLocalName: "address", uri: uriNs2)?.stringValue, "Foobar City", "addresss==Foobar City") + + // Overwrite attributes + element.addAttribute(XMLNode.attribute(withName: "ns1:age", stringValue: "33") as! XMLNode) + XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "33", "ns1:age==33") + element.addAttribute(XMLNode.attribute(withName: "ns1:name", uri: uriNs1, stringValue: "Tommy") as! XMLNode) + XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tommy", "ns1:name==Tommy") + + // Remove attributes + element.removeAttribute(forName: "name") + XCTAssertNil(element.attribute(forLocalName: "name", uri: nil), "name removed") + XCTAssertNotNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name not removed") + element.removeAttribute(forName: "ns1:name") + XCTAssertNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name removed") + } func test_comments() { let element = XMLElement(name: "root")