Since everything in these EXML documents is a Property
element, it makes it very hard to navigate. So, we will layout the structure here. The structure is derived by view Parent elements name and value, followed by any child element names and values.
This is a first stab at parsing this file. I will put what I learned in the Conclusion section. I will be using native Browser API's to perform this. The only external dependency will be use of the TailwindCSS CDN.
- root: GcNGuiLayerData
Data
element- name: ElementData value: GcNGuiElementData.xml
- name: Style value: TkNGuiGraphicStyle.xml
- name: Image value: ""
- name: Children value: ""
- name: DataFilename value: UI/CONTROLSPAGEPC.MBIN
- name: AltMode value: None
Lets see if I can grab them using standard document.querySelector or getElementBy methods.
const file = document.getElementById('upload');
file.addEventListener('change', () => {
const file_buffer = file.files[0];
const reader = new FileReader();
reader.addEventListener('load', (buffer) => {
let result = buffer.target.result;
// Parse the Dom tree of the EXML File
const parser = new DOMParser();
const doc = parser.parseFromString(result, "application/xml");
}
// Read the upload file as a Text String
reader.readAsText(file_buffer)
}
First, we setup the variable to capture the selected file from the file input. Followed by adding a event listener that listens to file changes. We then create a new FileReader instance that can read the file and output it to a stream. Since this is an xml file, we read it as a text file using readAsText
and passing the buffer returned from the reader. At this point, we have an output stream of text that contains the contents of the xml file.
Next step is to parse that stream into something we can work with. To do that, we will create a new instance to of DOMParser
to handle that for us.
// Parse the Dom tree of the EXML File
const parser = new DOMParser();
const doc = parser.parseFromString(result, "application/xml");
Next, let's capture the root element of the document.
// The Root element of the DOM tree is a Data Element, so grab it.
const Data = doc.querySelector('Data');
This gives us the following...
<Data template="GcNGuiLayerData">
<Property name="ElementData" value="GcNGuiElementData.xml">
<Property name="ID" value="INVENTORY_BOX"/>
<Property name="PresetID" value=""/>
<Property name="IsHidden" value="False"/>
<Property name="ForcedStyle" value="TkNGuiForcedStyle.xml">
<Property name="NGuiForcedStyle" value="None"/>
</Property>
<Property name="Layout" value="GcNGuiLayoutData.xml">
{...}
</Data>
Notice that the Data
element has a template attribute. From my understanding, MBINCompiler uses static templates for each category of the PAK files that it processes. These templates are basically Struts or outlines for the data contained in each file or class within that file. If your familiar with using Struts in code, then you know what I mean. Think of it as type hinting for an object. (very high level view).
We can grab this value by simply doing the following:
// Grab the name of the template
const TemplateName = Data.getAttribute('template');
// Grab the header section of the display area
const displayHeader = display.querySelector('h2')
// Append that content to the display header section
header_appended.setAttribute('id', 'header_appended');
header_appended.textContent = ` for ${TemplateName}`;
displayHeader.appendChild(header_appended)
The value of this attribute is the template that was used to parse the original PAK file. Outside of that, the only other value that has for us is that since we have the root element of the DOM tree, we should now be able to traverse that tree.
To do that, we will grab all the child nodes of the Data
element and iterate over the results and put that into a definition list. From here, I will let the embedded comments handle the rest of describing what's going on.
for (const nodes of Data.childNodes) {
// weed out dummy #text nodes (contains newline charactor)
if (nodes.nodeName != "#text") {
// attach definition terms content to dt elements
let dt = document.createElement('dt');
dt.setAttribute('class', 'font-bold mt-2');
dt.textContent = `${nodes.getAttribute('name')} - ${nodes.getAttribute('value')}`;
if (nodes.hasChildNodes) {
for (node of nodes.childNodes) {
if (node.nodeName != "#text") {
// create the definition description element
// and set it's content
let dd = document.createElement('dd');
dd.setAttribute('class', 'font-thin');
dd.textContent = `Name: ${node.getAttribute('name')} Value: ${node.getAttribute('value')}\n`
// attach the description to the term
dt.appendChild(dd)
}
}
}
// attach the term to the list of defintions
list.appendChild(dt)
}
}
// Attach the completed defintion list to the display area
display.appendChild(list)
Although this does work. There are some parts that need a bit more work. I need check for child nodes on each iteration and capture those for display. Currently, that is not happening. Based on this finding, the use of the Definition List is no longer viable. Not to mention the current A11y issues with DL's, inject lists inside the description elements would be make it even worse. I need to work out a better way to display this data.
Basic mechanics being applied here are pretty simple, but a great exercise in DOM Parsing
- After reading the file into a Buffer, as Text, we use the Native DOMParser to recreate the original DOM structure of the XML file.
- Knowing that the root element of the EXML file is a Data element, we use a
querySelector
to grab it. From here, we have to meat of the document. - From here, it's a matter os asking if the current element we are iterating over is a Parent Element of a Child Element. We accomplish by checking for a Truthy value of the
.hasChildNodes()
method. - Once we have a Parent Element, we create a UL for it, which will be nested in the root OL. I used a OL to nest the UL to help create some seperation, and it was a simpler solution to creating nested divs to create cards. The Lists are broken down like so:
- The root Data elements child nodes are placed in the OL as list items.
- The list items of the OL are parent nodes to further data, so we create a UL to hold them and parse out their child nodes into li's.
- Before we could set the data in the li, we had to check for
#text
nodes, which merely holds a newline character (\n) and weed them out. - I used different grades of the same background color and padding offset to help differentiate between the differnt layers of parent and children data.
- bg-slate-50 (display area and OL, so they would blend together)
- bg-slate-100 (level 2)
- bg-slate-200 (level 3)
- bg-slate-300 (level 4)
All in all, this is not a great way to represent data on a page, but for this example it kinda works. In the future, I will explore better ways to displaying this data. But for now, I accomplished what I wanted. I can easily read each EXML file and decern what information it holds. Or doesn't hold most of the time.
I have appened an updated script block to show the refactor of the code that captures all 4 levels of Parent and Child nodes within the EXML files structure.