Code archives/File Utilities/BlitzXML

This code has been declared by its author to be Public Domain code.

Download source code

BlitzXML by John J.
(Posted 22 years ago)
BlitzXML makes it easy to load, manipulate, and save XML files. BlitzXML is a library of functions for manipulating xml data, including a fast (parses roughly twice as fast as Microsoft Internet Explorer's XML viewer) xml parser and saver. XML is widely used anywhere from word-processors to level builders.

Download the BlitzXML Documentation, Blitz3D Example Code and media (example.xml)
The example code should give you a good idea how XML can be used in game development, world builders, applications, etc.

Original BlitzXML Forum Thread
;============================= BlitzXML =================================
;Copyright (C) 2005 John Judnich

;BlitzXML is an XML (eXtendable Markup Language) function library for
;blitz. You don't even need to have any knowledge of the syntax of XML
;to use BlitzXML, although an understanding of the terms and structure
;is helpful.

;At the user's (programmer's) point of view,
;BlitzXML is a way of storing bits of data similar to the way folders
;are stored on a hard drive. For example an node (item) named "inventory" may contain
;child nodes (sub-items) such as, "key". Each node may have an unlimited
;amount of sub-nodes, and each sub-node may have sub-sub-nodes, etc.
;Each node may have a name, and a number of attributes. Nodes may
;contain both sub-nodes (called children), and text data. To get a
;better idea how xml works, look at "example.xml".

;When a data structure is constructed with BltizXML, it may be saved
;to a file using XML syntax, which is really just a more strict form of
;HTML. XML files may also be loaded and parsed into BlitzXML just as
;easily. Due to the flexibility of XML, this library should be very
;useful for level editors, games that load levels from XML files,
;or any program requiring structured Or
;complex data to be saved to and loaded from a file.

;========================================================================



;**** Constant Declarations =============================================

Const MAX_ATTRIBUTES = 32	;The maximum number of attributes a xmlNode may have
Const MAX_ERRORS = 64		;The maximum number of errors and warnings that will be processed until the xml parser aborts the operation
Const PARSER_RECURSE = 1024 ;The maximum number of virtually "recursive" steps for the parser.
Global XML_INDENTATION$ = Chr$(9)	;The character(s) used to indent when saving files

;**** Type Declarations =================================================

;xmlNode Type - This is the main building block of BlitzXML. All xml data
;is stored with this Type, which gets manipulated by the user. The xml data
;can then be loaded from and saved to files.
Type xmlNode
	Field Name$								;The name of the node
	Field AttributeCount					;The number of attributes for the node
	Field AttributeName$[MAX_ATTRIBUTES]	;The attribute name array
	Field AttributeValue$[MAX_ATTRIBUTES]	;The attribute value array
	Field Contents$							;The data contents of the node
	Field Parent.xmlNode					;This node's parent node. If this is set to Null, then this node is the "root" node
	Field Level								;This is the node's level
	
	;These fields are manipulated by xml_RegisterChild(), xml_GetChild(), and xml_UnregisterChild()
	Field ChildCount						;The number of the node's children
	Field ChildBank							;A memory bank of handles to the node's children
End Type


;**** Global Declarations ===============================================

Global xml_Error$[MAX_ERRORS]
Global xml_ErrorPos[MAX_ERRORS]
Global xml_ErrorCount


;**** Interface Functions ===============================================
;Interface functions are the functions the user (the programmer) uses
;in their program, unlike the internal functions, which are only called
;by these functions.

;This function returns the level the node is at. If the node is the
;root node, it is at level 0. If it is a child of the root node,
;the level will be 1. If it is a child of a child of the root node,
;the level of 2 will be returned, etc.
Function xmlNodeLevel(Node)
	this.xmlNode = Object.xmlNode(Node)
	Return this\Level
End Function

;This returns a node's parent node. If the node has no parent (if it's
;the root node), 0 will be returned.
Function xmlNodeParent(Node)
	this.xmlNode = Object.xmlNode(Node)
	If this\Parent = Null Then Return 0
	Return Handle(this\Parent)
End Function

;This returns the number of children the node has. In many cases,
;the node will contain no children, therefore returning 0.
Function xmlNodeChildCount(Node)
	this.xmlNode = Object.xmlNode(Node)
	Return this\ChildCount
