Skip to content

Commit 3ed9466

Browse files
authored
Merge pull request #2313 from hironytic/xml-attribute-namespace
XML: Add namespace support for XML attributes
2 parents 4fe4011 + f3882fa commit 3ed9466

File tree

6 files changed

+135
-16
lines changed

6 files changed

+135
-16
lines changed

CoreFoundation/Parsing.subproj/CFXMLInterface.c

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -370,16 +370,38 @@ _CFXMLNodePtr _CFXMLNewComment(const unsigned char* value) {
370370
return xmlNewComment(value);
371371
}
372372

373-
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* value) {
374-
return xmlNewProp(node, name, value);
373+
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr node, const unsigned char* name, const unsigned char* uri, const unsigned char* value) {
374+
xmlNodePtr nodePtr = (xmlNodePtr)node;
375+
xmlChar *prefix = NULL;
376+
xmlChar *localName = xmlSplitQName2(name, &prefix);
377+
378+
_CFXMLNodePtr result;
379+
if (uri == NULL && localName == NULL) {
380+
result = xmlNewProp(node, name, value);
381+
} else {
382+
xmlNsPtr ns = xmlNewNs(nodePtr, uri, localName ? prefix : NULL);
383+
result = xmlNewNsProp(nodePtr, ns, localName ? localName : name, value);
384+
}
385+
386+
if (localName) {
387+
xmlFree(localName);
388+
}
389+
if (prefix) {
390+
xmlFree(prefix);
391+
}
392+
return result;
375393
}
376394

377395
CFStringRef _CFXMLNodeCopyURI(_CFXMLNodePtr node) {
378396
xmlNodePtr nodePtr = (xmlNodePtr)node;
379397
switch (nodePtr->type) {
380398
case XML_ATTRIBUTE_NODE:
381399
case XML_ELEMENT_NODE:
382-
return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8);
400+
if (nodePtr->ns && nodePtr->ns->href) {
401+
return CFStringCreateWithCString(NULL, (const char*)nodePtr->ns->href, kCFStringEncodingUTF8);
402+
} else {
403+
return NULL;
404+
}
383405

384406
case XML_DOCUMENT_NODE:
385407
{
@@ -917,8 +939,56 @@ CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node) {
917939
return result;
918940
}
919941

920-
_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName) {
921-
return xmlHasProp(node, (const xmlChar*)propertyName);
942+
static inline xmlNsPtr _searchNamespace(xmlNodePtr nodePtr, const xmlChar* prefix) {
943+
while (nodePtr != NULL) {
944+
xmlNsPtr ns = nodePtr->ns;
945+
while (ns != NULL) {
946+
if (xmlStrcmp(prefix, ns->prefix) == 0) {
947+
return ns;
948+
}
949+
ns = ns->next;
950+
}
951+
nodePtr = nodePtr->parent;
952+
}
953+
return NULL;
954+
}
955+
956+
void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node) {
957+
xmlNodePtr propNodePtr = (xmlNodePtr)propertyNode;
958+
xmlNodePtr nodePtr = (xmlNodePtr)node;
959+
if (propNodePtr->type != XML_ATTRIBUTE_NODE || nodePtr->type != XML_ELEMENT_NODE) {
960+
return;
961+
}
962+
if (propNodePtr->ns != NULL
963+
&& propNodePtr->ns->href == NULL
964+
&& propNodePtr->ns->prefix != NULL) {
965+
xmlNsPtr ns = _searchNamespace(nodePtr, propNodePtr->ns->prefix);
966+
if (ns != NULL && ns->href != NULL) {
967+
propNodePtr->ns->href = xmlStrdup(ns->href);
968+
}
969+
}
970+
}
971+
972+
_CFXMLNodePtr _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* uri) {
973+
xmlNodePtr nodePtr = (xmlNodePtr)node;
974+
xmlChar* prefix = NULL;
975+
xmlChar* localName = xmlSplitQName2(propertyName, &prefix);
976+
977+
if (!uri) {
978+
xmlNsPtr ns = _searchNamespace(nodePtr, prefix);
979+
uri = ns ? ns->href : NULL;
980+
}
981+
_CFXMLNodePtr result;
982+
result = xmlHasNsProp(node, localName ? localName : propertyName, uri);
983+
984+
if (localName) {
985+
xmlFree(localName);
986+
}
987+
if (prefix) {
988+
xmlFree(prefix);
989+
}
990+
991+
return result;
922992
}
923993

924994
_CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options) {

CoreFoundation/Parsing.subproj/CFXMLInterface.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ _CFXMLDocPtr _CFXMLNewDoc(const unsigned char* version);
147147
_CFXMLNodePtr _CFXMLNewProcessingInstruction(const unsigned char* name, const unsigned char* value);
148148
_CFXMLNodePtr _CFXMLNewTextNode(const unsigned char* value);
149149
_CFXMLNodePtr _CFXMLNewComment(const unsigned char* value);
150-
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* value);
150+
_CFXMLNodePtr _CFXMLNewProperty(_CFXMLNodePtr _Nullable node, const unsigned char* name, const unsigned char* _Nullable uri, const unsigned char* value);
151151

