WBuilder

What is it?

I don't know about you, but I find that the most tedious and error-prone part of building a web site of any size is keeping all the links pointing the right way. Thus, I wrote WBuilder to help partially automate the process.


What does it do?

The idea is that WBuilder makes it trivial to build a web site with a simple hierachical structure. Just create whatever folders you want, and populate them with *.wb files. Each such file contains some commands for WBuilder itself, and then an incomplete HTML page. WBuilder turns each *.wb file into a real HTML web page, and crosslinks them all automatically. And that is all.


Detailed Operation

Each *.wb file becomes a normal HTML file (*.html). WBuilder does this by gluing some (hard-coded) stuff onto the top and bottom of the file. (If nothing else, this means you don't have to type in the 5-mile-long DOCTYPE declaration!) As coded, the headers added include an XHTML-strict DOCTYPE, and a reference to a stylesheet Root.css in the current folder. (But you could change that if you wanted.) Also puts the time of processing, the version of WBuilder, and some links to the W3C validator(s) on the bottom of the page. (Again, you could change if it you wanted.)

In addition to this, every single page is given "breadcrumbs" - links that point to every page visited to get to this point. (Due to the strictly hierachical structure, there is only 1 possible path to each page.)

On top of that, in every folder a file called Root.html is generated, which links to every file in the current folder, and the Root.html file in every subfolder (if any).

(Last but not least, I have just finished a total rewrite of WBuilder to make it incremental. That is, now it only generates altered pages, in a similar way to the "make" program recompiling C files...)


Source Code

The source code consists of 6 separate files:

  • libWrite.tcl (Contains the hard-coded HTML stuff.)
  • libFile.tcl (Contains a few trivial file utilities.)
  • libProcess.tcl (Slurps up the *.wb files and generates multiple *.tmp files from their contents.)
  • libBuild.tcl (Reads in *.tmp files and builds *.html files from them.)
  • libCompile.tcl (Does all the complex dependency chasing to rebuild only changed files!)
  • WBuilder200.tcl (WBuilder v2.00 main script. source's the other files and presents a nice clickable GUI.)

The one you'll probably want to edit is libWrite.tcl. Note that the code contains a slight amount of weirdness because it's designed to be run by FreeWrap! And very few comments. And probably lots of stuff that could be done better.

Top of File: libWrite.tcl

 proc WriteHead {Handle Title Scripts} \
 {
   puts $Handle {<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">}
   puts $Handle {<html>}
   puts $Handle {<head>}
   puts $Handle "  <title>Orphi.net: $Title</title>"
   puts $Handle {  <link rel="stylesheet" type="text/css" href="Root.css"/>}

   foreach Script $Scripts {puts $Handle "  <script language=\"JavaScript\" href=\"$Script\"></script>"}

   puts $Handle {</head>}
   puts $Handle {<body>}
   puts $Handle {}
   puts $Handle "<h1>$Title</h1>"
   puts $Handle {}
 }

 proc WriteBreadcrumbs {Handle Title Crumbs} \
 {
   puts $Handle {<p class="breadcrumbs">}
   puts $Handle {You are here:}

   foreach Crumb $Crumbs {puts $Handle "<a href=\"[lindex $Crumb 0]\">[lindex $Crumb 1]</a> &rarr;"}

   puts $Handle "<strong>$Title</strong>"
   puts $Handle {</p>}
   puts $Handle {}
 }

 proc WriteLinks {Handle Links} \
 {
   puts $Handle {<p class="links">}

   foreach Link $Links {puts $Handle "&lang;<a href=\"[lindex $Link 0]\">[lindex $Link 1]</a>&rang;"}

   puts $Handle {</p>}
   puts $Handle {}
 }

 proc WriteTail {Handle} \
 {
   global Version

   set DateTime [clock format [clock seconds] -format "%I:%M %p (%Z) on %a %d-%b-%Y"]

   puts $Handle {<hr/>}
   puts $Handle {}
   puts $Handle {<p class="build">}
   puts $Handle "Built by WBuilder $Version at $DateTime."
   puts $Handle {</p>}
   puts $Handle {}
   puts $Handle {<p>}
   puts $Handle {<a href="http://validator.w3.org/check?uri=referer">}
   puts $Handle {<img src="http://www.w3.org/Icons/valid-xhtml10" width="88" height="31" alt="Valid XHTML 1.0 Strict"/>}
   puts $Handle {</a>}
   puts $Handle {<a href="http://jigsaw.w3.org/css-validator/check/referer">}
   puts $Handle {<img src="http://jigsaw.w3.org/css-validator/images/vcss" width="88" height="31" alt="Valid CSS"/>}
   puts $Handle {</a>}
   puts $Handle {</p>}
   puts $Handle {}
   puts $Handle {</body>}
   puts $Handle {</html>}
 }

Bottom of File: libWrite.tcl

Top of File: libFile.tcl

 proc FContents {filename} \
 {
   set IN [open $filename r]
   set Out [read -nonewline $IN]
   close $IN

   return $Out
 }

 proc FEmpty {filename} \
 {
   set OUT [open $filename w]
   close $OUT
 }

 proc FWrite {filename data} \
 {
   set OUT [open $filename w]
   puts $OUT $data
   close $OUT
 }

Bottom of File: libFile.tcl

Top of File: libProcess.tcl

 proc Process {filename} \
 {
   puts "->Process: $filename"

   set IN [open "$filename.wb" r]

   set Title "(No Title)"
   set Scripts [list]

   while {![eof $IN]} \
   {
     set Line [gets $IN]

     if {$Line=="#"} {break}

     set Line [split $Line ":"]
     set Key [lindex $Line 0]
     set Val [lindex $Line 1]

     if {$Key=="Title"}  {set Title $Val}
     if {$Key=="Script"} {lappend Scripts $Val}
   }

   FWrite "$filename.title.tmp"   $Title
   FWrite "$filename.scripts.tmp" $Scripts

   FWrite "$filename.data.tmp" [read $IN]

   close $IN
 }

Bottom of File: libProcess.tcl

Top of File: libBuild.tcl

 proc Build {filename} \
 {
   set RootMode "no"
   if {[file tail $filename] == "Root"} {set RootMode "yes"}
   puts "->Build: $filename (Root Mode = $RootMode)"

   set Dir [file dirname $filename]
   set Title   [FContents "$filename.title.tmp"]
   set Scripts [FContents "$filename.scripts.tmp"]
   set Crumbs  [FContents [file join $Dir "Crumbs.tmp"]]
   if {!$RootMode} {lappend Crumbs [list "Root.html" [FContents [file join $Dir "Root.title.tmp"]]]}

   set OUT [open $filename.html w]
   WriteHead $OUT $Title $Scripts
   WriteBreadcrumbs $OUT $Title $Crumbs
   if {$RootMode} {WriteLinks $OUT [CollectLinks $Dir]}
   puts $OUT [FContents "$filename.data.tmp"]
   WriteTail $OUT
   close $OUT
 }

 proc CollectLinks {dirname} \
 {
   puts "--->CollectLinks: $dirname"

   set Out [list]

   set Subs [glob -nocomplain -types d -directory $dirname -tails *]
   set Subs [lsort -ascii $Subs]

   foreach Sub $Subs \
   {
     set File [file join $Sub "Root.html"]
     set Title [FContents [file join $dirname $Sub "Root.title.tmp"]]

     lappend Out [list $File $Title]
   }

   set Files [glob -nocomplain -types f -directory $dirname -tails *.wb]
   set Files [lsort -ascii $Files]

   foreach File $Files \
   {
     if {$File=="Root.wb"} {continue}

     set Root [file rootname $File]
     set F "$Root.html"
     set T [FContents [file join $dirname "$Root.title.tmp"]]

     lappend Out [list $F $T]
   }

   return $Out
 }

 proc MakeCrumbs {dirname} \
 {
   puts "->MakeCrumbs: $dirname"

   set Parent [file dirname $dirname]

   set Crumbs [FContents [file join $Parent "Crumbs.tmp"]]
   lappend Crumbs [list "Root.html" [FContents [file join $Parent "Root.title.tmp"]]]

   set Out [list]

   foreach Crumb $Crumbs \
   {
     set File [lindex $Crumb 0]
     set Title [lindex $Crumb 1]

     set File [file join ".." $File]

     lappend Out [list $File $Title]
   }

   FWrite [file join $dirname "Crumbs.tmp"] $Out
 }

Bottom of File: libBuild.tcl

Top of File: libCompile.tcl

 proc Compile {dirname top} \
 {
   puts "Compile: $dirname (Top = $top)"

   # Generate Crumbs.tmp
   set Crumbs [file join $dirname "Crumbs.tmp"]
   if {$top=="yes"} \
   {
     UpdateIf [list] $Crumbs "FEmpty $Crumbs"
   } \
   {
     set Parent [file dirname $dirname]
     set P1 [file join $Parent "Crumbs.tmp"]
     set P2 [file join $Parent "Root.title.tmp"]
     UpdateIf [list $P1 $P2] $Crumbs "MakeCrumbs $dirname"
   }

   # Check Root.wb exists
   set Root [file join $dirname "Root"]
   UpdateIf [list] "$Root.wb" "FEmpty \"$Root.wb\""

   # Generate *.tmp
   set Files [glob -nocomplain -types f -directory $dirname *.wb]
   foreach File $Files \
   {
     set F [file rootname $File]
     UpdateIf [list "$F.wb"] "$F.title.tmp" "Process $F"
   }

   # Generate *.html (except Root.html)
   foreach File $Files \
   {
     set F [file rootname $File]
     if {[file tail $F]=="Root"} {continue}
     UpdateIf [list "$F.title.tmp"] "$F.html" "Build $F"
   }

   # Compile all subfolders
   set Subs [glob -nocomplain -types d -directory $dirname *]
   foreach Sub $Subs {Compile $Sub no ; lappend Files [file join $Sub "Root.wb"]}

   # Generate Root.html
   UpdateIf [concat $Files [list $Crumbs]] "$Root.html" "Build $Root"
 }

 proc UpdateIf {sources target command} \
 {
   if {![file exists $target]} {eval $command ; return}

   set Time1 [file mtime $target]

   foreach Source $sources \
   {
     if {$Time1 < [file mtime $Source]} {eval $command ; return}
   }
 }

 proc Clean {root_dir} \
 {
   puts "Clean: $root_dir"

   set Subs [glob -nocomplain -types d -directory $root_dir *]

   foreach Sub $Subs {Clean $Sub}

   set Files [glob -nocomplain -types f -directory $root_dir *.tmp]
   foreach File $Files {file delete $File}

   set Files [glob -nocomplain -types f -directory $root_dir *.html]
   foreach File $Files {file delete $File}
 }

Bottom of File: libCompile.tcl

Top of File: WBuilder200.tcl

 set Version "2.00"
 set Date "03-Sep2-2006"

 set PATH "/Orphi/Tcl-Tk/WBuilder/v2"
 source "$PATH/libWrite.tcl"
 source "$PATH/libFile.tcl"
 source "$PATH/libProcess.tcl"
 source "$PATH/libBuild.tcl"
 source "$PATH/libCompile.tcl"

 proc Export {Src Dst} \
 {
   puts "Export: $Src -> $Dst"

   if {![file exists $Dst]} {file mkdir $Dst}

   set Files [glob -nocomplain -types f -directory $Src *]

   foreach File $Files \
   {
     if {[file extension $File]==".tmp"} {continue}
     if {[file extension $File]==".wb"}  {continue}

     set FF [file tail $File]
     set DF [file join $Dst $FF]

     set Time1 [file mtime $File]
     set Time2 0

     if {[file exists $DF]} {set Time2 [file mtime $DF]}

     if {$Time1 > $Time2} {puts "-->Copy $File" ; file copy -force $File $Dst}
   }

   set Subs [glob -nocomplain -types d -directory $Src *]

   foreach Sub $Subs \
   {
     Export $Sub [file join $Dst [file tail $Sub]]
   }
 }

 proc DoIt {} \
 {
   Compile "Src" "yes"
   puts "\n"
   Export "Src" "Dst"
   puts "\n\n"
 }

 console show
 console title "WBuilder $Version ($Date)"

 wm title . "WBuilder $Version"
 label .date -text "Written $Date" -font "Times 8 roman"
 button .go    -text "Compile" -font "Times 12 roman" -bg #00FF00 -command {DoIt}
 button .clean -text "Clean"   -font "Times 12 roman" -bg #FF0000 -command {Clean Src ; puts "\n"}
 button .quit  -text "Quit"    -font "Times 12 roman" -bg #0000FF -command {exit}
 pack .date  -side top
 pack .go    -side left
 pack .clean -side left
 pack .quit  -side left

Bottom of File: WBuilder200.tcl


Usage

Put all 6 *.tcl files in the same place. Create a subfolder there called "Src", and put some *.wb files in there. Here is an example file:

 Title:This is a test
 #
 <p>Just checking to make sure this works!</p>

Now start up a Tcl/Tk interpreter and source the file WBuilder200.tcl. (Should source the other files; you'll likely have to play with the silly hard-coded pathnames neccessary to make it work with FreeWrap.) If it's working, you should get 3 buttons: Compile, Clean, Quit. Presumably the last button is self-explanatory. (?) The Compile button will scan the Src folder for *.wb files. For each such file, a matching *.html file will be created.

If it doesn't already exist, an empty file Root.wb is created. This is the "index" page, if you like. The corresponding HTML page will link to every file in the whole folder.

Next a folder called "Dst" is created, and every file in Src that is not a *.wb or *.tmp file is copied there. Upload this to your web server and go!

You can also create subfolders and put *.wb files in there too. Note that all "root" pages list subfolders in ASCII order, then files in the same folder in ASCII order. (ASCII order of file names, not page titles.)

If you edit any *.wb files and hit Compile again, it should recompile just that file (and also any "root" pages that mention it).

BTW, the Clean button just deletes all the *.tmp files and the generated *.html files. If you do this, everything gets regenerated next time you compile...


Credits:

Created by The Mathematical Orchid.


Beware - It seems the Wiki has somehow chewed up libFile.tcl; specifically anything that looks like a hyperlink. Anyone know how to fix? (The actual Wiki source is fine; it's just what's on the screen that's wrong.)