diff --git a/experiments/modern/src/utils.js b/experiments/modern/src/utils.js index 70428b56..af5cf23a 100644 --- a/experiments/modern/src/utils.js +++ b/experiments/modern/src/utils.js @@ -23,11 +23,43 @@ export async function readUint8array(zip, filepath) { return file.async("uint8array"); } -// Transform an xml tree structure by mapping over each node breadth first +function flatten(arr) { + const newArr = []; + arr.forEach(item => { + if (Array.isArray(item)) { + newArr.push(...item); + } else { + newArr.push(item); + } + }); + return newArr; +} + +// Note: Bad things will happen if the root node you pass returns an array. In +// our use case, the root node never returns an array. +// Note: Because children are processed in parallel, the order in which nodes +// are processed can be a bit confusing. The only guarantee we make is that +// children will be processed before their parent. +export async function asyncDepthFirstFlatMap(node, mapper) { + const { children } = node; + if (children == null) { + return mapper(node); + } + + const promisedChildren = children.map(child => + asyncDepthFirstFlatMap(child, mapper) + ); + const mappedChildren = await Promise.all(promisedChildren); + const flatChildren = flatten(mappedChildren); + + return mapper({ ...node, children: flatChildren }); +} + +// Transform an 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); +export async function asyncTreeMap(node, mapper) { + const mapped = await mapper(node); if (mapped.children == null) { return mapped; } @@ -41,20 +73,20 @@ export async function asyncTreeMap(xml, mapper) { 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 includedFile = await readXml(zip, node.attributes.file); - if (includedFile == null) { - console.warn( - `Tried to include a file that could not be found: ${ - node.attributes.file - }` - ); - return node; - } - return { ...node, children: includedFile.children }; + if (node.name !== "include") { + return node; } - return node; + // TODO: Normalize file names so that they hit the same cache + // TODO: Ensure this node does not already have children for some reason + const includedFile = await readXml(zip, node.attributes.file); + if (includedFile == null) { + console.warn( + `Tried to include a file that could not be found: ${ + node.attributes.file + }` + ); + return node; + } + return { ...node, children: includedFile.children }; }); } diff --git a/experiments/modern/src/utils.test.js b/experiments/modern/src/utils.test.js index 33c5a6f7..18a572a5 100644 --- a/experiments/modern/src/utils.test.js +++ b/experiments/modern/src/utils.test.js @@ -25,7 +25,7 @@ describe("readXml", () => { }); }); -describe("asyncTreeMao", () => { +describe("asyncTreeMap", () => { it("runs parents before children", async () => { const callNodeNames = new Set(); const mapper = node => { @@ -65,3 +65,37 @@ describe("inlineIncludes", () => { expect(resolvedXml).toMatchSnapshot(); }); }); + +describe("asyncDepthFirstFlatMap", () => { + test("encounters children first", async () => { + const encounterd = []; + const mapper = async node => { + encounterd.push(node.name); + if (node.replaceWithChildren) { + return node.children; + } + return { ...node, name: node.name.toLowerCase() }; + }; + + const start = { + name: "A", + children: [ + { name: "B" }, + { + name: "C", + children: [ + { name: "E" }, + { name: "F", replaceWithChildren: true, children: [{ name: "G" }] }, + ], + replaceWithChildren: true, + }, + { name: "D" }, + ], + }; + expect(await Utils.asyncDepthFirstFlatMap(start, mapper)).toEqual({ + name: "a", + children: [{ name: "b" }, { name: "e" }, { name: "g" }, { name: "d" }], + }); + expect(encounterd).toEqual(["B", "E", "G", "D", "F", "C", "A"]); + }); +});