Most pages on a site share some content
Many pages want to be customized based on data
So many sites use a templating system
Embed commands in an existing language like EJS
Create a mini-language with its own commands like Jekyll
Put directives in specially-named attributes in the HTML
<html>
<body>
<ul z-loop="item:names">
<li><span z-var="item"/></li>
</ul>
</body>
</html>
z-loop: repeat this
z-num: a constant number
z-var: fill in a variable
z-if: conditional
<html>
<body>
<ul>
<li><span>Johnson</span></li>
<li><span>Vaughan</span></li>
<li><span>Jackson</span></li>
</ul>
</body>
</html>
data = {"names": ["Johnson", "Vaughan", "Jackson"]}
dom = read_html("template.html")
expander = Expander(dom, data)
expander.walk()
print(expander.result)
data would come from a configuration file or databaseChainMap, but we'll write our ownclass 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
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")
class Expander(Visitor):
def __init__(self, root, variables):
super().__init__(root)
self.env = Env(variables)
self.handlers = HANDLERS
self.result = []
The environment
Handlers for our special node types
The result (strings we'll concatenate at the end)
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
If this is text, "display" it
If this is a special node, run a function
Otherwise, show the opening tag
Return value is "do we proceed"?
def close(self, node):
if isinstance(node, NavigableString):
return
elif self.hasHandler(node):
self.getHandler(node).close(self, node)
else:
self.showTag(node, True)
Handlers come in open/close pairs
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]]
hasHandler looks for attributes with special names
getHandler gets the one we need
def open(expander, node):
expander.showTag(node, False)
expander.output(node.attrs["z-num"])
def close(expander, node):
expander.showTag(node, True)
A module with open and close functions
None of our handlers need state, so we don't need objects
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)
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()
<html>
<body>
<h1>Static Text</h1>
<p>test</p>
</body>
</html>
<html>
<body>
<h1>Static Text</h1>
<p>test</p>
</body>
</html>
<html>
<body>
<p><span z-num="123"/></p>
</body>
</html>
<html>
<body>
<p><span>123</span></p>
</body>
</html>
<html>
<body>
<p><span z-var="varName"/></p>
</body>
</html>
<html>
<body>
<p><span>varValue</span></p>
</body>
</html>
{"varName": "varValue"}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)
The handler determines whether to show this tag and go deeper
What if the variable's value changes between opening and closing?
<html>
<body>
<p z-if="yes">Should be shown.</p>
<p z-if="no">Should <em>not</em> be shown.</p>
</body>
</html>
<html>
<body>
<p>Should be shown.</p>
</body>
</html>
{"yes": True, "no": False}Create a new stack frame holding the current value of the loop variable
Expand all of the node's children with that stack frame in place
Pop the stack frame to get rid of the temporary variable
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)
<html>
<body>
<ul z-loop="item:names">
<li><span z-var="item"/></li>
</ul>
</body>
</html>
<html>
<body>
<ul>
<li><span>Johnson</span></li>
<li><span>Vaughan</span></li>
<li><span>Jackson</span></li>
</ul>
</body>
</html>
The z-if issue might mean we need state after all
Tackle that before going any further
And figure out how to do unit testing