jack-html implementation details
Summary
In this post, we show how jack-html processes the following component
'(:div/foo.bar
"A"
(:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F"))
in order to return the following string
"<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>DEF</div>"
that we can use to produce this following html snippet:
<div id="foo" class="bar">
A
<p class="baz">B<hr />C</p>
DEF
</div>
Description of the program
In this section we call input-tree the data structure jack-html
receives as input.
For instance, in the form
(jack-html '(:div.foo (:p "bar-1" (:span "baz") "bar-2")))
the data structure
'(:div.foo (:p "bar-1" (:span "baz") "bar-2"))
is the input-tree.
With that said, let's describe our program.
While traversing input-tree (DFS algorithm), we builds a tree
containing a left part and a right part that represents the html
string to be returned at the end of the traversal.
While the left part of this tree is always a string, the right part can grow recursively by dividing it into two parts (left and right).
In the following example, we can see the successive states of the
this tree when input-tree is '(:div.foo (:p "bar-1" (:span "baz")
"bar-2")):
(:left ""
:right "")
(:left "<div class=\"foo\">"
:right "</div>")
(:left "<div class=\"foo\"><p>"
:right "</p></div>")
(:left "<div class=\"foo\"><p>bar-1"
:right "</p></div>")
(:left "<div class=\"foo\"><p>bar-1<span>"
:right (:left "</span>"
:right "</p></div>"))
(:left "<div class=\"foo\"><p>bar-1<span>baz"
:right (:left "</span>"
:right "</p></div>"))
(:left "<div class=\"foo\"><p>bar-1<span>baz</span>"
:right "</p></div>")
(:left "<div class=\"foo\"><p>bar-1<span>baz</span>bar-2"
:right "</p></div>")
When we have completly traversed input-tree, we return the
concatenation of the left part and the right part of tree.
Specifically, at the beginning of the programs we set the variables
tree, rest, comps and comp that define the state of the program.
tree is the tree we build a each iteration that contains the elements
needed to construct the final returned value.
rest is the stack that contains the parts of input-tree left out to be
processed later.
comps is the current list of components being treated. Components in
comps are at the same level in input-tree.
comp is the current component (always equal to the first element of
comps) that determines how we must update the state (tree, rest and
comps) of the program at each iteration:
when
compisnil, we ignore this component and follow the traversal with thecdrofcomps,when
compis a string or a number, we updatetreeby appendingcomp(as a string) to the left part oftree, the right part staying the same,when
compis a list of components we put them at the same level of thecdrofcomps, specifically we setcompsto be(append comp (cdr comps)),when
compis a tag component, two cases must be distinguished according to the value of(cdr comps):if
(cdr comps)is non-nil:we update
treeaccording to the value returned byjack-tagand by dividing the right part oftreeinto two parts,we push the components
(cdr comps)on the stackrestand,the components to be treated in the next iteration are the children of the tag component
comp(which can be either(cdr comp)or(cddr comp)), socompsshould be set to contains those children only, but as we need to remember that once those children has been treated we have to treat the element added to the stackrest, we also append tocompsthe keyword:rest,
if
(cdr comps)is nil:it means that there is no part of
input-treeto be added to the stackrestat this iteration, soreststays unchanged,we just update
treeaccording to the value returned byjack-tag(without dividing the right part oftree) and,the components to be treated in the next iteration are the children of the tag component
comp(which can be either(cdr comp)or(cddr comp)), socompsis set to be those children,
when
compis the keyword:rest, that means we have completed the traversal of a part ofinput-tree, so:we update
treeaccordingly by appending the left part of the right part oftreeto its left part, and we set its right part to be the right part of the right part oftree,now we have to treat the most recently added list of components in the stack
rest. To do so, we setcompsto be the first element ofrestand remove that first element fromrest(this can be done like this(setq comps (pop rest))),
when
compis any other object, we skip it or we raise an error depending on the variablejack-html-raise-error-p.
Finally, when comp is nil and (cdr comps) too, it means that we have
completly traversed input-tree, no more iterations are needed and we
return the concatenation of the left part and the right part of tree.
Running the program step by step
Let's go through each iteration that the following evaluation implies by printing out and commenting the successive states of our program:
(jack-html
'(:div/foo.bar
"A"
(:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F")))
input-tree, tree, rest, comps and comp have the same meaning as in the
previous section.
And, each state of the program is representing as a plist where:
:treehold the value oftree,:restthe value ofrest,:compsthe value ofcompsand,:compthe value ofcomp.
First the state is initialized like this (before entering in the while
loop):
(:tree (:left ""
:right "")
:rest nil
:comps ((:div/foo.bar
"A"
(:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F")))
:comp (:div/foo.bar
"A"
(:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F")))
As comp (equal to (:div/foo.bar ...)) is a tag component, and is the
only component in comps:
treeis updated with the left part and right part of the tag returned byjack-tagfunction,reststack staysnil,the components of
input-treethat should be treated in the next iteration are the children of the tagcomp, which are the components at the same level of the string component"A", specifically, the new value ofcompsiscdrof the currentcomp,
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">"
:right "</div>")
:rest nil
:comps ("A"
(:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F"))
:comp "A")
As comp (equal to "A") is a string component,
we append
"A"to the left part oftree,and we iterate on the
cdrof the currentcomps(it means on the next components at the same level of"A"component),
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A"
:right "</div>")
:rest nil
:comps ((:p (@ :class "baz") "B" (:hr) "C")
("D" "E" "F"))
:comp (:p (@ :class "baz") "B" (:hr) "C"))
As comp (equal to (:p (@ :class "baz") ...)) is a tag component but
not the only components in comps:
treeis updated with the left part being the concatenation of its current left part and the left part of the tag returned byjack-tag, and the right part is a subtree with its left part being the right part of the tag return byjack-tagand the right part being its current right part,we push the
cdrofcompsto the stackrestin order to treat it after,the components to be treated in the next iteration are the children of the tag component
comp(which are the components at the same level of the string component"B"), socompsshould be set to contains those children only, but as we need to remember that once those children has been treated we have to treat the element added to the stackrest, we also append tocompsthe keyword:rest,
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">"
:right (:left "</p>"
:right "</div>"))
:rest ((("D" "E" "F")))
:comps ("B" (:hr) "C" :rest)
:comp "B")
As comp (equal to "B") is a string component, we do the same thing we
did before for the string component "A" (note that the right part of
tree and rest are unchanged).
So the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B"
:right (:left "</p>"
:right "</div>"))
:rest ((("D" "E" "F")))
:comps ((:hr) "C" :rest)
:comp (:hr))
As comp (equal to (:hr)) is a tag component but not the only
components in comps:
treeis updated with the left part being the concatenation of its current left part and the left part of the tag returned byjack-tag, and the right part is a subtree with its left part being the empty string""(because(:hr)is a void tag) and the right part being its current right part,we push the
cdrofcompsto the stackrestin order to treat it after,because
(:hr)has no children,compsis the empty list to which we append the keyword:rest(for the same reason as before),
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />"
:right (:left ""
:right (:left "</p>"
:right "</div>")))
:rest (("C" :rest) (("D" "E" "F")))
:comps (:rest)
:comp :rest)
As comp is equal to the keyword :rest,
we update
treeby appending the left part of the right part oftreeto its left part, and we set its right part to be the right part of the right part oftree,we set
compsto be the most recently added list of components in the stackrestthat we also remove from the stack,
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />"
:right (:left "</p>"
:right "</div>"))
:rest ((("D" "E" "F")))
:comps ("C" :rest)
:comp "C")
As comp (equal to "C") is a string component, we do the same thing we
did before for the string components "A" and "B" (note that the right
part of tree and rest are unchanged).
So the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C"
:right (:left "</p>"
:right "</div>"))
:rest ((("D" "E" "F")))
:comps (:rest)
:comp :rest)
As comp is equal to the keyword :rest,
we update
treeby appending the left part of the right part oftreeto its left part, and we set its right part to be the right part of the right part oftree,we set
compsto be the most recently added list of components in the stackrestthat we also remove from the stack,
so the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>"
:right "</div>")
:rest nil
:comps (("D" "E" "F"))
:comp ("D" "E" "F"))
As comp (equal to ("D" "E" "F")) is a list of components,
we put them at the same level of the cdr of comps, specifically we set
comps to be (append comp (cdr comps)). Note that tree is unchanged.
So the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>"
:right "</div>")
:rest nil
:comps ("D" "E" "F")
:comp "D")
As comp (equal to "D") is a string component, we do the same thing we
did before for the string components "A", "B" and "C" (note that the
right part of tree and rest are unchanged).
So the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>D"
:right "</div>")
:rest nil
:comps ("E" "F")
:comp "E")
As comp (equal to "E") is a string component, we do the same thing we
did before for the string components "A", "B", "C" and "D" (note that
the right part of tree and rest are unchanged).
So the new state is:
(:tree
(:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>DE"
:right "</div>")
:rest nil
:comps ("F")
:comp "F")
As comp (equal to "F") is a string component, we do the same thing we
did before for the string components "A", "B", "C", "D" and "F" (note
that the right part of tree and rest are unchanged).
So the new state is:
(:tree (:left "<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>DEF"
:right "</div>")
:rest nil
:comps nil
:comp nil)
At that point, as comp is nil and (cdr comps) too, we get out of the
loop.
That means that we have completely traversed input-tree.
And now tree contains all the data we need to produce the html
string.
Eventually, we return the concatenation of the left part and the right
part of tree.
"<div id=\"foo\" class=\"bar\">A<p class=\"baz\">B<hr />C</p>DEF</div>"