End Function

;This returns one of the node's children, specified by ChildIndex.
;ChildIndex may be set anywhere from 1 to the amount of children
;the node has, which can be obtained from the xmlNodeChildCount()
;function.
Function xmlNodeChild(Node, ChildIndex)
	this.xmlNode = Object.xmlNode(Node)
	Return Handle(xml_GetChild(this, ChildIndex))
End Function

;This function will search for the first node matching the specified
;name and parent. Specifying a parent (optional) will only search
;nodes that are children of the specified parent node. If you only want
;to find a direct child of this node (not sub-childs), set Recurse to False.
Function xmlNodeFind(Name$, Parent, Recurse = True)
	parentnode.xmlNode = Object.xmlNode(Parent)
	For this.xmlNode = Each xmlNode
		If this\Parent = parentnode Then
			If Lower(this\Name) = Lower(Name) Then
				Return Handle(this)
			End If
			If Recurse = True And this\ChildCount > 0 Then
				ret = xmlNodeFind(Name, Handle(this), True)
				If ret <> 0 Then Return ret
			End If
		End If
	Next
End Function

;This function adds a new node to the "tree" of existing xml nodes. Set
;ParentNode to the node you would like this to be a child of, or set it
;to 0 if this is the "root" node. Note: only one root node is allowed.
;Optionally, Name$ can be set to a name the node will initially be given,
;although the node can be renamed later with xmlNodeNameSet()
Function xmlNodeAdd(ParentNode, Name$="NewNode")
	this.xmlNode = New xmlNode
	parent.xmlNode = Object.xmlNode(ParentNode)
	
	this\Parent = parent
	If parent = Null Then
		this\Level = 0
	Else
		top.xmlNode = parent
		If parent\ChildCount = 0 Then
			top.xmlNode = parent
		Else
			top.xmlNode = Object.xmlNode( xmlNodeChild(ParentNode, 1) )
		End If
		Insert this After top
		this\Level = parent\Level + 1
		xml_RegisterChild(parent, this)
	End If
	
	this\Name = Name
	Return Handle(this)
End Function

;This function deletes the given node, including all of it's children
;(sub-nodes), if there are any. Ignore the ChildIndex variable, as it
;is used internally when recursively deleting the node's children.
;This can be used to delete an entire XML file in memory by deleting it's
;handle (root node)
Function xmlNodeDelete(Node, ChildIndex = 0)
	this.xmlNode = Object.xmlNode(Node)
	
	For i = 1 To this\ChildCount
		xmlNodeDelete(Handle(xml_GetChild(this, 1)), 1) ;The index is always 1 because the list will keep getting smaller while they are getting deleted - (just like holding down the delete key at the beginning of a document)
	Next
	
	If this\Parent <> Null Then
		If ChildIndex = 0 Then
			For i = 1 To this\Parent\ChildCount
				If xml_GetChild(this\Parent, i) = this Then ChildIndex = i:Exit
			Next
		End If
		xml_UnregisterChild(this\Parent, ChildIndex)
	End If
	
	FreeBank this\ChildBank
	Delete this
End Function

;This sets a node's name. Note: A node's name must not be a blank string
Function xmlNodeNameSet(Node, Name$)
	this.xmlNode = Object.xmlNode(Node)
	this\Name = Name
End Function

;This returns the name of a node
Function xmlNodeNameGet$(Node)
	this.xmlNode = Object.xmlNode(Node)
	Return this\Name
End Function

;This sets the value of an attribute of a node. If the attribute does
;not exist, it will be created. The attribute's value may be any valid
;string of characters, not including double quotes. The value is allowed
;to be a blank string.
;Example:
;xmlNodeAttributeSet(node, "alpha", "0.7")
Function xmlNodeAttributeValueSet(Node, Attribute$, Value$)
	this.xmlNode = Object.xmlNode(Node)
	
	;Check if the attribute exists or not
	indx = 0
	For i = 1 To this\AttributeCount
		If Attribute = this\AttributeName[i] Then indx = i:Exit
	Next
	
	;Create a new attribute if it doesn't exist
	If indx = 0 Then
		this\AttributeCount = this\AttributeCount + 1
		this\AttributeName[this\AttributeCount] = Attribute
		indx = this\AttributeCount
	End If
	
	;Set the attribute's value
	this\AttributeValue[indx] = Value
