The Problem

Design Options

  1. Embed commands in an existing language like EJS

  2. Create a mini-language with its own commands like Jekyll

  3. Put directives in specially-named attributes in the HTML

Three options for page templates
Figure 1: Three different ways to implement page templating.

What Does Done Look Like?

i
<html>
  <body>
    <ul z-loop="item:names">
      <li><span z-var="item"/></li>
    </ul>
  </body>
</html>

What Does Done Look Like?

i
<html>
<body>
<ul>
<li><span>Johnson</span></li>

<li><span>Vaughan</span></li>

<li><span>Jackson</span></li>
</ul>
</body>
</html>

How Do We Call This?

i
data = {"names": ["Johnson", "Vaughan", "Jackson"]}

dom = read_html("template.html")
expander = Expander(dom, data)
expander.walk()
print(expander.result)

Managing Variables

i
class Env:
    def __init__(self, initial):
        self.stack = [initial.copy()]

    def push(self, frame):
        self.stack.append(frame)

    def pop(self):
        self.stack.pop()

    def find(self, name):
        for frame in reversed(self.stack):
            if name in frame:
                return frame[name]
        return None

Visiting Nodes

i
class Visitor:
    def __init__(self, root):
        self.root = root

    def walk(self, node=None):
        if node is None:
            node = self.root
        if self.open(node):
            for child in node.children:
                self.walk(child)
        self.close(node)

    def open(self, node):
        raise NotImplementedError("open")

    def close(self, node):
        raise NotImplementedError("close")

Expanding a Template

i
class Expander(Visitor):
    def __init__(self, root, variables):
        super().__init__(root)
        self.env = Env(variables)
        self.handlers = HANDLERS
        self.result = []

Open…

i
    def open(self, node):
        if isinstance(node, NavigableString):
            self.output(node.string)
            return False
        elif self.hasHandler(node):
            return self.getHandler(node).open(self, node)
        else:
            self.showTag(node, False)
            return True

…and Close

i
    def close(self, node):
        if isinstance(node, NavigableString):
            return
        elif self.hasHandler(node):
            self.getHandler(node).close(self, node)
        else:
            self.showTag(node, True)

Managing Handlers

i
    def hasHandler(self, node):
        return any(
            name in self.handlers
            for name in node.attrs
        )

    def getHandler(self, node):
        possible = [
            name for name in node.attrs
            if name in self.handlers
        ]
        assert len(possible) == 1, "Should be exactly one handler"
        return self.handlers[possible[0]]

But What's a Handler?

i
def open(expander, node):
    expander.showTag(node, False)
    expander.output(node.attrs["z-num"])

def close(expander, node):
    expander.showTag(node, True)

Variables Are Similar

i
def open(expander, node):
    expander.showTag(node, False)
    expander.output(expander.env.find(node.attrs["z-var"]))

def close(expander, node):
    expander.showTag(node, True)

Testing

i
import json
import sys
from bs4 import BeautifulSoup
from expander import Expander

def main():
    with open(sys.argv[1], "r") as reader:
        variables = json.load(reader)

    with open(sys.argv[2], "r") as reader:
        doc = BeautifulSoup(reader.read(), "html.parser")
        template = doc.find("html")

    expander = Expander(template, variables)
    expander.walk()
    print(expander.getResult())

if __name__ == "__main__":
    main()

Static Text

i
<html>
  <body>
    <h1>Static Text</h1>
    <p>test</p>
  </body>
</html>
i
<html>
<body>
<h1>Static Text</h1>
<p>test</p>
</body>
</html>

Constants

i
<html>
  <body>
    <p><span z-num="123"/></p>
  </body>
</html>
i
<html>
<body>
<p><span>123</span></p>
</body>
</html>

Variables

i
<html>
  <body>
    <p><span z-var="varName"/></p>
  </body>
</html>
i
<html>
<body>
<p><span>varValue</span></p>
</body>
</html>

Conditionals

i
def open(expander, node):
    check = expander.env.find(node.attrs["z-if"])
    if check:
        expander.showTag(node, False)
    return check

def close(expander, node):
    if expander.env.find(node.attrs["z-if"]):
        expander.showTag(node, True)

Testing

i
<html>
  <body>
    <p z-if="yes">Should be shown.</p>
    <p z-if="no">Should <em>not</em> be shown.</p>
  </body>
</html>
i
<html>
<body>
<p>Should be shown.</p>

</body>
</html>

Loops

  1. Create a new stack frame holding the current value of the loop variable

  2. Expand all of the node's children with that stack frame in place

  3. Pop the stack frame to get rid of the temporary variable

Loops

i
def open(expander, node):
    index_name, target_name = node.attrs["z-loop"].split(":")
    expander.showTag(node, False)
    target = expander.env.find(target_name)
    for value in target:
        expander.env.push({index_name: value})
        for child in node.children:
            expander.walk(child)
        expander.env.pop()
    return False

def close(expander, node):
    expander.showTag(node, True)

Testing

i
<html>
  <body>
    <ul z-loop="item:names">
      <li><span z-var="item"/></li>
    </ul>
  </body>
</html>
i
<html>
<body>
<ul>
<li><span>Johnson</span></li>

<li><span>Vaughan</span></li>

<li><span>Jackson</span></li>
</ul>
</body>
</html>

Next Steps

Summary

Concept map for page templates
Figure 2: Concept map.