Transform a tree

In the last step, we defined an in-memory JavaScript object to hold the set of markdown content we want to convert to HTML. We wrapped the object in the async tree interface so that other code can access that content as an abstract tree, without the need to know specifically how and where that content is stored.

With that in place, we can now write code to transform the tree of markdown into a corresponding tree of HTML.

Visualize the transformation

In our markdown-to-HTML transformation, we will create a virtual tree of HTML content based on the real tree of markdown content. The trees will have the same shape, but the keys and values will both differ.

g Hello, **Alice**. -> Hello, **Bob**. -> Hello, **Carol**. ->
g Alice.html <p>Hello, <strong>Alice</strong>.</p> ->Alice.html Alice.html Bob.html <p>Hello, <strong>Bob</strong>.</p> ->Bob.html Bob.html Carol.html <p>Hello, <strong>Carol</strong>.</p> ->Carol.html Carol.html
Tree of markdown content
Virtual tree of HTML

The markdown tree is “real”, in the sense that it is persistently stored in some fashion as object.js. In the code that follows, we’ll create a new tree which is virtual as it exists only while the code is running.

Rough in the transformation

The transformation will be a function that accepts an async tree of markdown and returns a new async tree of HTML.

/* src/flat/transform.js */

import { marked } from "marked";

export default function (tree) {
  return {
    async keys() {
      /* TODO */
    async get(key) {
      /* TODO */

To translate markdown to HTML, we will use the marked markdown processor. Other markdown processors would work equally well.

Transform the keys

The first step is to transform the extension on the keys from .md to .html.

When dealing with content, we often use an extension in a name as a type signature to indicate the type of data contained therein. In this case, we want the keys of the transformed tree to reflect the fact its contents are HTML.

    async keys() {
      const markdownKeys = Array.from(await tree.keys());
      const htmlKeys = => key.replace(/.md$/, ".html"));
      return htmlKeys;

The transformed tree’s keys method iterates over the keys of the inner tree. If a key ends in .md, the extension will be replaced with .html. For example, if the inner tree has a key, the transformed tree will return the key Alice.html.

Transform the values

The second step is to transform the markdown values into HTML values.

    async get(key) {
      if (key.endsWith(".html")) {
        const markdownKey = key.replace(/.html$/, ".md");
        const markdown = await tree.get(markdownKey);
        if (markdown) {
          return marked(markdown.toString());
      } else {
        return tree.get(key);

The get function is given a key, most likely one ending in .html. This function will then ask the underlying markdown tree for a corresponding markdown file. If asked for foo.html, it asks the markdown tree for

When it comes to keys, the get function is working in the opposite direction of the keys method. The keys method maps a markdown key to an HTML key so that it can enumerate what HTML keys it virtually contains. The get function maps an HTML key to a markdown key so that it can retrieve the corresponding markdown content.

If the markdown tree actually returns a value, we cast that value to a string. In the object tree we’re using for markdown content at this point, the value will already be a string. But in the general case, we’d like to handle any value type that can be cast to a string. We then pass the markdown string to the marked function to get the corresponding HTML.

If the HTML tree is asked for something that doesn’t end in .html, it simply forwards that request to the inner tree and returns the result.

Test the transform

We can now verify that our transform is working as expected by adapting the same tests we used to verify our object tree. The only changes are to: 1) expect .html keys instead of .md keys, and 2) expect HTML content instead of markdown content.

/* src/flat/transform.test.js */

import assert from "node:assert";
import test from "node:test";
import object from "./object.js";
import transform from "./transform.js";

const tree = transform(object);

test("can get the keys of the tree", async () => {
  assert.deepEqual(await tree.keys(), ["Alice.html", "Bob.html", "Carol.html"]);

test("can get the value for a key", async () => {
  const alice = await tree.get("Alice.html");
  assert.equal(alice, "<p>Hello, <strong>Alice</strong>.</p>\n");

test("getting a non-existent value returns undefined", async () => {
  const david = await tree.get("David.html");
  assert.equal(david, undefined);

From inside the src/flat directory, run the transform tests:

$ node transform.test.js# tests 3
# pass 3
# fail 0

Display the transformed tree

We can bake the transform and the object together to create a final HTML tree.

/* src/flat/htmlObject.js */

import tree from "./object.js";
import transform from "./transform.js";

export default transform(tree);

You can think of this transform function as a function that takes a tree of markdown and returns a virtual tree of HTML. But keep in mind that the virtual tree is actually asynchronous. The transform function does not do any substantive work when it is called above.

One way to think about this virtual HTML tree is that it represents a tree of promises for the real work. The real work — to enumerate the HTML keys, and to get the values of those keys — will only be performed when we actually traverse the tree.

Use the same json utility we wrote earlier to dump this transformed tree to the console.

$ node json htmlObject.js
  "Alice.html": "<p>Hello, <strong>Alice</strong>.</p>\n",
  "Bob.html": "<p>Hello, <strong>Bob</strong>.</p>\n",
  "Carol.html": "<p>Hello, <strong>Carol</strong>.</p>\n"

The json utility traverses the virtual HTML tree, causing it to do its work of transforming markdown to HTML as the utility builds an in-memory object it ultimately displays as JSON. You can see that the displayed JSON has the desired shape, keys, and values as the virtual HTML tree in the diagram at the top of this page.


Next: File trees