Add utility to inline includes in the XML data structure

This commit is contained in:
Jordan Eldredge 2019-06-13 19:21:19 -07:00
parent 77fbee8dc8
commit 0d61aa88c2
5 changed files with 4362 additions and 40 deletions

View file

View file

@ -2,44 +2,10 @@ import React from "react";
import JSZip from "jszip";
import "./App.css";
import { xml2js } from "xml-js";
import * as Utils from "./utils";
const SkinContext = React.createContext(null);
async function readXml(zip, file) {
// TODO: Handle case where file is not found
// TODO: Escape `file` for rejex characters
const text = await zip.file(new RegExp(file, "i"))[0].async("text");
return xml2js(text, { compact: false, elementsKey: "children" });
}
async function walkAsync(xml, visitor) {
await visitor(xml);
if (xml.children != null) {
await Promise.all(xml.children.map(child => walkAsync(child, visitor)));
}
return xml;
}
async function resolveIncludes(xml, zip) {
// TODO: Use _.memoize or similar
const includes = {};
async function readInclude(file) {
if (!includes[file]) {
includes[file] = readXml(zip, file);
}
return includes[file];
}
return walkAsync(xml, async node => {
if (node.name === "include") {
// TODO: Normalize file names so that they hit the same cache
const includeXml = await readInclude(node.attributes.file);
node.children = includeXml.children;
}
return xml;
});
}
async function getSkin() {
const resp = await fetch(
process.env.PUBLIC_URL + "/skins/CornerAmp_Redux.wal"
@ -47,10 +13,7 @@ async function getSkin() {
const blob = await resp.blob();
const zip = await JSZip.loadAsync(blob);
const skinXml = await readXml(zip, "skin.xml");
const resolved = await resolveIncludes(skinXml, zip);
console.log(resolved);
const player = zip.file("xml/player-elements.xml");
const player = Utils.getCaseInsensitveFile(zip, "xml/player-elements.xml");
const xml = await player.async("text");
const elementsDoc = await xml2js(xml, {
@ -66,7 +29,7 @@ async function getSkin() {
case "bitmap": {
const { file, gammagroup, h, id, w, x, y } = element.attributes;
// TODO: Escape file for regex
const img = zip.file(new RegExp(file, "i"))[0];
const img = Utils.getCaseInsensitveFile(zip, file);
const imgBlob = await img.async("blob");
const imgUrl = URL.createObjectURL(imgBlob);
images[id.toLowerCase()] = { file, gammagroup, h, w, x, y, imgUrl };

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,41 @@
import { xml2js } from "xml-js";
export function getCaseInsensitveFile(zip, filename) {
// TODO: Escape `file` for rejex characters
return zip.file(new RegExp(filename, "i"))[0];
}
// Read a
export async function readXml(zip, file) {
// TODO: Handle case where file is not found
const text = await getCaseInsensitveFile(zip, file).async("text");
return xml2js(text, { compact: false, elementsKey: "children" });
}
// Transform an xml tree structure by mapping over each node breadth first
// Parents are mapped before their children
// Children are mapped in parallel
export async function asyncTreeMap(xml, mapper) {
const mapped = await mapper(xml);
if (mapped.children == null) {
return mapped;
}
const promises = mapped.children.map(child => {
return asyncTreeMap(child, mapper);
});
const children = await Promise.all(promises);
return { ...mapped, children };
}
export async function inlineIncludes(xml, zip) {
return asyncTreeMap(xml, async node => {
if (node.name === "include") {
// TODO: Normalize file names so that they hit the same cache
// TODO: Ensure this node does not already have children for some reason
const { children } = await readXml(zip, node.attributes.file);
return { ...node, children };
}
return node;
});
}

View file

@ -0,0 +1,67 @@
import * as Utils from "./utils";
import JSZip from "jszip";
import { promises as fsPromises } from "fs";
import path from "path";
async function getSkinZip() {
const skinBuffer = await fsPromises.readFile(
path.join(__dirname, "../public/skins/CornerAmp_Redux.zip")
);
return JSZip.loadAsync(skinBuffer);
}
describe("getCaseInsensitiveFile", () => {
it("gets a file independent of case", async () => {
const zip = await getSkinZip();
expect(Utils.getCaseInsensitveFile(zip, "SkIn.XmL")).not.toEqual(null);
});
});
describe("readXml", () => {
it("gets a file independent of case", async () => {
const zip = await getSkinZip();
const xml = await Utils.readXml(zip, "SkIn.XmL");
expect(xml).toMatchSnapshot();
});
});
describe("asyncTreeMao", () => {
it("runs parents before children", async () => {
const callNodeNames = new Set();
const mapper = node => {
callNodeNames.add(node.name);
if (node.name === "root.2") {
const children = [{ name: "root.2.1" }];
return { ...node, children };
}
return node;
};
const structure = {
name: "root",
children: [{ name: "root.1" }, { name: "root.2" }, { name: "root.3" }],
};
const mappedStructure = await Utils.asyncTreeMap(structure, mapper);
expect(callNodeNames).toEqual(
new Set(["root", "root.1", "root.2", "root.2.1", "root.3"])
);
expect(mappedStructure).toEqual({
name: "root",
children: [
{ name: "root.1" },
{ name: "root.2", children: [{ name: "root.2.1" }] },
{ name: "root.3" },
],
});
});
});
describe("inlineIncludes", () => {
it("inlines the contents of included files as children of the include node", async () => {
const zip = await getSkinZip();
const xml = await Utils.readXml(zip, "SkIn.XmL");
const resolvedXml = await Utils.inlineIncludes(xml, zip);
expect(resolvedXml).toMatchSnapshot();
});
});