2011-02-19 19:19:39 +01:00
Website Navigation Generator
============================
2011-02-27 21:42:12 +01:00
:Author: Hermann Voßeler
:Date: 2/2011
2011-02-21 08:24:58 +01:00
This page contains documentation and notes regarding the +menugen.py+ --
written 2/2011 during our attempt to get the new Lumiera website online finally.
The link::http://git.lumiera.org/gitweb?p=website-staging;a=blob;f=menugen.lua;h=aad2129d7f4ed3f3b35b2fc3ac2a63a9f1bfb62d;hb=menugen[initial draft version] was written by _cehteh_ in Lua
**************************************************************************
The purpose of the +*menugen*+ script is to maintain the navigation menu
on the Lumiera website semi-automatically. In the usual setup, this script
is triggered from a _Git push_ -- it walks the web subdirectories and
discovers menu entries. The generated HTML page contains both visible
elements and JavaScript snippets to display and highlight the menu
on the client side appropriately
**************************************************************************
Overview: how it works
----------------------
The menu generation and display is comprised of several parts working together
. the +build_website.sh+ is triggered as a Git post-receive hook, whenever new
commits are transfered to the website Git repository. After discovering new
Asciidoc source files and generating the corresponding HTML files, the
menu generator script is invoked
. the +menugen+ python script walks the subdirectories to discover possible
menu contents. It visits Asciidoc source files (`*.txt`) and picks up
- the location / URL
- the title
- special `//MENU:` directives embedded in Asciidoc comments
. after building a complete menu tree (actually a DAG), this data structure
is walked to generate output HTML into a `menu.html` file in website root.
. the page template (`page.conf`) for generated Asciidoc pages contains an
+<IFrame>+ to display this `menu.html`
. when loading `menu.html`, some JavaScript elements generated into the body
alongside with the visible content will execute, causing a lookup table
in the client side memory being populated with the menu entries and parent
dependencies. Each individual menu entry has an attached unique ID, originally
generated by the server side +menugen+ script. The clientside JavaScript always
addresses elements directly through these IDs, mostly ignoring the actual DOM
structure
. whenever a new webpage is loaded, the `onload` handler on the +<IFrame>+ (or
a similar mechanism) invokes the +markPageInMenu()+ JavaScript function, which
addresses the IFrame by its ID `inavi`, and calls into the JavaScript located
there. This script in turn finds the menu entry corresponding to the current
page with the help of the lookup table mentioned above; this allows to highlight
the current page and fold any other branches of the menu to keep the visible
part reasonably small to fit on a single page
. folding and highlighting changes are done by manipulating the style of these
elements; the actual presentation is mostly controlled by a `menu.css`
. any further JavaScript functions used to operate the menu are located in
the statically served `menu.js` -- the generated menu contains only the
``moving parts''
Configuring menu generation
---------------------------
While, generally speaking, the script was written to remove the need to care
for the menu most of the time, there are numerous extension points and configuration
options to deal with special cases. Adjustments can be done on several levels:
* the +menugen+ python script contains in embedded set of _predefined menu entries,_
forming the backbone of the generated menu. The use of this feature is optional
and can be enabled with the `-p` or `--predefined` switch. These predefined
configuration steps are done in a function +addPredefined()+ right at the top;
the configuration is written in the style of an _internal DSL_ and should be
fairly self explanatory.
* when discovering Asciidoc page sources, special `//MENU:` directives are
processed (`//` marks an Asciidoc comment). The remainder of such a line
is always parsed as a single directive; in case of a parsing error a warning
is printed and the line will be ignored. The individual directives mostly
correspond to similar functions usable in the aforementioned internal DSL;
actually both kinds of configuration have the same effect: they attach
some modification command to the menu element in question. Note especially
that such directives can modify the discovery of further pages -- pages
can be attached, excluded, ordered; and the discovery can be redirected
to another subdirectory.
* the actual code generation is mostly based on python template code contained
in a separate script +menuformat.py+ -- located alongside the main menu generator
script. This code generation is driven by a classical recursive tree visitation
over the menu data structure built up thus far; code generation hooks are called
on each tree leaf and when entering and leaving inner nodes (submenu nodes).
* the highlighting is done by the client side JavaScript in +js/menu.js+ --
mostly just by _adding or removing CSS classes_ dynamically. The actual styling
of the menu entries is thus largely independent of the menu generation (but of
course the CSS selectors must line up with the basic structure of the generated
code). The current version of this CSS stylesheet makes heavy use of _contextual
selectors_ and the general cascading mechanism to build up the final style; e.g.
the indentation according to the menu level is done by attaching a style based
on the number of nested HTML elements.
Summary of menu placement directives
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With the term _placement directives_ we denote all the adjustments and configuration
possible either through the internal DSL for the predefined menu structure, or through
the `//Menu:` lines in the individual pages.
addressing menu nodes
^^^^^^^^^^^^^^^^^^^^^
Each menu entry corresponds to a menu node in the internal data structure. In the most
general case, this structure is a _Directed Acyclic Graph_, because a node might be
hooked up below several different parent nodes. In this case, such a node will also
be visited multiple times for code generation -- one time for each parent it is
attached below. Amongst these parent nodes, the first parent node attached is called
the _primary parent_, because this first attachment of a node defines the _logical
path_ uniquely describing this node. Note, this logical path can be different to
the actual web paths / URLs generated, and also be different to the file system
path where the source file resides. It is just defined by the chain of parent
nodes leading to the root of the menu data structure.
The leaf element of this logical menu path is called the _ID_ of the node. Typically
this ID corresponds to the filename without the extension. But for the code generation
and the client sides JavaScripts, the full menu path is used as an HTML id element,
because -- generally speaking -- only the full menu path denotes an element unambiguously.
When working with nodes, and especially when writing placement directives in the individual
source files, in most cases it is not necessary to specify the full menu path of a node.
Actually, nodes can be addressed by any path suffix, and even just by the bare node ID.
But when there is an ambiguity, just the first node found is picked. Because nodes have
an unique identity, this can sometimes yield rather wired results. To minimise the
danger of ambiguities, the _discovery_ of source pages always addresses the menu node
to be populated with the full menu path.
configuration example
^^^^^^^^^^^^^^^^^^^^^
[source,python]
--------------------------------------------------------------------------
def addPredefined():
2013-10-28 06:12:05 +01:00
root = Node(TREE_ROOT, label='Lumiera') # <1>
proj = root.linkChild('project') # <2>
2011-02-21 08:24:58 +01:00
proj.linkChild('faq')
2013-10-28 06:12:05 +01:00
proj.prependChild ('screenshots') # <3>
2011-02-21 08:24:58 +01:00
proj.putChildLast ('press')
2013-10-28 06:12:05 +01:00
proj.putChildAfter('faq', refPoint=Node('screenshots')) # <4>
2011-02-21 08:24:58 +01:00
2013-10-28 06:12:05 +01:00
proj.link('http://issues.lumiera.org/roadmap', label="Roadmap (Trac)") # <5>
2011-02-21 08:24:58 +01:00
Node('rfc').sortChildren()
--------------------------------------------------------------------------
<1> the _root node_ by convention uses a special ID token. Additional
fields of the node object can be given as named parameters. Here
we define the visual menu label to be ``Lumiera''
<2> a child node `root/project` is attached. Note: this node will
later be picked up, when the actual page discovery delves down
into the 'project' subdirectory and encounters a 'index.txt'
there. Index files are always searced _within_ the directory;
they may be called `index.txt` or use the same name as the
enclosing directory.
<3> this placement directive defines that a node `screenshots`
shall be prepended at the start of the list. Because such a node
doesn't yet exist, a new node `root/project/screenshots` is
created as a side-effect.
<4> this directive places an entry after another entry, which is
assumed to exist when this directive gets applied finally.
All placement directives get applied in order of definition,
just before the output for a given node is generated.
Note also the constructor syntax +Node(\'screenshots')+: here
the constructor just acts as a general factory function; either
it creates a new node, or it fetches an existing node with matching
node path from the internal +NodeIndex+
<5> here we create a submenu entry in the project menu, featuring
an external link. The ID of that menu node will be derived from
the name in the url (here `roadmap`) -- it can be defined explicitly
if necessary (+id=...+)
supported placement directives
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[options="header", width="70%",cols="^m,<m,s<",frame="topbot",grid="all"]
`------------------------`------------------------------------`---------------------------------------------------
internal DSL Asciidoc source
Node(<id>) -- discover `id.txt` -- create new node or retrieve existing node
linkChild(id) basic function for attaching child node
linkParent(id) basic function to attach below parent
putChildLast(id) [attach] child <id> move child to current end of list
appendChild(id) [append] child <id>
putChildFirst(id) move child to current list start
prependChild(id) prepend [child] <id>
putChildAfter(id,ref) [attach|put] child <id> after <ref> move child after the given ref entry
link(url[,id][,label]) [child <id>] link ::<url>[<label>] attach an entry, holding an external link
Node(<id>,label=<lbl>) label|title <lbl> define the visible text in the menu entry
sortChildren() sort [children] sort all children currently in list
enable(False) off|disable|deactivate make node passive; any children/parents added later are ignored
enable([True]) on|active|activate make node active again (this is the default)
detach() detach cut away any parents and children, disable the node
2011-02-21 09:09:55 +01:00
discover(srcdirs=...) include dir <token>[,<token>] instead of current dir, retrieve children from other dirs (relative)
discover(includes=...) include <token>[,<token>] explicitly use the listed elements as children
2011-02-21 08:24:58 +01:00
discover(excludes=...) exclude <token>[,<token>] after discovering, filter names matching the <token> (without extension)
--------------------------------------------------------------------------------------------------------------------
commandline options
^^^^^^^^^^^^^^^^^^^
The behaviour of the +menugen+ script can be influenced by some options:
2011-03-28 07:01:12 +02:00
2011-02-21 08:24:58 +01:00
predefined:: using the built-in predefined nodes
scan:: discover nodes
debug:: dump data structure after discovery
text:: generate plaintext version of the menu
webpage:: actually generate HTML / JavaScript
a positional parameter denotes the start directory for discovery (default is current).
This directory is assumed also to be the web root; any URLs are generated relative
Design and Implementation notes
-------------------------------
The initial observation was that actually we're parsing and processing some kind of
_Domain Specific Language_ here. Thus the general advice for such undertakings does
apply: we should try to handle the actual language just as a thin layer on top of
some kind of _semantic model_. In our case, this model is the menu tree to be generated,
while the actual ``syntax tree'' is the real filesytem, holding Asciidoc files with
embedded comments. Thus, the semantic model was developed first, and separate of the
syntax of the specifications; it was tested to generate suitable HTML and CSS.
The syntactic elements where then added as a collection of parser or matcher objects,
each with the ability to recognise and implement one kind of placement specification.
Each such +Placement+ subclass exposes an +acceptVerb()+ function for handling invocations
of the internal DSL functions, and an +acceptDSL()+ function to parse and accept a
`//Menu:` line from some Asciidoc source file. This approach makes adding further
configuration options simple.
Another interesting question is to what extent the actual path handling and file discovery
logic should be configurable. My reasoning is, that any attempts towards larger flexibility
are mostly moot, because we can't overcome the fact that this *is* logic to be cast into
program code. Extension points or strategy objects will just have the effect to tear apart
the actual code thus will make the code harder to read. Thus I confined myself just to
configure the index file name and file extensions.
2011-02-19 19:19:39 +01:00
Known issues
2011-02-21 08:24:58 +01:00
~~~~~~~~~~~~
2011-02-19 19:19:39 +01:00
* for sake of simplicity, there is _one_ generated container HTML element
2011-02-21 08:24:58 +01:00
per menu entry. In case this entry is a submenu, the `<ul>`-element is
used, _not_ the preceding headline `<li>` -- this is due to the fact
2011-02-19 19:19:39 +01:00
that this submenu entry is going to be collapsed eventually, but has
the side-effect of highlighting _only_ that submenu block, _not_ the
2011-02-21 08:24:58 +01:00
preceding headline.
2011-02-19 19:19:39 +01:00
* the acceptable DSL syntax needs to be documented manually; there is
no way to generate this information. Doing so would require to add
specific information methods into Placement subclasses, and it would
result in duplicated information between the regular expressions
and the informations returned by such information methods.
This was deemed dangerous.
2011-02-21 08:24:58 +01:00
* the +\_\_repr\_\_+ of the Placement subclasses is not an _representation_
but rather a +\_\_str\_\_+ -- but unfortunately the debugger in PyDev
invokes +\_\_repr\_\_+
* the startdir for automatic discovery is an global variable
2011-02-19 19:19:39 +01:00
* when through the use of redirection, the same file is encountered
multiple times during discovery, it is treated repeatedly, each times
associated with another node, because, on discovery, the node-ID is
2011-02-21 08:24:58 +01:00
generated as +parentPath/fileID+, to avoid mixing up similarly named
2011-02-19 19:19:39 +01:00
files in different directories. (The NodeIndex allows to retrieve
a node just by its bare ID, without path anyway)
2011-02-21 01:44:32 +01:00
* no escaping: currently any variable text is written to the generated
2011-02-21 08:24:58 +01:00
HTML without any sanitising or escaping. This might be a security issue,
especially because Git pushes immediately trigger menu generation.
2011-02-21 01:44:32 +01:00
* the method Node.matches() is implemented sloppily: it uses just a mutual
postfix match, while actually it should line up full path components and
check equality on components, starting from the path end. This cheesy
implementation can yield surprising side-effects: e.g. an not-yet attached
2011-02-21 08:24:58 +01:00
node `\'end'` could match a new menu page `\'documentation/backend'`
2011-02-21 01:44:32 +01:00