Writing XML with ObjC NSXMLNode


(Eric) #1

Mind-blowing how fast this is.

I have produced a valid file and have a running script (using code cribbed from Shane Stanley). My script is ok but two questions came up.

The script loops through the source data and inserts it in the proper place with if/thens.
Q1) When there are nested elements and attributes, I insert them in order like this-

set chElement to (current application’s NSXMLNode’s elementWithName:“chapter” stringValue:"")
(partElement’s addChild:chElement)
set A1 to (current application’s NSXMLNode’s attributeWithName:“id” stringValue:linkID)
(chElement’s addAttribute:A1)
set A2 to (current application’s NSXMLNode’s attributeWithName:“seq” stringValue:(seq as text))
(chElement’s addAttribute:A2)
set newElement to (current application’s NSXMLNode’s elementWithName:“title” stringValue:desc_)
(chElement’s addChild:newElement)

It makes me wonder if a whole element could be set in one shot— similar to the way in AS that we do "Make new __ with properties {x,x,x,x,x}

Q2) One of the elements has html tags in it. Using our previous XML writing method, I included !<[CDATA[]] tags to protect the tags in that element. This is not possible with NSXMLNode because it wisely escaping the <> signs. I could blow the CDATA tags in there after the file is written with BBedit or sed but that’s not so nice.

(Shane Stanley) #2

No. But you can make it a bit more compact using methods like setAttributesWithDictionary: and elementWithName:children:attributes:

NSXMLNode will put the tags in for you:

set theNode to current application's NSXMLNode's alloc()'s initWithKind:(current application's NSXMLTextKind) options:(current application's NSXMLNodeIsCDATA)
theNode's setStringValue:"Hello <b>world</b>"
theNode's XMLString() as text
--> "<![CDATA[Hello <b>world</b>]]>"

(Eric) #3

Fantastic on both counts. Thank you.
And now just two steps from the shoreline I am fully submerged and hypoxic. The Apple docs are pretty spartan. I went back to my Second Edition Everyday AppleScriptObjC lessons and did a little refreshing.

I’m reading the NSXMLNode docs in XCode 8.1 and looking at the option you set for the CDATA.
That part is clear. Next, you use ‘setStringValue’ which appears to be a subclass. According to the doc,

In most cases, you need only invoke the superclass implementation, adding any subclass-specific code before or after the invocation, as necessary.

It’s unclear from that override, how to
a) insert a child with the CDATA option
b) insert a child without that option.

I tried it every which way! :confounded:

Compacting the insert step

… you can make it a bit more compact using methods like setAttributesWithDictionary: and elementWithName:children:attributes:

The method takes a key and object (like an AS record I guess)
- (void)setAttributesWithDictionary:(NSDictionary<NSString *,NSString *> *)attributes;

This is formatted with some text in purple. How does one interpret that to know where the key and the object belong? And, is this the way to add multiple attributes to an element in one line? Could you write an example please?

+ (id)elementWithName:(NSString *)name children:(NSArray<NSXMLNode *> *)children attributes:(NSArray<NSXMLNode *> *)attributes;

That method looks like another way to do add multiple attributes to a child. It almost makes sense! I feel like I’m almost getting it.

Thanks for the help and I hope it’s okay asking 20 questions.

(Shane Stanley) #4

No, the property is stringValue, so you set it using setStringValue:.

I think you’re getting confused. That code creates a node. You would add it to some other node using addChild: or one of the insertChild:: methods.

The key is the attribute key, the value is the attribute value. You provide them as a record.

I suspect it sets rather than adds – it may replace, I can’t remember off-hand.

There’s nothing to it:

someNodes's setAttributesWithDictionary:{first_name:"Henrik", last_name:"Sabroe"}

(Eric) #5

I was most definitely confused. Things are shaping up except for a terminology conflict. The XML requires the attribute ‘scheme’, which is reserved by Foundation.

    <class scheme="safari-classification">course</class>

When I attempted to set that attribute with —
newElement's setAttributesWithDictionary:{scheme:"safari-classification"}
result = nadda

I went back to—
set A1 to (current application’s NSXMLNode’s attributeWithName:“scheme” stringValue:“safari-classification”)
(newElement’s addAttribute:A1)

I also noticed that ObjC doesn’t balk at variables ending with underscore e.g. color_

I am most grateful for your help. This is so fast!

(Shane Stanley) #6

You can use pipes:

newElement's setAttributesWithDictionary:{|scheme|:"safari-classification"}

Or make a dictionary:

newElement's setAttributesWithDictionary:(current application's NSDictionary's dictionaryWithObjects:{"safari-classification"} forKeys:{"scheme"})

The latter is a bit safer in that you don’t even have to know if there are clashes.

(Eric) #7

Thank you Shane. Things are groovy.
I loop through my data list of records. It does seem like my script is still on the verbose side and could use some more compacting.

-- Start root, Insert attributes
set rootElement to current application's NSXMLNode's elementWithName:("safarimeta")
rootElement's setAttributesWithDictionary:{fpi:SBO_isbn, format:"video"}

set Record_List to {{name_:"title", value_:TIT}, {name_:"promote", value_:trigger}, and so on ...}
repeat with x from 1 to count Record_List
	set data_ to item x of Record_List
	set newElement to (current application's NSXMLNode's elementWithName:(name_ of data_) stringValue:(value_ of data_))
	if name_ of data_ = "points" then
		set A1 to (current application's NSXMLNode's attributeWithName:"royalty" stringValue:"all")
		(newElement's addAttribute:A1)
		(rootElement's addChild:newElement)
	else if name_ of data_ = "graphic" then
		(newElement's setAttributesWithDictionary:{fileref:(SBO_isbn & ".jpg"), role:"large-cover"})
		(rootElement's addChild:newElement)
	else if name_ of data_ = "description-short" then
		-- Make a new element for desc_
		set descElement to (current application's NSXMLNode's elementWithName:("description-short"))
		-- Add it to the root element
		(rootElement's addChild:descElement)
		-- Set the CDATA string in a new Node
		set dNode to (current application's NSXMLNode's alloc()'s initWithKind:(current application's NSXMLTextKind) options:(current application's NSXMLNodeIsCDATA))
		(dNode's setStringValue:"Over <b>five</b> hours of Frank video talking. ")
		dNode's XMLString() as text
		--add the CDATA string to the already existing desc element
		(descElement's addChild:dNode)
		-->	More "else if" s here ...
		(rootElement's addChild:newElement)
	end if
end repeat

Note: I try to figure it out before asking.

(Shane Stanley) #8

You’re coding at a lower level so it will tend to be verbose. What matters is that it works, and is reasonably fast.

(Eric) #9


Thanks again for the help. Much obliged.