Lightweight XML Parser and Writerο
Unit: MiniXML
Overviewο
MiniXML is a compact XML parser and writer designed for Turbo Pascal 7.0, providing DOM-style access to XML configuration files and data. It handles XML files up to ~64KB (Turbo Pascal real-mode heap block limit) and supports the core XML features needed for game configuration and data storage.
Featuresο
64K file support: Handles XML files up to ~64KB
DOM-style tree structure: Navigate XML as parent/child/sibling nodes
Attribute support: Read element attributes, maximum 8, linear lookup
Text content: Handles both small (<255 bytes) and large text content
Comments and processing instructions: Skips XML comments and
<?xml?>declarationsUTF-8 BOM detection: Automatically skips UTF-8 byte order mark
Memory efficient: Uses dynamic allocation for large text content
Helper utilities: Word array parser for numeric data
Dependenciesο
GENTYPES.PAS: Generic types
Type Definitionsο
const
XML_MaxNameLength = 20; { Maximum element/attribute name length }
XML_MaxAttrsCount = 8; { Maximum attributes per node }
type
PAttrString = PChar;
TPAttrStringArray = array[0..XML_MaxAttrsCount - 1] of PAttrString;
PXMLNode = ^TXMLNode;
TXMLNode = record
Name : string[XML_MaxNameLength];
{ Text content buffer (all text stored here) }
TextBuf : Pointer; { points to a byte buffer }
TextLen : Word; { number of bytes in TextBuf }
TextCap : Word; { allocated capacity in bytes }
AttrKeys : TPAttrStringArray;
AttrValues: TPAttrStringArray;
AttrCount : Integer;
FirstChild : PXMLNode;
NextSibling: PXMLNode;
Parent : PXMLNode;
end;
Core APIο
Loading and Freeingο
XMLLoadFile - Load XML from file
function XMLLoadFile(const FileName: string; var Root: PXMLNode): Boolean;
Parameters:
FileName: Path to XML file (DOS 8.3 format, max ~64KB)Root: Returns pointer to root node on success
Returns: True if successful, False on error
Example:
var
Root: PXMLNode;
begin
if XMLLoadFile('CONFIG.XML', Root) then
begin
{ Use the XML tree... }
XMLFreeTree(Root); { Always free when done! }
end;
end;
XMLFreeTree - Free XML tree and all memory
procedure XMLFreeTree(Node: PXMLNode);
CRITICAL: Always call this to free the entire XML tree and prevent memory leaks.
Attributesο
XMLAttr - Get attribute value
function XMLAttr(const Node: PXMLNode; const Name: string): string;
Parameters:
Node: Element nodeName: Attribute name
Returns: Attribute value, or '' if not found
Example:
ID := XMLAttr(Node, 'id');
Width := XMLAttr(Sprite, 'width');
XMLHasAttr - Check if attribute exists
function XMLHasAttr(const Node: PXMLNode; const Name: string): Boolean;
Example:
if XMLHasAttr(Enemy, 'boss') then
WriteLn('Boss enemy detected!');
XMLGetText - Get text content from node
function XMLGetText(const Node: PXMLNode): string;
Returns: Text content as string (clamped to 255 bytes if longer)
Example:
Description := XMLGetText(XMLFirstChild(Root, 'description'));
WriteLn('Description: ', Description);
Utility Functionsο
XMLReadWordArray - Parse numeric array from text content
function XMLReadWordArray(const Node: PXMLNode; var Arr: PWord;
var Count: Word): Boolean;
Parameters:
Node: Element containing numeric text (e.g., β10 20 30 40β)Arr: Returns pointer to allocated Word arrayCount: Returns number of values parsed
Returns: True if successful
IMPORTANT: Caller must free array with FreeMem(Arr, Count * SizeOf(Word))
Example:
var
Values: PWord;
Count: Word;
i: Integer;
begin
if XMLReadWordArray(DataNode, Values, Count) then
begin
for i := 0 to Count - 1 do
WriteLn('Value[', i, '] = ', PWordArray(Values)^[i]);
FreeMem(Values, Count * SizeOf(Word));
end;
end;
Usage Examplesο
Iterating Through Listsο
{ Load sprite definitions from XML }
var
Root, Sprites, Sprite: PXMLNode;
ID, FileName: string;
begin
XMLLoadFile('SPRITES.XML', Root);
Sprites := XMLFirstChild(Root, 'sprites');
Sprite := XMLFirstChild(Sprites, 'sprite');
while Sprite <> nil do
begin
ID := XMLAttr(Sprite, 'id');
FileName := XMLAttr(Sprite, 'file');
WriteLn('Sprite: ', ID, ' -> ', FileName);
Sprite := XMLNextSibling(Sprite, 'sprite');
end;
XMLFreeTree(Root);
end;
Reading Text Contentο
var
Node: PXMLNode;
begin
Node := XMLFirstChild(Root, 'description');
{ Small text (<255 bytes) stored in Node^.Text }
if Node^.Text <> '' then
WriteLn('Description: ', Node^.Text);
{ Large text (>=255 bytes) stored in TextBuf }
if Node^.TextBuf <> nil then
WriteLn('Large text, ', Node^.TextLen, ' bytes');
end;
Checking Optional Attributesο
var
Level: PXMLNode;
IsBoss: Boolean;
begin
Level := XMLFirstChild(Root, 'level');
{ Check for optional 'boss' attribute }
IsBoss := XMLHasAttr(Level, 'boss') and
(XMLAttr(Level, 'boss') = 'true');
if IsBoss then
WriteLn('Boss level!')
else
WriteLn('Normal level');
end;
Sample XML Fileο
<?xml version="1.0" encoding="US-ASCII"?>
<resources>
<!-- Audio -->
<music name="fantasy" path="FANTASY.HSC" />
<sound name="explode" path="EXPLODE.VOC" />
<!-- Images -->
<image name="test" path="TEST.PCX" palette="test" />
<image name="player" path="PLAYER.PCX" palette="player" />
<!-- Fonts -->
<font name="small" path="FONT-SM.XML" />
<font name="large" path="FONT-LG.XML" />
<!-- Animations -->
<sprite name="player_idle" image="player" duration="0.8">
<frame x="0" y="0" width="32" height="32" />
<frame x="32" y="0" width="32" height="32" />
<frame x="64" y="0" width="32" height="32" />
<frame x="96" y="0" width="32" height="32" />
</sprite>
<sprite name="player_run" image="player" duration="0.5">
<frame x="0" y="32" width="32" height="32" />
<frame x="32" y="32" width="32" height="32" />
<frame x="64" y="32" width="32" height="32" />
<frame x="96" y="32" width="32" height="32" />
<frame x="128" y="32" width="32" height="32" />
<frame x="160" y="32" width="32" height="32" />
</sprite>
</resources>
Limitationsο
File size: Maximum ~64KB (Turbo Pascal heap block limit in real-mode DOS)
Attributes per element: Maximum 8 attributes (can be increased via
XML_MaxAttrsCount)Attribute values: Maximum 255 bytes (Turbo Pascal string limit)
Element/Attribute names: Maximum 20 bytes (can be increased via
XML_MaxNameLength)No validation: Does not validate against DTD/XSD schemas
No entities: HTML entities (
<,&, etc.) not decodedNo namespaces: XML namespaces not supported
Memory Managementο
Automatic allocation: Nodes and buffers allocated dynamically
Manual cleanup: Call
XMLFreeTree(Root)to free entire treeMemory leaks: Failing to call
XMLFreeTreewill leak memoryNested elements: All children recursively freed by
XMLFreeTree
Performance Characteristicsο
Attribute lookup: O(n) linear scan
Child iteration: O(n) linear scan
File loading: O(n) single-pass parse
Memory overhead: ~103 bytes per node (base) + text content + (actual attributes Γ ~20-50 bytes each)
π¦ Memory Usageο
This section describes the exact RAM footprint of the XML parser.
π§± Base Memory per Nodeο
A TXMLNode record contains:
Name: 21 bytes (string[20])
TextBuf: 4 bytes (pointer)
TextLen: 2 bytes (Word)
TextCap: 2 bytes (Word)
AttrKeys: 16 bytes (8 Γ Pointer)
AttrValues: 16 bytes (8 Γ Pointer)
AttrCount: 2 bytes (Integer)
Tree pointers: 12 bytes (FirstChild, NextSibling, Parent)
βββββββββββββββββββββββββββββββββββββββββββββββ
Record total: ~95β110 bytes per node
Attribute lookup is a simple and fast linear scan (max 8 attributes).
π·οΈ Per Attribute Cost (Dynamic Allocation)ο
Each attribute uses variable-sized memory:
Key string: Length(Key) + 1 byte
Value string: Length(Value) + 1 byte
βββββββββββββββββββββββββββββββββββββββββββββββ
Total: ~5β20 bytes typical
(instead of the old 512 bytes)
Example:
code="65"β key 4 bytes + value 3 bytes = 8 bytes totalx="123"β ~7 bytesname="monsterslayer"β ~16 bytes
π Text Content Memoryο
Text is stored in a dynamically growing buffer:
First allocation: 256 bytes
Grows by doubling, then +4096
Maximum size: ~64 KB
Memory used:
TextBuf capacity: 256β65520 bytes (depending on growth)
TextLen: actual used bytes
Nodes without text do not allocate any text memory.
π Memory Usage Example (FONT-SM.XML)ο
For a ~5.5 KB XML containing:
92 nodes
367 attributes
No text content
Memory usage:
Node records: ~9.8 KB
Attributes: ~2.4 KB
Text buffers: 0 bytes
--------------------------------
Total: ~12 KB
Common Patternsο
Find specific element by attributeο
function FindLevelByID(Root: PXMLNode; ID: string): PXMLNode;
var
Levels, Level: PXMLNode;
begin
FindLevelByID := nil;
Levels := XMLFirstChild(Root, 'levels');
if Levels = nil then Exit;
Level := XMLFirstChild(Levels, 'level');
while Level <> nil do
begin
if XMLAttr(Level, 'id') = ID then
begin
FindLevelByID := Level;
Exit;
end;
Level := XMLNextSibling(Level, 'level');
end;
end;
Count elementsο
function XMLCountChildren(const Node: PXMLNode; const Name: string): Integer;
Safe attribute parsingο
procedure GetIntAttr(Node: PXMLNode; AttrName: string; var Value: Integer);
var
S: string;
Code: Integer;
begin
if XMLHasAttr(Node, AttrName) then
begin
S := XMLAttr(Node, AttrName);
Val(S, Value, Code);
if Code <> 0 then
WriteLn('Warning: Invalid integer in ', AttrName);
end;
end;
XML Writing & Tree Modificationο
Saving XML Filesο
function XMLSaveFile(const FileName: string; Root: PXMLNode): Boolean;
Writes an XML file from a MiniXML tree.
Produces well-formed XML.
Correct indentation and nesting.
Self-closing empty tags.
Escapes all required characters (
& < > " ').Handles large text blocks (CSV, maps) without modification.
If the root node is
#document, only its children are written.
Returns:
True on success, False on error.
function GetSaveXMLError: string;
Returns last XML save error message.
Creating Child Nodesο
function XMLAddChildElement(Parent: PXMLNode; const Name: string): PXMLNode;
Creates a new element and attaches it as the last child of Parent.
Example:ο
var N: PXMLNode;
N := XMLAddChildElement(Root, 'tileset');
XMLSetAttr(N, 'firstgid', '1');
Setting Element Textο
procedure XMLSetText(Node: PXMLNode; const S: string);`
Replaces the entire text content of a node.
Useful for:
<data encoding="csv">numeric lists
small strings
Example:ο
XMLSetText(DataNode, '1,2,3,4,5');
Appending Text to a Nodeο
procedure XMLAppendText(Node: PXMLNode; const S: string);
Appends text to existing node text, expanding the internal buffer if needed.
Useful when writing large CSV maps:
XMLSetText(DataNode, '1,2,3');
XMLAppendText(DataNode, ',4,5,6');
Setting Attributes (Existing)ο
procedure XMLSetAttr(Node: PXMLNode; const Key, Value: string);
Creates or updates an attribute:
XMLSetAttr(Node, 'width', '320');
XMLSetAttr(Node, 'height', '200');
Building an XML Fileο
Below is a simple example that demonstrates reading, modifying, and saving an XML file:
var
Root, Node: PXMLNode;
begin
if not XMLLoadFile('DATA\RES.XML', Root) then
Halt;
{ Modify root attribute }
XMLSetAttr(Root, 'version', '2.0');
{ Add a <resource> entry }
Node := XMLAddChildElement(Root, 'resource');
XMLSetAttr(Node, 'name', 'sprite');
XMLSetAttr(Node, 'path', 'DATA\SPRITE.PCX');
XMLSetText(Node, 'Auto-generated entry.');
{ Replace text of <testdata> }
Node := XMLFirstChild(Root, 'testdata');
if Node <> nil then
XMLSetText(Node, '10,20,30,40,50');
{ Save the result }
if not XMLSaveFile('DATA\RES-OUT.XML', Root) then
WriteLn('Save error: ', GetSaveXMLError);
XMLFreeTree(Root);
end.
Notes & Limitationsο
MiniXML does not preserve whitespace formatting of the input file.
The writer may reorder attributes (still valid XML).
Text nodes store bytes as-is; no UTF-8 conversion.
Large
<data>blocks are handled efficiently using raw buffers.
Troubleshootingο
Problem: XMLLoadFile returns False
Check file exists and path is correct (use DOS 8.3 format)
Ensure file is under ~64KB (65500 bytes)
Check file is valid XML (well-formed)
Problem: Attribute not found
Check attribute name spelling (case-sensitive)
Verify element actually has the attribute in XML file
Check if node pointer is
nil
Problem: Memory leak / out of memory
Ensure
XMLFreeTreeis called for every loaded XMLCheck for extremely large text content (>64KB per node)
Reduce number of nodes/attributes if memory constrained
Problem: Text content truncated at 255 bytes
This is expected for
Node^.TextstringLarge text automatically uses
TextBufinsteadCheck
Node^.TextBuf <> nilandNode^.TextLenfor large text
Testingο
See TESTS\XMLTEST.PAS for a complete example program demonstrating all features.
Compile and run:
cd TESTS
CXMLTEST.BAT
XMLTEST.EXE
Design Goalsο
MiniXML was created in 2025 specifically for this DOS game engine with the following priorities:
Memory Efficiency:
Dynamic allocation for all variable-size data
Only allocate whatβs actually needed (attributes, text)
Optimized for typical game config files (few attributes per node)
Result: ~103 bytes per empty node vs. 4680 bytes in naive design (98% reduction)
Real-Mode DOS Compatibility:
All allocations respect TP7 heap block limits (β€64KB per block)
Buffer growth capped at safe limits
Word-sized position/length fields for >32KB file support
Tested on DOSBox-X and real hardware
Simplicity:
Single-pass parser, no preprocessing
DOM-style tree for easy navigation
See Alsoο
TESTS\XMLTEST.PAS: Complete usage example
DATA\RES.XML: Resource XML file for testing