mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-25 02:57:30 +00:00
Add utility to inline includes in the XML data structure
This commit is contained in:
parent
77fbee8dc8
commit
0d61aa88c2
5 changed files with 4362 additions and 40 deletions
0
experiments/modern/.watchmanconfig
Normal file
0
experiments/modern/.watchmanconfig
Normal 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 };
|
||||
|
|
|
|||
4251
experiments/modern/src/__snapshots__/utils.test.js.snap
Normal file
4251
experiments/modern/src/__snapshots__/utils.test.js.snap
Normal file
File diff suppressed because it is too large
Load diff
41
experiments/modern/src/utils.js
Normal file
41
experiments/modern/src/utils.js
Normal 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;
|
||||
});
|
||||
}
|
||||
67
experiments/modern/src/utils.test.js
Normal file
67
experiments/modern/src/utils.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue