Qwicap's templating scheme is really a general-purpose XHTML (XML) slicer/dicer; it does not depend on, or use, any custom markup; there is no special language to describe a "template"; there is no meta information that must be provided in order to use an XHTML document as a template, and so on. It turned out to be the 800 lb. gorilla of the project, because the goal was a simple API, a nearly non-existent learning curve, efficient internals that favor caching, and a syntax for programmers that could, for example, allow a single line of code to locate a particular table in a document, find its first body row, make a copy of that, insert the copy into the table, and set the contents of all of its cells. Another line could mark every even-numbered row of the table to be one color, and every odd row another. (In practice, for the sake of clarity, one might want to do that in more than two lines, but then again, one might not.)
// Load a document and convert it to a modifiable state. MutableMarkup MyPage = Q.getDocument("mypage.xhtml").getMutable();
In order to use the templater to modify a portion of an XHTML document, the document must
first be loaded into memory and parsed. The Qwicap
class provides the
getDocument
method for this purpose.1 Given a
string specifying a path to a document that is not already present in the cache,
getDocument
reads the file from disk, parses it and caches it as an instance
of the ImmutableMarkup
class. Because nothing can modify its contents,
ImmutableMarkup
allows cached instances of documents to be shared safely
among multiple threads, etc.
A modifiable
instance of an immutable document is created by invoking its getMutable
method,
which returns a copy of the document as an instance of the MutableMarkup
class.
That "copy" initially shares 100% of the backing store, and markup objects, of the original
ImmutableMarkup
object, so the conversion costs very little in terms of memory
or time. Modifiable representations of the elements (tags) in MutableMarkup
are
created transparently as the document is modified, but strictly on an as-needed basis; the
vast majority of the tags in a typical document are unlikely to be modified at all, and
those tags continue to be references to the original, cached, immutable elements, and
therefore consume no additional memory.
// By way of example, locate the first row of the body of the table // marked with the "my-output-table" class. Results FirstRow = MyPage.get("table.my-output-table tbody tr:first-child");
Given a MutableMarkup
object, the first step in modifying a portion of the
document is to locate that portion. This is done by invoking the get
method
with an appropriate Cascading Style
Sheets level 2 (CSS2) Selector string. CSS2 selectors are, in effect, the query language of
the Qwicap templater.2 They were chosen because they were
designed for locating elements in XHTML, and because they add nothing to the web application
developer's learning curve. (Any web developer has to learn the basics of CSS selectors
sooner or later.)
The get
method returns a
Results
object which contains a list of all the elements (tags) in the markup that matched the
supplied CSS2 selector string.
All the methods that modify documents are provided by the Results
class,
and almost all such methods return a new instance of Results
which lists
all of the elements that were acted upon. When no elements are selected or acted-upon,
an empty Results
object is returned. These behaviors combine to make
method invocation chaining possible, safe, and highly convenient. They also help to
isolate code from the whims of web page designers/editors; if some element expected by
your code is removed from the document, your code doesn't break or throw exceptions, it
just does nothing. (This also allows a developer to safely "offer" a document more
information than it might want. If the web page designer has a use for a piece of
information, they can include markup that matches the particular CSS selector pattern
documented by the developer. If the information is unwanted, the relevant markup
can be omitted/deleted without fear of breaking the application.)
// Duplicate the first row of the table, inserting the // duplicate prior to the original first row. Results NewFirstRow = FirstRow.duplicateBefore(); // Set the contents of the new first row. NewFirstRow.setContents("td.first-name", "Alexander"); NewFirstRow.setContents("td.middle-initial", "T"); NewFirstRow.setContents("td.last-name", "Great");
The methods of the Results
class that modify documents are divided into
three groups: those that operate on tags as a whole, those that operate on tag attributes,
and those that operate on the contents of tags (the material between a "start tag"
and an "end tag", for example between "<p>
" and "</p>
"
tags). Also, because the "class" attributes of tags are more likely to be modified than any
other, a set of convenience methods is provided specifically for operating on them.
In general, there are three basic modifications that can be performed: "set", "add" and
"delete". "Set" and "add" operations both insert new material, but "set" replaces any
existing material in doing so, while "add" appends to any existing material. "Delete"
removes the specified material. A group of higher-level methods including
duplicate
, extract
, replace
and
setContents
are provided in order to combine and/or extend the basic
functionality.
// Get a list of all rows in the table body. Results Rows = MyPage.get("table.my-output-table tbody tr"); // Add class designations that will permit our style sheet to color // the even- and odd-rows of the table body independently, hopefully // increasing the readability of large tables. Note that we begin // by removing any such pre-existing designations. Rows.deleteClass("even-row"); Rows.deleteClass("odd-row"); Rows.even().addClass("even-row"); Rows.odd().addClass("odd-row");
Note that "set" and "delete" operations are idempotent, as is the addClass
function (unlike the other "add" operations), so code employing the Qwicap templater
generally doesn't have to make itself aware of the state of a body of markup before
manipulating it. This is an aid to both coding convenience and robustness.
Results
objects also include methods for filtering the matches they
contain in ways that can't be replicated using CSS2 selectors. Examples include:
first
which returns only the first match, even
which returns
only the even numbered matches, nth
which returns the match at the
specified index (the "Nth" match), and distinct
which returns a
Results
object guaranteed to be free of duplicate matches (helpful
when get
is used to conduct more than one search at a time).
// Now, do all of the above in two lines of code, without temporary variables, // just to demonstrate that it can be done. MyPage.get("table.my-output-table tbody tr:first-child").duplicateBefore(). setContents("td.first-name", "Alexander", "td.middle-initial", "T", "td.last-name", "Great"); MyPage.get("table.my-output-table tbody tr").deleteClass("even-row odd-row"). pop().even().addClass("even-row").pop(2).odd().addClass("odd-row");