5.2.8 Additional exercise
As a final task of our exercise we will add some interactivity
into our HTML documents by linking each Watch’s page to the spec sheets of
previous and next Watches. There is again a little catch here: the first page
only has a link to the previous page and the last page only has a link to the
previous page, while intermediate pages can link both ways. This means that
there is going to be some variation when outputting the HTML for individual
Watch pages.
There is of course more than one way to implement this
requirement. We will explore two different ways to do this, starting with the
one that is probably the most obvious to most people with any previous
programming experience. In this solution we create an index for each Watch model
first and then use this index when we need to fetch the information about the
previous and next models while creating the spec sheet for a Watch model. The
relevant parts of this solution look like this:
@ix='0'
foreach >Watch; orderby y num
{ local 'ix_' @++ix write id close }
@ix='0'
foreach >Watch; orderby y num
{ .
.
'<p class="navigation">'
@prev = __(local 'ix_' @ix++ read)
@next = __(local 'ix_' @++ix-- read)
if @prev then
'Previous model: ' _linkHTML(@prev)
endif
if @prev AND @next then ' | ' endif
if @next then
'Next model: ' _linkHTML(@next)
endif
'</p>' newline
.
.
}
After executing the first three lines we will have the
MERL equivalent of an array, a set of local variables that store the name for
the Watch model at the respective index, i.e.:
@ix_1 = Ace
@ix_2 = Delicia
@ix_3 = Sporty
@ix_4 = Celestron
@ix_5 = Celestra
After this initialization, we reset @ix
and enter the main foreach loop where
we assign the @prev and @next local variables with the names of the Watch models
preceding and following the one currently at hand. As the syntax here may
initially appear a bit cryptic, we will walk through it step by step. Consider
the first assignment:
@prev = __(local 'ix_' @ix++ read)
Starting from
within the local...read command in the
parentheses, we first construct the name of the index variable pointing to the
previous Watch model:
'ix_' @ix++
The name of the variable starts always
with prefix ‘ix_’ that is followed by the index number. The counter
arithmetic in this case reads as: output the current value of
@ix first, and then increment @ix by 1.
Hence, we will get the name for the index variable (i.e. ‘ix_0’,
‘ix_1’ etc.) and have our counter updated. After this step, the
assignment line is thus effectively:
@prev = __(local 'ix_0' read)
The
local...read command will fetch the
value for the named local variable. At this point it is worth pointing out that
there is no ix_0 among our existing index variables. Reading the value of an
as-yet-undefined variable just returns an empty string, which is what we want
here as there is no predecessor for our first Watch model. The surrounding
__(...) is just used so that we can
have multiple or non-trivial commands on the right-hand side of the assignment:
it’s actually a call to a subgenerator called ‘__’, which
simply returns the value of the argument built up between the parentheses. The
subsequent iterations will result in the values of variables @ix_1, @ix_2 etc.,
which will give us the name of the preceding Watch to store in @prev. We can
then use that later to output the appropriate HTML for a link to the file of the
same name, in a subgenerator _linkHTML(@prev) whose definition is left to the
reader.
The assignment of the @next variable is otherwise similar
to above, but the name for the index variable is constructed
differently:
'ix_' @++ix--
The name starts with the
‘ix_’ prefix again but is followed now by the index number of the
next Watch model. The counter arithmetic thus now reads: increment counter by 1
(to get the next index), output its value, and then decrease it by one (to get
back to current index as we need it during the next step of iteration).
Otherwise the assignment executes as with @prev. On the final Watch model we
will encounter the variable name ‘ix_6’ that is not pointing
anywhere and will end up having @next as blank – which is again what we
want as there is no Watch model following the last one.
The solution above uses MERL’s associative variables
like an Array, i.e. it forms key and value pairs where the key is a numerical
index and the value is a Watch model name. However, this requires us to keep
track of index counters during the creation and use of the array. It is also
possible to use associative variables in a more organic way, more like a
Dictionary or HashMap than an Array, by using Watch names as keys instead of
building numerical indices to use as a key. This way we no longer need to
maintain the information about indices. The new variable names also make the
code more readable and understandable. Rather than trying to write “C in
MERL”, we are using MERL in a more natural way. Using this approach, the
complete SpecSheet() generator will now look like this:
SpecSheet()
_translators()
@defaultDir = __(subreport '_default directory' run)
@cssFilename = _myFilename('css')
filename @defaultDir @cssFilename write _css() close
@prev = ''
foreach >Watch; orderby y num
{ local 'prev_' id write @prev close
local 'next_' @prev write id close
@prev = id
}
foreach >Watch; orderby y num
{ filename @defaultDir _myFilename('html') write
'<html>' newline
'<head><link rel="stylesheet" type="text/css" '
'href="' @cssFilename '"/></head>' newline
'<body>' newline
'<h1>' id '</h1>' newline
'<p class="doc">' :Documentation '</p>' newline
do .Display
{ @count = '0'
do :UnitZones { @count++%null }
'<p class="info">Number of time units shown: '
@count '</p>' newline
'<p class="info">Buttons: '
dowhile :Buttons { id ' ' }
'</p>' newline
}
do .LogicalWatch
{ '<p class="info">Apps: '
do decompositions
{ foreach .Start [Watch]
{ $visited = ' '
do ~From~To.()
{ _handleStateAndRecurse() }
}
}
'</p>' newline
}
'<p class="navigation">'
@prev = __(local 'prev_' id read)
@next = __(local 'next_' id read)
if @prev then
'Previous model: ' _linkHTML(@prev)
endif
if @prev AND @next then ' | ' endif
if @next then
'Next model: ' _linkHTML(@next)
endif
'</p>' newline
'</body>' newline
'</html>' newline
close
}
The execution of the first
foreach loop will create the following
variables:
@prev_Ace =
@next_Ace = Delicia
@prev_Delicia = Ace
@next_Delicia = Sporty
@prev_Sporty = Delicia
@next_Sporty = Celestron
@prev_Celestron = Sporty
@next_Celestron = Celestra
@prev_Celestra = Celestron
@next_Celestra =
Now, by knowing the id (i.e. the name)
of the current Watch model we can construct the names for variables that point
to its processor and successor. Within the second loop we will now use this
trick to create the @prev and @next local variables as we did earlier. Otherwise
the generator remains the same as before.
The final output of this generator will now result in a
web page like this:
Figure 5–9. Interactive web page generated from the Watch Example.
The
generator could still be improved, e.g. to make sure that any characters illegal
as literals in HTML, like & and <, are escaped when found in the model.
This can be handled simply by appending %xml to the relevant output commands,
e.g. id%xml and :Documentation%xml.
Adding these final touches to our exercise completes this
primer for developing generators with MERL in MetaEdit+. After this general
overview of generator development practices, the following chapters will provide
more comprehensive descriptions of the MERL development tools like the Generator
Editor and Debugger, and of the individual MERL
commands.