Version 5 of EFX Content Management System

Updated 2005-11-09 17:51:42

EFX is a Content Management System (CMS) written in Tcl. I wrote it because I needed something to help my son's school maintain their events, news, newsletters pictures (a process in which they previously employed Frontpage).

I took a look at Bricolage, and while it is really cool (plus it doesn't require you to use any kind of app server or apache plugin -- it produces plain old HTML pages), I found its use of Mason cumbersome and I wasn't ready to brush up on my Perl. Plus, its a really big system that is difficult to install and master. (My target was a fairly moderate site of about 30-40 pages plus 50 or so pictures.)

I am still working on the school website (and EFX), but I found EFX already quite useful.

EFX (so far) consists of

  • EFX engine - A template engine based on ideas behind Zope Page Templates (which I had the pleasure of helping to define back in 2000) where markup in embedded in XML (XHTML) attributes.
  • Navmenu - A Navigational tool that helps you build hierarchical navigational menus and map out your site.
  • Story - A simple news content management tool that allows users to just put text files (with a title and date line) in a directory for inclusion into the website.
  • Calendar - An event management tool that presents a calendar UI interface to an events page.

The EFX engine is about 450 lines of Tcl that uses TclXml to parse and operate on XML documents to produce XHTML pages. It is the most interesting part of the system. Here are just a few of the commands it exposes inside of XML element attributes:

  • efx:eval="command" - Evaluates command. Eats element and children. No output.
  • efx:eval_r="command" - Evaluates command. Replaces element with result of the command.
  • efx:subst="" - Substitutes commands/variables in children. Outputs current node and children.
  • efx:onlyIf="expression" - Evaluates expression (with expr). If true, outputs element and children. If false, eat element and children.
  • efx:foreach="var|command" - Iterates var over result of command, processing children (which may contain additional efx commands) until var is {}.
  • efx:processFile="input_file args" - Process additional XML files.

Commands can be any Tcl expression. All commands and variables are evaluated within a common Tcl namespace.

Each XML file is treated as a closure. Variable bindings introduced in each page is in effect until that page is finished (then any old bindings are restored). You can use efx:processFile to nest closures.

Okay, enough of that, how about an example?

Here is the XML site generator for a simple blog that I made in about 20 minutes using the EFX toolkit: http://www.maplefish.com/todd/blog

I intend on improving the blog for doing RSS feeds and a few more features, but since it is a static site, no feedback/comment support is planned.

The source XML that generated all of the pages:

   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   <html xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
     <head>
       <div efx:eval="source blog_settings.tcl"/>
       <div efx:eval="set BASEURL ."/>
       <div efx:eval="package require story"/>
       <div efx:eval="package require navmenu"/>
       <div efx:eval="navmenu::parse_file navmenu.map"/>
       <div efx:eval="set LinkMenu [navmenu::get Links]"/>

       <div efx:markBlock="MetaStuff">
        <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
        <meta efx:subst="" name="keywords" content="$BLOG_KEYWORDS"/>
        <meta name="generator" content="EFX"/>
        <style efx:subst="" type="text/css">@import url("$BASEURL/css/$BLOG_STYLESHEET");</style>
       </div>
       <title efx:subst="">$BLOG_NAME</title>

       <div efx:eval="set StoryDir ./entries"/>
     </head>
   <body>
     <div id="Header">
       <div efx:markBlock="BlogHeader">
         <div efx:subst="">$BLOG_HEADER</div>
       </div>
     </div>

     <div id="Content">
       <div efx:foreach="s|story::stories $StoryDir -max 10">
         <h4 efx:eval_r="story::title_as_ref $s"/>
         <small efx:eval_r="clock format [story::date $s] -format {%A, %B %d }"/><br />
         <p efx:eval_r="story::body $s"/>
         <hr efx:onlyIf="[llength $s]"/>
       </div>
     </div>

    <div id="Menu">
      <h4>Recent Entries</h4>
      <ul efx:foreach="s|story::stories $StoryDir -max 10">
         <li efx:eval_r="story::title_as_local_link $s">Title</li>
      </ul>
      <hr />
      <h4>Archives</h4>
      <div efx:eval="set MONTH_MENU [navmenu::make]"/>
      <!-- We make two passes, the first builds the menu of months;
       the second renders the month (which will need the menu of months
       for linking) 
       -->
      <div efx:foreach="pass| list make_menu render_month">
        <div efx:eval="set lns [story::stories $StoryDir]"/>
        <div efx:eval="set months [story::months $lns]"/>
        <div efx:foreach="month| set months">
           <div efx:eval="setl {MONTH_STORIES lns} [story::next_by_month $lns]"/>
           <div efx:eval="set THIS_MONTH $month"/>
           <div efx:onlyIf="$pass == {make_menu}">
             <div efx:eval="set MONTH_MENU [navmenu::add $MONTH_MENU $THIS_MONTH $THIS_MONTH.html]"/>
           </div>
           <div efx:onlyIf="$pass == {render_month}">
             <div efx:processFile="./month.xmlt ./$THIS_MONTH.html"/>
           </div>
        </div>
     </div>
     <div efx:eval_r="navmenu::render $MONTH_MENU {}"/>
     <hr />
     <h4>Links</h4>
     <div efx:eval_r="navmenu::render $LinkMenu $HERE"/>
    </div>
   </body>
   </html>

The blog_settings.tcl file just contains a few set commands to introduce BLOG_XXX variables:

   set BLOG_NAME {EFX Development}
   set BLOG_KEYWORDS {Todd Coram, EFX, templates}
   set BLOG_STYLESHEET {simple.css}
   set BLOG_HEADER {<h1> Todd's EFX Development Log </h1>}

Navmenu.map looks like:

   navmenu::menu Links {
        {EFX Development Log} index.html {}
        BREAK - -
        {Maplefish Home} http://www.maplefish.com {}
        {Almost Free Text} http://www.maplefish.com/todd/aft.html {}
   }

I'm hoping to do a code release by next week. If you are interested in just looking at a raw code drop, check out http://www.maplefish.com/todd/efx/efx.tar.gz . The blog sources are at http://www.maplefish.com/todd/efx/blog.tar.gz

Comments are most welcome! -- Todd Coram