152152
CFStringRef _Nullable _CFXMLNodeCopyURI(_CFXMLNodePtr node);
153153
void _CFXMLNodeSetURI(_CFXMLNodePtr node, const unsigned char* _Nullable URI);
@@ -200,7 +200,8 @@ CFStringRef _CFXMLCopyStringWithOptions(_CFXMLNodePtr node, uint32_t options);
200200
CF_RETURNS_RETAINED CFArrayRef _Nullable _CFXMLNodesForXPath(_CFXMLNodePtr node, const unsigned char* xpath);
201201
CFStringRef _Nullable _CFXMLCopyPathForNode(_CFXMLNodePtr node);
202202

203-
_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const char* propertyName);
203+
void _CFXMLCompletePropURI(_CFXMLNodePtr propertyNode, _CFXMLNodePtr node);
204+
_CFXMLNodePtr _Nullable _CFXMLNodeHasProp(_CFXMLNodePtr node, const unsigned char* propertyName, const unsigned char* _Nullable uri);
204205

205206
_CFXMLDocPtr _CFXMLDocPtrFromDataWithOptions(CFDataRef data, unsigned int options);
206207

Docs/Status.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ There is no _Complete_ status for test coverage because there are always additio
141141
| `XMLDocument` | Mostly Complete | Substantial | `init()`, `replacementClass(for:)`, and `object(byApplyingXSLT...)` remain unimplemented |
142142
| `XMLDTD` | Mostly Complete | Substantial | `init()` remains unimplemented |
143143
| `XMLDTDNode` | Complete | Incomplete | |
144-
| `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, `attribute(forLocalName:uri:)`, namespace support, and others remain unimplemented |
144+
| `XMLElement` | Incomplete | Incomplete | `init(xmlString:)`, `elements(forLocalName:uri:)`, namespace support, and others remain unimplemented |
145145
| `XMLNode` | Incomplete | Incomplete | `localName(forName:)`, `prefix(forName:)`, `predefinedNamespace(forPrefix:)`, and others remain unimplemented |
146146
| `XMLParser` | Complete | Incomplete | |
147147

Foundation/XMLElement.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ open class XMLElement: XMLNode {
9090
}
9191

9292
removeAttribute(forName: name)
93+
_CFXMLCompletePropURI(attribute._xmlNode, _xmlNode);
9394
addChild(attribute)
9495
}
9596

@@ -98,7 +99,7 @@ open class XMLElement: XMLNode {
9899
@abstract Removes an attribute based on its name.
99100
*/
100101
open func removeAttribute(forName name: String) {
101-
if let prop = _CFXMLNodeHasProp(_xmlNode, name) {
102+
if let prop = _CFXMLNodeHasProp(_xmlNode, name, nil) {
102103
let propNode = XMLNode._objectNodeForNode(_CFXMLNodePtr(prop))
103104
_childNodes.remove(propNode)
104105
// We can't use `xmlRemoveProp` because someone else may still have a reference to this attribute
@@ -170,7 +171,7 @@ open class XMLElement: XMLNode {
170171
@abstract Returns an attribute matching this name.
171172
*/
172173
open func attribute(forName name: String) -> XMLNode? {
173-
guard let attribute = _CFXMLNodeHasProp(_xmlNode, name) else { return nil }
174+
guard let attribute = _CFXMLNodeHasProp(_xmlNode, name, nil) else { return nil }
174175
return XMLNode._objectNodeForNode(attribute)
175176
}
176177

@@ -179,7 +180,8 @@ open class XMLElement: XMLNode {
179180
@abstract Returns an attribute matching this localname URI pair.
180181
*/
181182
open func attribute(forLocalName localName: String, uri URI: String?) -> XMLNode? {
182-
NSUnimplemented()
183+
guard let attribute = _CFXMLNodeHasProp(_xmlNode, localName, URI) else { return nil }
184+
return XMLNode._objectNodeForNode(attribute)
183185
}
184186

185187
/*!

Foundation/XMLNode.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ open class XMLNode: NSObject, NSCopying {
122122
_xmlNode = _CFXMLNewNode(nil, "")
123123

124124
case .attribute:
125-
_xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", ""))
125+
_xmlNode = _CFXMLNodePtr(_CFXMLNewProperty(nil, "", nil, ""))
126126

127127
case .DTDKind:
128128
_xmlNode = _CFXMLNewDTD(nil, "", "", "")
@@ -199,7 +199,7 @@ open class XMLNode: NSObject, NSCopying {
199199
@abstract Returns an attribute <tt>name="stringValue"</tt>.
200200
*/
201201
open class func attribute(withName name: String, stringValue: String) -> Any {
202-
let attribute = _CFXMLNewProperty(nil, name, stringValue)
202+
let attribute = _CFXMLNewProperty(nil, name, nil, stringValue)
203203

204204
return XMLNode(ptr: attribute)
205205
}
@@ -209,10 +209,9 @@ open class XMLNode: NSObject, NSCopying {
209209
@abstract Returns an attribute whose full QName is specified.
210210
*/
211211
open class func attribute(withName name: String, uri: String, stringValue: String) -> Any {
212-
let attribute = XMLNode.attribute(withName: name, stringValue: stringValue) as! XMLNode
213-
// attribute.URI = URI
212+
let attribute = _CFXMLNewProperty(nil, name, uri, stringValue)
214213

215-
return attribute
214+
return XMLNode(ptr: attribute)
216215
}
217216

218217
/*!

TestFoundation/TestXMLDocument.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class TestXMLDocument : LoopbackServerTest {
2020
("test_stringValue", test_stringValue),
2121
("test_objectValue", test_objectValue),
2222
("test_attributes", test_attributes),
23+
("test_attributesWithNamespace", test_attributesWithNamespace),
2324
("test_comments", test_comments),
2425
("test_processingInstruction", test_processingInstruction),
2526
("test_parseXMLString", test_parseXMLString),
@@ -284,6 +285,52 @@ class TestXMLDocument : LoopbackServerTest {
284285
XCTAssertEqual(element.attribute(forName:"hello")?.stringValue, "world", "\(element.attribute(forName:"hello")?.stringValue as Optional)")
285286
XCTAssertEqual(element.attribute(forName:"foobar")?.stringValue, "buzbaz", "\(element.attributes ?? [])")
286287
}
288+
289+
func test_attributesWithNamespace() {
290+
let uriNs1 = "http://example.com/ns1"
291+
let uriNs2 = "http://example.com/ns2"
292+
293+
let root = XMLNode.element(withName: "root") as! XMLElement
294+
root.addNamespace(XMLNode.namespace(withName: "ns1", stringValue: uriNs1) as! XMLNode)
295+
296+
let element = XMLNode.element(withName: "element") as! XMLElement
297+
element.addNamespace(XMLNode.namespace(withName: "ns2", stringValue: uriNs2) as! XMLNode)
298+
root.addChild(element)
299+
300+
// Add attributes without URI
301+
element.addAttribute(XMLNode.attribute(withName: "name", stringValue: "John") as! XMLNode)
302+
element.addAttribute(XMLNode.attribute(withName: "ns1:name", stringValue: "Tom") as! XMLNode)
303+
304+
// Add attributes with URI
305+
element.addAttribute(XMLNode.attribute(withName: "ns1:age", uri: uriNs1, stringValue: "44") as! XMLNode)
306+
element.addAttribute(XMLNode.attribute(withName: "ns2:address", uri: uriNs2, stringValue: "Foobar City") as! XMLNode)
307+
308+
// Retrieve attributes without URI
309+
XCTAssertEqual(element.attribute(forName: "name")?.stringValue, "John", "name==John")
310+
XCTAssertEqual(element.attribute(forName: "ns1:name")?.stringValue, "Tom", "ns1:name==Tom")
311+
XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "44", "ns1:age==44")
312+
XCTAssertEqual(element.attribute(forName: "ns2:address")?.stringValue, "Foobar City", "ns2:addresss==Foobar City")
313+
314+
// Retrieve attributes with URI
315+
XCTAssertEqual(element.attribute(forLocalName: "name", uri: nil)?.stringValue, "John", "name==John")
316+
XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tom", "name==Tom")
317+
XCTAssertEqual(element.attribute(forLocalName: "age", uri: uriNs1)?.stringValue, "44", "age==44")
318+
XCTAssertNil(element.attribute(forLocalName: "address", uri: uriNs1), "address==nil")
319+
XCTAssertEqual(element.attribute(forLocalName: "address", uri: uriNs2)?.stringValue, "Foobar City", "addresss==Foobar City")
320+
321+
// Overwrite attributes
322+
element.addAttribute(XMLNode.attribute(withName: "ns1:age", stringValue: "33") as! XMLNode)
323+
XCTAssertEqual(element.attribute(forName: "ns1:age")?.stringValue, "33", "ns1:age==33")
324+
element.addAttribute(XMLNode.attribute(withName: "ns1:name", uri: uriNs1, stringValue: "Tommy") as! XMLNode)
325+
XCTAssertEqual(element.attribute(forLocalName: "name", uri: uriNs1)?.stringValue, "Tommy", "ns1:name==Tommy")
326+
327+
// Remove attributes
328+
element.removeAttribute(forName: "name")
329+
XCTAssertNil(element.attribute(forLocalName: "name", uri: nil), "name removed")
330+
XCTAssertNotNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name not removed")
331+
element.removeAttribute(forName: "ns1:name")
332+
XCTAssertNil(element.attribute(forLocalName: "name", uri: uriNs1), "ns1:name removed")
333+
}
287334

288335
func test_comments() {
289336
let element = XMLElement(name: "root")

0 commit comments

Comments
 (0)