End Function

;This returns the value of the specified attribute, if it exists. If it
;doesn't exist, a blank string will be returned.
;Example:
;EntityAlpha Entity\Mesh, xmlNodeAttributeGet(Entity\Node, "alpha")
Function xmlNodeAttributeValueGet$(Node, Attribute$)
	this.xmlNode = Object.xmlNode(Node)
	
	;Find the attribute
	indx=0
	For i = 1 To this\AttributeCount
		If Attribute = this\AttributeName[i] Then indx = i:Exit
	Next
	
	;If the attribute exists, return it's value. If not, return a blank string
	If indx = 0 Then
		Return ""
	Else
		Return this\AttributeValue[indx]
	End If
End Function

;This sets the name of an attribute (NOT it's value). Note: attribute
;names are case sensitive
;Example:
;xmlNodeAttributeNameSet(node,"pitch","Xang")
Function xmlNodeAttributeNameSet(Node, Attribute$, NewName$)
	this.xmlNode = Object.xmlNode(Node)
	
	;Find the attribute
	indx = 0
	For i = 1 To this\AttributeCount
		If Attribute = this\AttributeName[i] Then indx = i:Exit
	Next

	;If the attribute exists, rename it
	If indx <> 0 Then
		this\AttributeName[indx] = NewName
	End If
End Function

;This deletes an attribute. Once a new attribute is created when
;using the xmlNodeAttributeSet() function, it will continue to
;reside in memory, and be saved to a file even if it's value is 
;blank. To remove an un-used (or used) attribute of a node, use
;this function.
;Example:
;xmlNodeAttributeDelete(node, "hidden")
Function xmlNodeAttributeDelete(Node, Attribute$)
	this.xmlNode = Object.xmlNode(Node)
	
	;Find the attribute
	indx = 0
	For i = 1 To this\AttributeCount
		If Attribute = this\AttributeName[i] Then indx = i:Exit
	Next
	
	;Delete the attribute, if it exists
	If indx <> 0 Then
		this\AttributeName[indx] = this\AttributeName[this\AttributeCount]
		this\AttributeValue[indx] = this\AttributeValue[this\AttributeCount]
		this\AttributeCount = this\AttributeCount - 1
	End If
End Function

;This sets a node's data string. A node's data is a string of
;text contained within the opening and closing node tags.
;Example:
;xmlNodeDataSet(titlenode, "BlitzXML")
Function xmlNodeDataSet(Node, NodeData$)
	this.xmlNode = Object.xmlNode(Node)
	this\Contents = NodeData
End Function

;This returns a node's data string. A node's data is a string
;of text contained within the opening and closing node tags.
Function xmlNodeDataGet$(Node)
	this.xmlNode = Object.xmlNode(Node)
	Return this\Contents
End Function

;This function saves all XML nodes to the specified file.
;If any errors occur, false will be returned, if not, true
;will be returned.
Function xmlSave(FileName$, Node)
	this.xmlNode = Object.xmlNode(Node)

	file = WriteFile(FileName)
	If file = 0 Then xml_AddError("Error writing XML file (possibly, file is in use, or is the folder/drive/file is write protected).", 0):Return
	
	WriteLine file, "<?xml version="+Chr(34)+"1.0"+Chr(34)+" ?>"
	xml_WriteNode(file, this)
	
	CloseFile file
End Function

Function xml_WriteNode(File, Node.xmlNode)
	Local NodeContents$, Indent$, Indent2$
	
	NodeContents = Node\Name
	For i = 1 To Node\AttributeCount
		NodeContents = NodeContents + " " + Node\AttributeName[i] + "=" + Chr$(34) + Node\AttributeValue[i] + Chr$(34)
	Next
	
	Indent = String$(XML_INDENTATION$, Node\Level)
	Indent2 = String$(XML_INDENTATION$, Node\Level+1)
	
	If Node\ChildCount = 0 Then
		If Node\Contents = "" Then
			WriteLine File, Indent + "<" + NodeContents + "/>" 
		Else
		    WriteLine File, Indent + "<" + NodeContents + ">" + Node\Contents + "</" + Node\Name + ">"
		End If
	Else
		WriteLine File, Indent + "<" + NodeContents + ">"
		If Node\Contents <> "" Then WriteLine File, Indent2 + Node\Contents
		
		For i = 1 To Node\ChildCount
			xml_WriteNode(File, Object.xmlNode(xmlNodeChild(Handle(Node), i)))
		Next
		
		WriteLine File, Indent + "</" + Node\Name + ">"
	End If
End Function

;This function loads and parses XML nodes from the specified XML file.
;Note: This (BlitzXML's xml parser) only supports xml files with standard
;xml tags and attributes with values enclosed in quotes. If the file
;is loaded successfully with no errors, a handle to the root node of the file
;will be returned. If not, 0 will be returned.
;Errors can be accessed using the xmlError$() and xmlErrorCount() functions.
Function xmlLoad(FileName$)
	Local attribute$[MAX_ATTRIBUTES]
	Local value$[MAX_ATTRIBUTES]
	Local nodestack[PARSER_RECURSE]
	Local rootnode
	
	DebugLog "Loading XML file: " + FileName
	xml_ClearErrors()
	begintime = MilliSecs()
	
	;Open the file
	file = ReadFile(FileName)
	If file = False Then
		xml_AddError("Error opening XML file: File does not exist.", 0)
		Return 0
	End If
	If Eof(file) = -1 Then
		xml_AddError("Error opening XML file: File is already in use by another program.", 0)
		Return 0
	End If
	
	;Read in all tags
	stacklevel = 0
	While Eof(file) = False
		;Get the next tag or data section
		tag$ = xml_NextItem(file)
		
		If tag$ <> "" Then 
		
			If xml_ItemType = 2 Then
				;Node contents
				xmlNodeDataSet(nodestack[stacklevel - 1], Trim(Trim(xmlNodeDataGet(nodestack[stacklevel - 1])) + " " + Trim(tag)))
			Else
				;Check if it's a closing tag, opening tag, or stand-alone tag
				If Left(tag,1) = "/" Then
					;Closing tag
					stacklevel = stacklevel - 1
					
					tmp.xmlNode = Object.xmlNode(nodestack[stacklevel])
					If tag <> "/" + tmp\Name Then xml_AddError("Unclosed tag (found <"+tag+">, expected </"+tmp\Name+">", FilePos(file))
				Else
					;Create a new node
					If stacklevel > 0 Then parent = nodestack[stacklevel - 1] Else parent = 0
					node = xmlNodeAdd(parent)
					If stacklevel = 0 Then rootnode = node
					
					;Get the name and attributes from the tag
					For i = 0 To attr:attribute[i] = "":value[i] = "":Next:attr = 0:opened = False:name$ = ""
					length = Len(tag)
					For i = 1 To length
						ch$ = Mid(tag, i, 1)
						If attr = 0 And ch = " " Then attr = attr + 1
						If ch = "=" Then attr = -attr
						If ch = Chr(34) Then
							If attr > 0 Then xml_AddError("Expecting equals symbol", FilePos(file))
							opened = 1 - opened
							If opened = False Then attr = Abs(attr):attr = attr + 1
						End If
						If ch <> Chr(34) And attr < 0 And opened Then value[-attr] = value[-attr] + ch
						If attr = 0 Then
							name = name + ch
						Else
							If attr > 0 And ch <> Chr(34) And ch<>" " Then attribute[attr] = attribute[attr] + ch
						End If
					Next
					For i = 1 To attr-1
						xmlNodeAttributeValueSet(node, attribute[i], value[i])
					Next
					xmlNodeNameSet(node, name)
					nodestack[stacklevel] = node
					
					If Right(tag,1) = "/" Then
						;Stand-alone tag
					Else
						;Opening tag
						stacklevel = stacklevel + 1
					End If
				End If
			End If
		End If
	Wend
	
	CloseFile file
	
	endtime = MilliSecs()
	parsetime# = (endtime - begintime) / 1000.0
	If xmlErrorCount > 0 Then DebugLog "Parse failed" Else DebugLog "Parse completed in "+parsetime+" seconds."
	
	If xmlErrorCount > 0 Then Return 0 Else Return rootnode
End Function

;This function returns the number of errors and warnings from the last
;file parse performed.
Function xmlErrorCount()
	Return xml_ErrorCount
End Function

;This returns the position of the specified error (in characters from
;the beginning of the file
Function xmlErrorPosition(ErrorNumber)
	If ErrorNumber > xml_ErrorCount Then Return 0
	Return xml_ErrorPos[ErrorNumber]
End Function

;This returns the description of the requested error.
Function xmlError$(ErrorNumber)
	If ErrorNumber > xml_ErrorCount Then Return ""
	Return xml_Error[ErrorNumber]
End Function



;**** Internal Functions ================================================
;Internal functions should not be called from ANYWHERE but from other
;BlitzXML functions. These functions are undocumented, and you should
;NOT use them.

Global xml_ItemType
Function xml_NextItem$(file)
	Local tag$
	While Eof(file) = False
		ch = ReadByte(file)
		If txt$ <> "" And (ch = 60 Or ch = 13) Then xml_ItemType = 2:SeekFile file,FilePos(file)-1:Return txt
		If ch <> 13 And ch <> 15 And ch <> 10 Then txt$ = txt$ + Chr(ch)
		If ch = 13 And txt <> "" Then txt = txt + " "
		
		If ch = 60 Then ;<
			If opened = True Then xml_AddError("Expecting closing bracket (>)", FilePos(file))
			opened = True
		End If
		If ch = 62 Then ;>
			txt = ""
			If opened = False Then xml_AddError("Expecting opening bracket (<)", FilePos(file))
			opened = False
			If Left(tag,4) = "<!--" Or Left(tag,2) = "<?" Then
				If Left(tag,4) = "<!--" And Right(tag,2) <> "--" Then xml_AddError("Expecting correct comment closure (-->)", FilePos(file))
				If Left(tag,4) = "<?" And Right(tag,2) <> "?" Then xml_AddError("Expecting correct header closure (?>)", FilePos(file))
				tag = ""
			Else
				xml_ItemType = 1
				Return Right(tag,Len(tag)-1)
			End If
		End If
		If opened Then tag = tag + Chr(ch)
	Wend
End Function

Function xml_RegisterChild(Node.xmlNode, Child.xmlNode)
	;Incriment the child count
	Node\ChildCount = Node\ChildCount + 1
	
	;Allocate memory for the data
	If Node\ChildBank = False Then
		Node\ChildBank = CreateBank(4)
	Else
		ResizeBank Node\ChildBank, Node\ChildCount * 4
	End If
	
	;Write the data
	Value = Handle(Child)
	PokeInt Node\ChildBank, (Node\ChildCount - 1) * 4, Value
End Function

Function xml_GetChild.xmlNode(Node.xmlNode, ChildIndex)
	;Check if the ChildIndex is valid
	If ChildIndex > Node\ChildCount Then Return Null
	
	;Get the child xmlNode object and return it
	Value = PeekInt(Node\ChildBank, (ChildIndex - 1) * 4)
	this.xmlNode = Object.xmlNode(Value)
	Return this
End Function

Function xml_UnregisterChild(Node.xmlNode, ChildIndex)
	;Check if the ChildIndex is valid
	If ChildIndex > Node\ChildCount Then Return False

	;"Swap" the child-to-be-deleted with the last child on the list, so the last child on the list is now the child to be deleted
	;(actually, it doesn't swap - to optimize it a little, the child-to-be-deleted doesn't get copied anywhere because it's not gonna be used)
	Value = PeekInt(Node\ChildBank, (Node\ChildCount - 1) * 4)
	PokeInt Node\ChildBank, (ChildIndex - 1) * 4, Value
	
	;Downsize the bank, erasing the last child on the list which would be the child-to-be-deleted
	ResizeBank Node\ChildBank, (Node\ChildCount - 1) * 4
	Node\ChildCount = Node\ChildCount - 1
	
	Return True
End Function

Function xml_ClearErrors()
	xml_ErrorCount = 0
End Function

Function xml_AddError(Description$, pos)
	xml_ErrorCount = xml_ErrorCound + 1
	xml_ErrorPos[xml_ErrorCount] = pos
	xml_Error[xml_ErrorCount] = Description
	
	DebugLog "Error at char #"+pos+":  "+Description
End Function