node tree

node tree
Photo by Florian Olivo / Unsplash

Let's try and code something simple. A tree of nodes:

Node (name: String, children: (Array of: Node))

We can likely use this right away to simulate HTML (without attributes):

document: Node =
  {name: 'html', children: (
    {name: 'head'},
    {name: 'body'})
  }

Let's keep going with the HTML analogy. We're going to at least need a text node variant.

Text (content: String);
Node (name: String, children: (Array of: Node + Text));

document :=
  {Node} {name: 'html', children: (
    {Node} {name: 'head'},
    {Node} {name: 'body', children: (
      {Text} {content: 'Hello World!'})
    )}
  }

We could make it more developer-friendly by having specific kinds of nodes to represent HTML, as there are specific kinds of attributes - as well as the generic ones - that we might want to add to those nodes.

Instead of making a big TaggedUnion of classes we can instead create a trait called Node and have specific kinds of things underneath that. There will be no requirements put on a Node to begin with making it a dead simple definition:

Node (methods: (,))
Text (#traits: (Node,), content: String)
Element (
  #traits: (Node,),
  #methods: ([name][-> String][],),
  children: (Array of: &Node))
[element name] [Element -> String] [element class name]

html (#traits: (Element,))
head (#traits: (Element,))
body (#traits: (Element,))

document :=
  {html} {children: (
    {head} {},
    {body} {children: (
      {Text} {content: 'Hello World'}
    )}
  )}

Creating the object graph works, but is it really the best way to make an object graph like this? Let's try a different approach - let's write what we'd like it to look like and work backwards from there.

Node := Map of: String to: Node + String
document := (Node) (
  html: (Node) (
    head: (Node) (,),
    body: (Node) (style: 'font-weight: bold', content: 'Hello World')))

That's very succinct. What happened? we threw out the notion of using objects and went with a simple Map. Note we require the ability to map Node back to itself in this example. That's an important compiler feature to keep in mind.

Attributes are a mapping of string to string and if we treat 'the content' of an element as a special attribute named 'content' (which may be there should be a better name - or may be there should be a type for) then we're basically done. We can create HTML very easily now.

Node := Map of: String to: Node + String

[node as-html-to: stream] [Node, Stream -> ø]
[
  node | [name, child] [
    stream write: `<~[name]>`
    child as-html-to: stream
    stream write: `</~[name]>`]
]

[node as-html-to: stream] [String, Stream -> ø]
[node | escape-html | stream]

[string escape-html] [String -> String]
[string replace-all: '&' with: '&amp;']

document := (Node) (
  html: (Node) (
    head: (Node) (,),
    body: (Node) (style: 'font-weight: bold', content: 'Hello World')))
html := document as-html-to: stdout

This isn't bad. It's doing a bunch of string processing that we don't necessarily want to do - it'd be better if we could stream out string chunks and have them combined together on a destination stream - such as stdout or the network. That could be useful in general. We can use coroutines to do this:

[node to-html] [node: Node -> yield: (Yield of: String)]
[node | [name, child] [
  yield <- `<~[name]>`;
  yield <- child next;
  yield <- '`</~[name]>`]]

[node to-html] [node: String -> yield: (Yield of: String)]
[yield <- node replace-all: '&' with: '&amp;']

[string replace-all: character with: replacement]
[String, Character, String -> yield: (Yield of: String)]
[string | [each] [
  each == character
    then: [yield <- replacement]
    else: [yield <- character]]]

It might work? it's not something that I can easily test as I don't have a working compiler or interpreter for it just yet. It's just a shot in the dark and really just an excuse to try and write something in STZ to see how the syntax holds up.

Honestly? not that bad. Not that bad at all. I do find it interesting how 'plain data' ends up being the easiest to work with rather than diving deeper in to objects. May be that will be a common recurring theme. We shall see.