Embedding mini languages within string literals is often not the best way to model a problem domain. An alternative is to leverage the full power of Nim’s syntax. A templating system for convenient HTML tree generation is a good example here. But before we can outline the macro’s design we need to model the HTML tree: type
Tag* = enum 1
text, html, head, body, table, tr, th, td
TagWithKids = range[html..high(Tag)] 2
HtmlNode* = ref object 3
case tag: Tag
of text:
s: string
else:
kids: seq[HtmlNode]
proc newTextNode*(s: sink string): HtmlNode = 4
HtmlNode(tag: text, s: s)
proc newTree*(tag: TagWithKids; kids: varargs[HtmlNode]): HtmlNode = 5
HtmlNode(tag: tag, kids: @kids)
proc add*(parent: HtmlNode; kid: sink HtmlNode) = parent.kids.add kid from std / xmltree import addEscaped proc toString(n: HtmlNode; result: var string) = 6
case n.tag
of text:
result.addEscaped n.s
else:
result.add "<" & $n.tag
if n.kids.len == 0:
247
result.add " />"
else:
result.add ">\n"
for k in items(n.kids): toString(k, result)
result.add "\n</" & $n.tag & ">"
proc $*(n: HtmlNode): string = 7
result = newStringOfCap(1000)
toString n, result
1 Reduced list of possible HTML tags. 2 A subtype of Tag that covers the tags that have kids. 3 A tree of HTML modelled via a case object. 4 newTextNode constructs a single text node. 5 newTree constructs a node with a variable number of children. 6 toString is recursive and uses a var string parameter as its buffer to
write to. This var parameter is crucial for efficiency.
7 For convenience a dollar operator is provided that allocates a large buffer
and then calls toString to make effective use of this buffer.
newTextNode, newTree, and add are good enough to produce complex HTML tables: proc toTable(headers: openArray[string]; data: seq[seq[int]]): HtmlNode = 1
assert headers.len == data.len 2
var tab = newTree(table)
for i in 0..<data.len:
var row = newTree(tr, newTree(th, newTextNode(headers[i])))
for col in data[i]:
row.add newTree(td, newTextNode($col))
tab.add row 3
result = newTree(html, newTree(body, tab)) 4
1 toTable produces a 2 dimensional HTML table from headers and data. 2 We require that every column has a corresponding header. 3 We must not forget to append the temporary row to tab. 4 The table is wrapped inside .... This style of programming is low level and error prone; it is easy to forget to append row to tab, for example. Instead we would like to write the following: 248 proc toTable(headers: openArray[string]; data: seq[seq[int]]): HtmlNode =
assert headers.len == data.len
result = buildHtml:
body:
table:
for i in 0..<data.len:
tr:
th:
text headers[i]
for col in data[i]:
td:
text $col
The domain specific language should compose with ordinary Nim code, we want to be able to use ordinary if and for statements inside the HTML templating system. The required buildHtml macro needs to walk the passed AST recursively and introduce temporary variables for each if and for statement. Every enum value of TagWithKids is translated to a newTree call, a call to text is translated to newTextNode: import macros proc whichTag(n: NimNode): Tag = 1
for e in low(TagWithKids)..high(TagWithKids):
if n.eqIdent($e): return e 2
return text 3
proc traverse(n, dest: NimNode): NimNode = 4
if n.kind in nnkCallKinds: 5
if n[0].eqIdent("text"):
expectLen n, 2
result = newCall(bindSym"newTextNode", n[1]) 6
if dest != nil:
result = newCall(bindSym"add", dest, result)
else:
let tag = whichTag(n[0])
if tag == text:
result = copyNimNode(n) 7
result.add n[0]
for i in 1..<n.len:
result.add traverse(n[i], nil)
else:
let tmpTree = genSym(nskVar, "tmpTree") 8
result = newTree(nnkStmtList,
newVarStmt(tmpTree, newCall(bindSym"newTree", n[0])))
249
for i in 1..<n.len:
result.add traverse(n[i], tmpTree)
if dest != nil:
result.add newCall(bindSym"add", dest, tmpTree)
else:
result = copyNimNode(n) 9
for child in n:
result.add traverse(child, dest)
macro buildHtml(n: untyped): untyped = let tmpTree = genSym(nskVar, "tmpTree") var call = newCall(bindSym"newTree", bindSym"html") result = newTree(nnkStmtListExpr, newVarStmt(tmpTree, call)) result.add traverse(n, tmpTree) 10 result.add tmpTree 11 1 whichTag returns the tag that the call operation corresponds to. 2 body: is mapped to Tag.body etc. 3 It returns text if it is not any tag. 4 traverse does the bulk of the work. It traverses n and produces a modified copy of the AST. dest is the potential destination of where to attach the HtmlNode to. 5 If the node is any kind of “call expression” we examine if it is a call to the text operation. 6 If so, it translates text x to newTextNode(x). 7 If the call is not a call to a tag simply traverse n recursively. 8 If the call is a tag transform it into (var tmpTree = newTree(tag); translatedBody; dest.add tmpTree). 9 For any node that is not a call expression traverse n recursively. 10 buildHtml calls the traverse auxiliary proc. 11 buildHtml transforms n into (var tmpTree = newTree(html); traverse(n); tmpTree) which is an nnkStmtListExpr that produces a value of type HtmlNode so that it can be bound to a variable.