The last section noted that, in the context of our markdown-to-HTML problem, it’s possible to conceptualize the markdown content as a tree:
This section introduces an interface suitable for working with such a tree, regardless of its underlying data representation.
AsyncTree
Let’s identify a minimal interface sufficient to define a wide variety of trees. This interface, which we’ll call the AsyncTree interface, has two parts:
- A method which produces the keys of the tree. In the tree above, the keys are
Alice.md
,Bob.md
, andCarol.md
. The keys will often be strings, but don’t have to be strings. - A method which gets the value for a given key. If we ask the above tree for
Alice.md
, we want to get backHello, **Alice**.
Here the value we get is text, but like the keys, the values can be of any data type.
In code, an implementation of the AsyncTree interface looks like this:
// An async tree of key-value dictionaries
const tree = {
// Get the value of a given key.
async get(key) { ... }
// Iterate over this tree node's keys.
async keys() { ... }
}
Notes:
The
keys
method must return an iterator: an object that can produce a sequence of values. The simplest way to meet this requirement is to a JavaScriptArray
orSet
, which provide built-in support for the iterator protocol.Both functions in the
Explorable
interface are marked with the async keyword, indicating that they are asynchronous functions. In practice, the functions may return immediately, but they have the potential, at least, to do work that will require a bit of time: retrieving data from the file system, accessing data from a network, or performing long calculations.The
keys
method does not have to return all the keys supported byget
! There may be keys thatget
can handle that thekeys
will not include. This turns out to be useful in a number of situations.An async tree’s
get
method is expected to returnundefined
if the key is not present in the tree.
Apply the AsyncTree interface to the object
Of the three data representations we looked at previously, the in-memory JavaScript object was perhaps the simplest, so let’s first look at applying the AsyncTree interface to a JavaScript object:
/* src/flat/object.js */
const obj = {
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
};
export default {
async get(key) {
return obj[key];
},
async keys() {
return Object.keys(obj);
},
};
This module exports an async tree that wraps the JavaScript object containing the markdown data. For now, this wrapper can only handle a flat object — later we will extend this to handle hierarchical objects.
Test the object tree
The first thing we can do with this object tree is programmatically verify it implements the AsyncTree interface.
/* src/flat/object.test.js */
import assert from "node:assert";
import test from "node:test";
import tree from "./object.js";
test("can get the keys of the tree", async () => {
assert.deepEqual(await tree.keys(), ["Alice.md", "Bob.md", "Carol.md"]);
});
test("can get the value for a key", async () => {
const alice = await tree.get("Alice.md");
assert.equal(alice, "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
assert.equal(await tree.get("xyz"), undefined);
});
From inside the src/flat
directory, run these tests to see that all test pass:
$ cd ../flat
$ node object.test.js
…
# tests 3
# pass 3
# fail 0
Next: Display a tree »