MG Aug 25th 2011 - My main Tcl project Potato MUSH (MUD) Client is hosted on Google Code these days. It has a fairly decent wiki, and I wanted to put the helpfiles up there. Maintaining two copies quickly became a pain, so I wrote this little parser for the Google Code Wiki syntax, with a widget for displaying the files.
MG Well, since Google Code is no more, Potato has moved to Github , and the code has been updated to use a (very restricted) subset of Markdown instead, and has a few bug fixes here and there, so check out that version instead of the code below.
It's not full-featured - I only coded in the features/syntax I needed - but it handles headers, nested lists, bold/italic, literal code blocks, links (wiki and web), and will also display images that are stored in the wiki directory in svn with the .wiki files. (Only .gif's, because I don't have Img in my app, but it would be simple to expand that.)
It doesn't do any of the other markup, and it doesn't parse HTML tags. The way it displays the Table of Contents (the #sidebar) is also a little limited - it will only show nested links and lists - as it uses a different parsing routine to show the links in a Treeview widget.
This is a lightly modified version, to remove/change a few things which are application-specific. To use:
Think that's about it. Adding more markup support shouldn't be too difficult - alas, I am too busy actually writing the helpfiles at the moment to spend any more time than necessary on the code...
(Oh, the wiki pages are assumed to be utf-8 encoded, as that's what Google expects, but you can change if need be.)
package require Tk namespace eval ::wikihelp { variable info; # General help info variable path; # Widget paths variable index; # File and topic names set info(defaultTopic) "Index" set info(TOC) "ContentsPage" set info(win) .wikihelp set info(indexed) 0 set info(disppath) "GoogleCode WikiHelp" set info(sb) $info(disppath) set info(filepath) {E:\PotatoSVN\potato.vfs\lib\help} } namespace eval ::wikihelp::images { variable wikiImages; variable wikiImagesLen; # Path that Wiki images are stored in set wikiImages "http://potatomushclient.googlecode.com/svn/wiki/" set wikiImagesLen [string length $wikiImages] } # Current, known limitations: # * Does not parse markup in link names # * Contents Page in Tree displays nothing but ul-list elements # * Contents Page in Tree does nothing but link and nested ul-list parsing (no bold, etc) # * Very little HTML support. Parsing (and ignoring) tags would be good. #: proc ::wikihelp::T #: arg formatstr a format string to translate #: arg args arguments for the format string #: return translated string proc ::wikihelp::T {formatstr args} { return [::msgcat::mc $formatstr {*}$args]; } #: proc ::wikihelp::help #: arg topic Topic to show. Defaults to "". #: desc Show the Help window, and display the given topic. If $topic is "", show the default topic. #: return nothing proc ::wikihelp::help {{topic ""}} { variable info; variable path; if { [winfo exists $info(win)] } { # Window already exists if { $topic ne "" } { showTopic $topic } return; } if { $topic eq "" } { set topic $info(defaultTopic) } if { !$info(indexed) } { index } toplevel $info(win) wm title $info(win) [T "%s - Help" $info(disppath)] pack [set frame [ttk::frame $info(win).frame]] -expand 1 -fill both pack [set path(pane) [::ttk::panedwindow $frame.pane -orient horizontal]] -side top -expand 1 -fill both pack [::ttk::frame $frame.sb] -side top -fill x -padx 3 -pady 2 pack [set path(sb) [ttk::label $frame.sb.l -textvariable ::wikihelp::info(sb) -width 75]] -side left -expand 0 -fill none $path(pane) add [set left [::ttk::frame $path(pane).left -relief ridge]] $path(pane) add [set right [::ttk::frame $path(pane).right -relief ridge -borderwidth 2]] set tree [::ttk::treeview $left.tree -padding [list 0 0 0 0] -selectmode browse -yscrollcommand [list $left.sb set] -show [list tree]] bind $tree <MouseWheel> "[bind Treeview <MouseWheel>];break" $tree tag configure link -foreground black $tree tag configure badlink -foreground red $tree tag configure wikilink;# used for internal links $tree tag configure weblink;# used for external links $tree tag bind wikilink <1> [list ::wikihelp::clickTopic %W %x %y] set treeSB [::ttk::scrollbar $left.sb -orient vertical -command [list $tree yview]] grid $tree $treeSB -sticky nsew grid rowconfigure $left $tree -weight 1 grid $tree $treeSB -sticky nsew set text [text $right.text -font TkDefaultFont -width 115 -height 30 -wrap word \ -yscrollcommand [list $right.sbY set] \ -xscrollcommand [list $right.sbX set] \ -state disabled] set margin 20 $text tag configure margins -lmargin1 $margin -lmargin2 $margin set listIndent [font measure TkDefaultFont -displayof $text " \u2022 "];# deliberately missing 1 trailing space incr listIndent 5 for {set i 1} {$i <= 16} {incr i} { $text tag configure "marginList$i" \ -lmargin1 [expr {$margin + ($listIndent*($i-1))}] \ -lmargin2 [expr {$margin + ($listIndent*$i)}] } $text tag configure bold -font [list {*}[font actual TkDefaultFont] -weight bold] $text tag configure bolditalic -font [list {*}[font actual TkDefaultFont] -weight bold -slant italic] $text tag configure italic -font [list {*}[font actual TkDefaultFont] -slant italic] $text tag configure noparse -font TkFixedFont $text tag configure noparsebold -font [list {*}[font actual TkFixedFont] -weight bold] $text tag configure noparseitalic -font [list {*}[font actual TkFixedFont] -slant italic] $text tag configure noparsebolditalic -font [list {*}[font actual TkFixedFont] -weight bold -slant italic] $text tag configure header1 -font [list {*}[font actual TkDefaultFont] -size 22] $text tag configure header2 -font [list {*}[font actual TkDefaultFont] -size 18] $text tag configure header3 -font [list {*}[font actual TkDefaultFont] -size 15] $text tag configure hr -overstrike 1 -justify center -spacing1 15 -spacing3 15 $text tag configure link -foreground blue $text tag configure badlink -foreground red $text tag bind link <Enter> [list $text configure -cursor hand2] $text tag bind link <Leave> [list $text configure -cursor {}] $text tag bind wikilink <1> [list ::wikihelp::clickTopic %W %x %y] $text tag bind weblink <1> [list ::wikihelp::webLink %W] $text tag bind weblink <Enter> [list ::wikihelp::linkHover $text 1] $text tag bind weblink <Leave> [list ::wikihelp::linkHover $text 0] $text tag bind wikilink <Enter> [list ::wikihelp::linkHover $text 2] $text tag bind wikilink <Leave> [list ::wikihelp::linkHover $text 0] set textSBY [::ttk::scrollbar $right.sbY -orient vertical -command [list $text yview]] set textSBX [::ttk::scrollbar $right.sbX -orient horizontal -command [list $text xview]] grid $text $textSBY -sticky nsew grid $textSBX -sticky nswe grid rowconfigure $right $text -weight 1 grid columnconfigure $right $text -weight 1 set path(tree) $tree set path(text) $text populateTOC if { ![showTopic $topic] && ![showTopic $info(defaultTopic)]} { showTopic $info(TOC) } return; };# ::wikihelp::help #: proc ::wikihelp::linkHover #: arg widget widget path #: arg type 1 = hovering web link, 2 = hovering wiki link, 0 = leaving link #: desc When hovering a link, set the status bar appropriately. (When leaving a link, set to the path of the current page.) #: return nothing proc ::wikihelp::linkHover {widget type} { variable path; variable info; if { !$type } { $path(sb) configure -textvariable ::wikihelp::info(disppath) return; } if { $type == 1 } { set type "weblink" } elseif { $type == 2 } { set type "wikilink" } else { return; } set tags [$widget tag names current] if { [set where [lsearch -glob $tags "linkTo:*"]] >= 0 } { set topic [string range [lindex $tags $where] 7 end] } else { set topic [$widget get {*}[$widget tag prevrange $type "current + 1 char"]] } if { $type eq "weblink" } { set info(sb) [T "Go to webpage: %s" $topic] } else { set info(sb) [T "View helpfile '%s'" $topic] } $path(sb) configure -textvariable ::wikihelp::info(sb) return; };# ::wikihelp::linkHover #: proc ::wikihelp::webLink #: arg widget widget path #: desc Process the click of a web link in $widget #: return nothing proc ::wikihelp::webLink {widget} { set tags [$widget tag names current] if { [set where [lsearch -glob $tags "linkTo:*"]] >= 0 } { set link [string range [lindex $tags $where] 7 end] } else { set link [$widget get {*}[$widget tag prevrange weblink "current + 1 char"]] } launchWebPage $link return; };# ::wikihelp::webLink #: proc ::wikihelp::launchWebPage #: arg url the webpage to launch #: desc attempt to load the webpage $url in a browser. This proc may need to be more robust at detecting default browsers. #: return nothing proc ::wikihelp::launchWebPage {url} { variable misc; if { ![string match "http://*" $url] && ![string match "https://*" $url] && ![string match "ftp://*" $url] } { set url "http://$url" } if { $misc(browserCmd) ne "" } { #set command [string map [list %1 $url] $misc(browserCmd)] # Try and parse out the command; this is up to the first space, if there's no leading quote, or # the quoted string if there is. if { [string range $misc(browserCmd) 0 0] eq {"} } { # A quoted string. set secondQuote [string first {"} $misc(browserCmd) 1] if { $secondQuote != -1 } { set lead [string range $misc(browserCmd) 1 [expr {$secondQuote - 1}]] set rest [string range $misc(browserCmd) [expr {$secondQuote + 1}] end] } } else { set space [string first " " $misc(browserCmd) ] set lead [string range $misc(browserCmd) 0 [expr {$space - 1}]] set rest [string range $misc(browserCmd) [expr {$space + 1}] end] } if { $lead ne "" } { set prefix [auto_execok $lead] if { $prefix ne "" } { set command "$prefix [string map [list %1 $url] $rest]" } } } else { # Try and figure out what to do. From https://wiki.tcl-lang.org/557 switch $::tcl_platform(os) { Darwin { set command [list open $url] } HP-UX - Linux - SunOS { foreach executable {firefox mozilla netscape iexplorer opera lynx w3m links epiphany galeon konqueror mosaic amaya browsex elinks} { set executable [auto_execok $executable] if { $executable ne "" } { set command [list $executable $url] break; } } } "Windows 95" { set command "[auto_execok start] {} [list $url]" } "Windows NT" { set command "[auto_execok start] {} [list [string map [list ^ ^^ & ^&] $url]]" } } } if { ![info exists command] || [catch {exec {*}$command &} err] } { if { [info exists err] } { error "Unable to launch browser via \"$command\": $err" } } return; };# ::wikihelp::launchWebPage #: proc ::wikihelp::clickTopic #: arg widget The widget clicked #: arg x x-coord of click #: arg y y-coord of click #: desc Process a click in $widget (either text or tree) and show the topic being clicked at $x,$y #: return nothing proc ::wikihelp::clickTopic {widget x y} { if { [winfo class $widget] eq "Text" } { # Link in the text widget set tags [$widget tag names current] if { [set where [lsearch -glob $tags "linkTo:*"]] >= 0 } { set topic [string range [lindex $tags $where] 7 end] } else { set topic [$widget get {*}[$widget tag prevrange wikilink "current + 1 char"]] } } else { # Link in the TOC Tree set tags [$widget item [$widget identify row $x $y] -tags] set topic [string range [lsearch -inline -glob $tags "linkTo:*"] 7 end] } showTopic $topic return; };# ::wikihelp::clickTopic #: proc ::wikihelp::showTopic #: arg topic The topic to display #: desc Show the given $topic in the text window #: return 1 if showed the topic successfully, 0 if not proc ::wikihelp::showTopic {topic} { variable index; variable path; variable info; variable history; if { [info exists index(file,$topic)] } { # Fine } elseif { [info exists index(summary,$topic)] } { set topic $index(summary,$topic);# we want the filename } else { # Not found return 0; } if { [catch {open [file join $info(filepath) $topic.wiki] r} fid] } { return 0;# can't open file } fconfigure $fid -encoding utf-8 $path(text) configure -state normal $path(text) delete 1.0 end foreach {text tags} [parse [read $fid]] { if { $text eq "<<IMAGE>>" } { $path(text) insert end "\n" $path(text) image create end -image $tags -padx 20 } else { $path(text) insert end $text $tags } } $path(text) configure -state disabled catch {close $fid} # Show the topic in the tree set curr [lindex [$path(tree) selection] 0] if { [info exists index(list,$topic)] && ($curr eq "" || $curr ni $index(list,$topic)) } { set new [lindex $index(list,$topic) 0] $path(tree) see $new $path(tree) selection set $new $path(tree) focus $new set join "" set disppath "" while { $new ne "" } { set text [$path(tree) item $new -text] set disppath "$text$join$disppath" set join " [format %c 8594] " set new [$path(tree) parent $new] } set info(disppath) $disppath } return 1; };# ::wikihelp::showTopic #: proc ::wikihelp::parse #: arg input String of text to parse #: desc Parse $input, which is a Wiki page, and return a list of text/tags for insertion into a text widget #: return list of text/tag pairs for [$textWidget insert] proc ::wikihelp::parse {input} { set italic 0 set bold 0 set noparse 0 set multinoparse 0 set linkparse 0 set headerparse 0 set values [list] set input [string map [list \r\n \n \r \n] $input] set buffer "" set past_pragma 0 set html_entity_names [list lt gt copy nbsp amp] set html_entity_chars [list "<" ">" [format %c 169] " " "&"];# <-- this uses a regular space for since we don't squish anyway foreach line [split $input "\n"] { set marginTag "margins" if { !$past_pragma } { if { [string index $line 0] eq "#" } { continue; # skip #pragma lines like #summary } else { set past_pragma 1 } } if { !$multinoparse && [string equal $line "\{\{\{"] } { set multinoparse 1 continue; } elseif { $multinoparse && [string equal $line "\}\}\}"] } { set multinoparse 0 continue; } elseif { $multinoparse } { # Don't parse this line lappend values "$line\n" [parseTags [list $marginTag] $bold $italic 1] continue; } elseif { [regexp {^#(summary|labels|sidebar)} $line] } { continue; } elseif { [regexp {^.*----+.*$} $line] } { # Horizontal rule lappend values " " hr "\n" "" continue; } elseif { [regexp {^( {2,})([*#]) *(.+)$} $line -> newlistdepth newlisttype rest] } { # List #abc set marginTag to list-depth tag with extra indents, insert better list chars, etc set newlistdepth [expr { [string length $newlistdepth] / 2}] if { ![info exists list($newlistdepth,type)] || $list($newlistdepth,type) ne $newlisttype } { set list($newlistdepth,type) $newlisttype if { $newlisttype eq "#" } { set list($newlistdepth,count) 0 } } if { $list($newlistdepth,type) eq "#" } { set listchar [incr $list($newlistdepth,count)] } else { set listchar [lindex [list \u2022 \u25e6 \u25a0 \u25a1 \u25c6 \u25c7 \u25b6 \u25b7] [expr {($newlistdepth-1) % 8}]] } if { $newlistdepth > 16 } { set marginTag "marginList16"; # We only have indent tags configured up to marginList16. } else { set marginTag "marginList$newlistdepth" } lappend values " $listchar " [list $marginTag] set line $rest } else { } # Get all non-special chars while { [string length $line]} { if { $noparse } { regexp {^([^`]*)(.)?(.*?)$} $line -> easy char line } elseif { $linkparse } { regexp {^([^]]*)(.)?(.*?)$} $line -> easy char line } else { regexp {^([^*_`=[]*)(.)?(.*?)$} $line -> easy char line } if { $easy ne "" } { if { $linkparse || $headerparse } { append buffer $easy } else { # insert everything up to our special char lappend values $easy [parseTags [list $marginTag] $bold $italic $noparse] } } if { $char eq "" } { continue; # nothing special to parse } switch -exact -- $char { * {set bold [expr {!$bold}]} _ {set italic [expr {!$italic}]} ` {set noparse [expr {!$noparse}]} \[ {set linkparse 1} \] {set linkparse 0 set link [parseLink $buffer] if { [lindex $link 0] eq "link" } { lappend values [lindex $link 2] [parseTags [concat $marginTag [lindex $link 3]] $bold $italic $noparse] } elseif { [lindex $link 0] eq "image" } { lappend values [list "<<IMAGE>>"] [list [lindex $link 2]] } elseif { [lindex $link 0] eq "plain" } { # Was probably an anchor which we don't support, but are good enough to ignore gracefully lappend values [lindex $link 1] [parseTags $marginTag $bold $italic $noparse] } set buffer "" } = {if { [string length $buffer] } { # Closing the header if { ![info exists headersize] } { set headersize $headerparse } incr headerparse -1 if { !$headerparse } { # Finished the close lappend values [string trim $buffer] [parseTags [list $marginTag header$headersize] $bold $italic $noparse] unset headersize set buffer "" } } else { incr headerparse } } } } if { $linkparse } { # We'll allow multi-line links } elseif { $headerparse } { # But not multiline headers if { $headerparse > 3 } { set headerparse 3 } if { [string length $buffer] } { lappend values [string trim $buffer] [parseTags [list $marginTag header$headerparse] $bold $italic $noparse] } set headerparse 0 set buffer "" } elseif { $noparse } { set noparse 0 } lappend values "\n" "" } # OK, end of the file. Let's see what we didn't close... if { $linkparse } { # OK, we left a link open. We almost certainly didn't mean to. Hrm. Let's just ignore it. :P } # OK, actually, links are the only multi-line markup that buffers the text, so we're good. return $values; };# ::wikihelp::parse #: proc ::wikihelp::parseLink #: arg str String to parse #: desc Parse out WikiLink text, which will be in the format "<linkto>[ <name>]". (If no <name>, it defaults to <linkto>.) #: desc If the link is to a Wiki-hosted image, try and convert to display the image instead. Otherwise, link as normal. #: return [list <type> <linkto> <name> [list <tags>]] where <type> is "text" or "image", and where <tags> are the appropriate text widget tags (badlink, or link + weblink/wikilink, possibly + linkTo:<linkto>) proc ::wikihelp::parseLink {str} { variable index; variable info; if { [set space [string first " " $str]] > -1 } { set linkto [string range $str 0 $space-1] set name [string trim [string range $str $space+1 end]] } else { set linkto $str set name "" } if { $name eq $linkto } { set name "" } if { [string index $linkto 0] eq "#" } { # An anchor on the current page - abort! if { $name eq "" } { set name [string range $linkto 1 end] } return [list "plain" $name]; } # Check for a Wiki Image if { [string equal -length $::wikihelp::images::wikiImagesLen $::wikihelp::images::wikiImages $linkto] } { # Looks like we have one. set imagename [file tail $linkto] if { [file extension $imagename] eq ".gif" && [file exists [file join $info(filepath) $imagename]] } { if { "::wikihelp::images::$imagename" ni [image names] } { image create photo ::wikihelp::images::$imagename -file [file join $info(filepath) $imagename] } return [list "image" $linkto ::wikihelp::images::$imagename]; } # If we get here, we didn't find the image, so we continue as normal with a link. } if { [regexp {^(f|ht)tps?://.+} $linkto] } { set tags [list weblink link "linkTo:$linkto"] } else { # Check for an anchor, and strip it if found if { [set anchor [string first "#" $linkto]] > -1 } { set linkto [string range $linkto 0 $anchor-1] } if { [info exists index(file,$linkto)] } { set tags [list link wikilink] if { $name ne "" } { lappend tags "linkTo:$linkto" } } else { set tags [list badlink] } } if { $name eq "" } { set name $linkto } return [list "link" $linkto $name $tags]; };# ::wikihelp::parseLink #: proc ::wikihelp::parseTags #: arg tags Starting tags #: arg bold Should we bold? #: arg italics Should we italicize? #: arg noparse Display fixed-width? #: desc Append "bolditalic", "bold" or "italic" tags, and/or "noparse", if appropriate, to $tags, and return result #: return New list of tags proc ::wikihelp::parseTags {tags bold italic noparse} { set str "[lindex [list "" "noparse"] $noparse][lindex [list "" "bold"] $bold][lindex [list "" "italic"] $italic]" if { $str ne "" } { lappend tags $str } return $tags; };# ::wikihelp::parseTags #: proc ::wikihelp::populateTOC #: desc Populate the Table of Contents treeview widget. It's in the format of a Wiki nested list, but uses special-case code b/c we need to insert into a treeview, not display in the textwidget. #: return nothing proc ::wikihelp::populateTOC {} { variable info; variable path; variable index; set tree $path(tree) $tree delete [$tree children {}] array unset index list,* # If possible, we'll use the designated Table of Contents file. if { [info exists info(TOC)] && [file exists [set file [file join $info(filepath) $info(TOC).wiki]]] && [file readable $file] } { # We have a TOC file, use that. if { ![catch {open $file r} fid] } { set parents [list {}] set indents [list 1] set parent {} set indent 1 set last {} while { [gets $fid line] >= 0 && ![eof $fid] } { if { ![regexp {^( {2,})\* *(.+?) *$} $line -> spaces topic] } { continue; # Skip non-list items } set topic [regsub -all -- "`(.+?)`" $topic {\1}];# Since we don't parse this (but Google does), strip out any backticks protecting text set count [expr {[string length $spaces] / 2}] if { $count > $indent } { lappend indents $count lappend parents $last set parent $last set indent $count } elseif { $count < $indent } { set cutoff [lsearch $indents $count] if { $cutoff == -1 } { set cutoff [expr {[llength $indents]-2}] } set indents [lrange $indents 0 $cutoff] set parents [lrange $parents 0 $cutoff] set indent [lindex $indents end] set parent [lindex $parents end] } if { [string match {\[*\]} $topic] } { set topic [string range $topic 1 end-1] if { [set space [string first " " $topic]] > -1 } { set summary [string range $topic $space+1 end] set topic [string range $topic 0 $space-1] if { [string trim $summary] eq "" } { set summary $topic } } else { set summary $topic } if { [info exists index(file,$topic)] } { set tags [list link wikilink linkTo:$topic] } else { set tags [list badlink] } set file $topic } elseif { [info exists index(file,$topic)] } { set file $topic set summary $index(file,$topic) set tags [list link wikilink linkTo:$file] } elseif { [info exists index(summary,$topic)] } { set file $index(summary,$topic) set summary $topic set tags [list link wikilink linkTo:$file] } else { set file $topic set summary $topic set tags [list];# don't badlink things that weren't linked anyway [list badlink] } set last [$tree insert $parent end -text $summary -tags $tags] lappend index(list,$file) $last } close $fid return; } } # If we get here, we can't find the TOC file, so we'll just list every page we have. foreach x [lsort -dictionary [array names index filename,]] { set last [$tree insert {} end -text $index($x) -tags [list link "linkTo:[string range $x 9 end]"]] lappend index(list,[string range $x 9 end]) $last } return; };# ::wikihelp::populateTOC #: proc ::wikihelp::index #: desc Index the available files, storing the filename and the "#summary" line (if present) to use as a title. #: return nothing proc ::wikihelp::index {} { variable info; variable path; variable index; foreach x [glob -nocomplain -dir $info(filepath) *.wiki] { if { [catch {open $x r} fid] } { continue; # Don't even track the filename, since we can't open it to read } set filename [file rootname [file tail $x]] set summary $filename ;# default in case no summary is present while { [gets $fid line] >= 0 && ![eof $fid] } { if { [string match "#summary *" $line] } { set summary [string trim [string range $line 9 end]] break; } } set index(file,$filename) $summary set index(summary,$summary) $filename close $fid } set info(indexed) 1 return; };# ::wikihelp::index package provide wikihelp 2.0.0 catch {console show} wikihelp::help
Zipguy 2013-09-03 - You can find out my email address by clicking on Zipguy.
Wow it sounds extremely interesting to me. I've got a problem extremely similar to the one you're describing.
I thought hard about the articles I've been writing for Mozilla, to which I am a contributor of articles, and am a big fan of good freeware, like tcl. I had a pretty good editor, which I've modified a lot, and it works extremely well. It is at ML - Heavily Modified & Improved. So I started to write a version of it that was to be entitled AWF (Article Work Files). But I had a large problem with getting code that understands wiki format. Here's what I had so far:
I looked hard for the icons (which are highlighted in red).
But I got lost in here, tcl'ers wiki, looking for anything that understood wiki, in tcl, but I was unhappy. There were lots of things that came back on my search. So, I got sidetracked, which is kind of easy for me, to happen.
So I'll have a look at your code, in detail and post something further on this page.