Skip to content

Instantly share code, notes, and snippets.

@unisys12
Last active February 11, 2023 14:34
Show Gist options
  • Save unisys12/055ea3e29caed2611081145605c3a294 to your computer and use it in GitHub Desktop.
Save unisys12/055ea3e29caed2611081145605c3a294 to your computer and use it in GitHub Desktop.
Parse EXML File and Display

Parsing EXML File from MBINCompiler

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.

Loose Property Structure

  • 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)

Conclusion

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.

Updated Script Block

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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<title>Parse File</title>
</head>
<body class="subpixel-antialiased">
<header class="flex justify-center">
<h1 class="text-3xl">Upload a File to Parse</h1>
</header>
<main class="flex flex-col justify-center">
<div class="container mx-auto">
<form method="post" enctype="multipart/form-data">
<label for="file">Please upload a file to parse: </label>
<input type="file" name="file" id="upload">
</form>
</div>
<div class="container mx-auto bg-slate-200 p-4 mt-4" id="display">
<h2 class="text-2xl">Display Area</h2>
</div>
</main>
<script>
const display = document.getElementById('display');
const file = document.getElementById('upload');
const displayHeader = display.querySelector('h2')
file.addEventListener('change', () => {
const file_buffer = file.files[0];
// refresh appended header on file change
if (display.querySelector('h2 > span')) {
displayHeader.removeChild(header_appended)
}
// Create the definition list, to prepare for population
// after reading the uploaded file
const list = document.createElement("dl");
// query the dl that was created. Because capturing `list`
// does not help in removing the element later.
// NULL on page load and refresh.
// Will exist on file change/upload
let dlist = document.querySelector('dl')
// refresh the appended defintion terms and descriptions
// on file change/upload. Causes screen flash.
if (dlist) {
display.removeChild(dlist)
}
const reader = new FileReader();
reader.addEventListener('load', (buffer) => {
let file_data = new Map();
let result = buffer.target.result;
// Parse the Dom tree of the EXML File
const parser = new DOMParser();
const doc = parser.parseFromString(result, "application/xml");
// The Root element of the DOM tree is a Data Element, so grab it.
const Data = doc.querySelector('Data');
// Grab the name of the template
const TemplateName = Data.getAttribute('template');
// Append the template name to the H2 for the display area
const header_appended = document.createElement('span');
header_appended.setAttribute('id', 'header_appended');
header_appended.textContent = ` For ${TemplateName}`;
displayHeader.appendChild(header_appended)
// Gather all parent Property elements
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)
}
}
})
// Read the upload file as a Text String
reader.readAsText(file_buffer)
// Attach the completed defintion list to the display area
display.appendChild(list)
})
</script>
</body>
</html>
// This is the updated script block, after a refactor of the code above. It could possibly be refactored again,
// but works and is far cleaner than the previous first attenpt.
<script>
const display = document.getElementById('display');
const file = document.getElementById('upload');
const displayHeader = display.querySelector('h2')
file.addEventListener('change', () => {
const file_buffer = file.files[0];
// refresh appended header on file change
if (display.querySelector('h2 > span')) {
displayHeader.removeChild(header_appended)
}
// Create the definition list, to prepare for population
// after reading the uploaded file
const list = document.createElement("ol");
list.setAttribute('class', 'list-decimal px-4')
// query the dl that was created. Because capturing `list`
// does not help in removing the element later.
// NULL on page load and refresh.
// Will exist on file change/upload
let ol = document.querySelector('ol')
// refresh the appended defintion terms and descriptions
// on file change/upload. Causes screen flash.
if (ol) {
display.removeChild(ol)
}
const reader = new FileReader();
reader.addEventListener('load', (buffer) => {
// Set the buffer result to a variable
let result = buffer.target.result;
// Parse the Dom tree of the EXML File
const parser = new DOMParser();
const doc = parser.parseFromString(result, "application/xml");
// The Root element of the DOM tree is a Data Element, so grab it.
const Data = doc.querySelector('Data');
// Grab the name of the template
const TemplateName = Data.getAttribute('template');
// Append the template name to the H2 for the display area
const header_appended = document.createElement('span');
header_appended.setAttribute('id', 'header_appended');
header_appended.textContent = ` For ${TemplateName}`;
displayHeader.appendChild(header_appended)
// Gather all parent Property elements
for (const nodes of Data.childNodes) {
// weed out dummy #text nodes (contains newline charactor)
if (nodes.nodeName != "#text") {
let li = document.createElement('li');
// attach definition terms content to dt elements
li.setAttribute('class', 'font-bold text-slate-50 mt-2');
li.textContent = `${nodes.getAttribute('name')} - ${nodes.getAttribute('value')}`;
list.appendChild(li)
// Rinse, repeat over & over
if (nodes.hasChildNodes()) {
for (const childNode of nodes.childNodes) {
if (childNode.nodeName != "#text") {
let childUL = document.createElement('ul')
let childLI = document.createElement('li')
childUL.setAttribute('class', 'font-medium pl-2 bg-slate-50')
childLI.setAttribute('class', `pl-4 text-slate-900`)
childLI.textContent = `Name: ${childNode.getAttribute('name')} Value=${childNode.getAttribute('value')}`
childUL.appendChild(childLI)
li.appendChild(childUL)
if (childNode.hasChildNodes()) {
for (const cn of childNode.childNodes) {
if (cn.nodeName != "#text") {
let GrandChildUL = document.createElement('ul')
let GrandChildLI = document.createElement('li')
GrandChildUL.setAttribute('class', 'font-medium pl-2 mx-4 bg-slate-100')
GrandChildLI.setAttribute('class', `pl-8 text-slate-900`)
GrandChildLI.textContent = `Name: ${cn.getAttribute('name')} Value=${cn.getAttribute('value')}`
GrandChildUL.appendChild(GrandChildLI)
childUL.appendChild(GrandChildUL)
if (cn.hasChildNodes()) {
for (const gcn of cn.childNodes) {
if (gcn.nodeName != "#text") {
let GreatGrandChildUL = document.createElement('ul')
let GreatGrandChildLI = document.createElement('li')
GreatGrandChildUL.setAttribute('class', 'font-medium pl-2 bg-slate-200')
GreatGrandChildLI.setAttribute('class', `pl-10 text-slate-900`)
GreatGrandChildLI.textContent = `Name: ${gcn.getAttribute('name')} Value=${gcn.getAttribute('value')}`
GreatGrandChildUL.appendChild(GreatGrandChildLI)
GrandChildUL.appendChild(GreatGrandChildUL)
if (gcn.hasChildNodes()) {
for (const ggcn of gcn.childNodes) {
if (ggcn.nodeName != "#text") {
let GreatGreatGrandChildUL = document.createElement('ul')
let GreatGreatGrandChildLI = document.createElement('li')
GreatGreatGrandChildUL.setAttribute('class', 'pl-2 bg-slate-300')
GreatGreatGrandChildLI.setAttribute('class', `ml-12 text-slate-900`)
GreatGreatGrandChildLI.textContent = `Name: ${ggcn.getAttribute('name')} Value=${gcn.getAttribute('value')}`
GreatGreatGrandChildUL.appendChild(GreatGreatGrandChildLI)
GrandChildUL.appendChild(GreatGreatGrandChildUL)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
})
// Read the upload file as a Text String
reader.readAsText(file_buffer)
// Attach the completed defintion list to the display area
display.appendChild(list)
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment