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
comp
isnil
, we ignore this component and follow the traversal with thecdr
ofcomps
,when
comp
is a string or a number, we updatetree
by appendingcomp
(as a string) to the left part oftree
, the right part staying the same,when
comp
is a list of components we put them at the same level of thecdr
ofcomps
, specifically we setcomps
to be(append comp (cdr comps))
,when
comp
is a tag component, two cases must be distinguished according to the value of(cdr comps)
:if
(cdr comps)
is non-nil:we update
tree
according to the value returned byjack-tag
and by dividing the right part oftree
into two parts,we push the components
(cdr comps)
on the stackrest
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)
), socomps
should 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 tocomps
the keyword:rest
,
if
(cdr comps)
is nil:it means that there is no part of
input-tree
to be added to the stackrest
at this iteration, sorest
stays unchanged,we just update
tree
according 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)
), socomps
is set to be those children,
when
comp
is the keyword:rest
, that means we have completed the traversal of a part ofinput-tree
, so:we update
tree
accordingly by appending the left part of the right part oftree
to 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 setcomps
to be the first element ofrest
and remove that first element fromrest
(this can be done like this(setq comps (pop rest))
),
when
comp
is 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:
:tree
hold the value oftree
,:rest
the value ofrest
,:comps
the value ofcomps
and,:comp
the 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
:
tree
is updated with the left part and right part of the tag returned byjack-tag
function,rest
stack staysnil
,the components of
input-tree
that 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 ofcomps
iscdr
of 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
cdr
of 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
:
tree
is 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-tag
and the right part being its current right part,we push the
cdr
ofcomps
to the stackrest
in 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"
), socomps
should 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 tocomps
the 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
:
tree
is 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
cdr
ofcomps
to the stackrest
in order to treat it after,because
(:hr)
has no children,comps
is 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
tree
by appending the left part of the right part oftree
to its left part, and we set its right part to be the right part of the right part oftree
,we set
comps
to be the most recently added list of components in the stackrest
that 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
tree
by appending the left part of the right part oftree
to its left part, and we set its right part to be the right part of the right part oftree
,we set
comps
to be the most recently added list of components in the stackrest
that 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>"