# Taygete Scrap Book - a poor man's clone of Jupyter Notebook
#
# chw June 2019
#
####################################################################

# Load webview and other stuff.

package require twv
package require Markdown

# On MacOSX we need Tk early, otherwise crashes occur.

if {$tcl_platform(os) eq "Darwin"} {
    package require Tk
}

# If nothing is in top toplevel, withdraw it assuming
# we don't need it for now.

if {([info command winfo] eq "winfo") &&
    [winfo exists .] && ([winfo children .] eq "")} {
    wm withdraw .
}

# For tsb related packages.

lappend auto_path [file dirname [info script]]

namespace eval ::tsb {
    variable inload 0
    variable ready 0
    variable dump ""
    variable file ""
}

# The history array of input fields.

array set H {1 ""}

# This is invoked from the JS window.external.invoke function.
# The first non-blank characters are assumed to be the integer id
# of the (input) field.

proc ::tsb::call_from_js {str} {
    variable dump
    if {![string match {[0-9]*} $str]} {
	set dump $str
	return
    }
    set n [string first " " $str]
    if {$n < 0} {
	return
    }
    set id [string trim [string range $str 0 $n]]
    incr n
    set str [string trim [string range $str $n end]]
    if {$id == 0} {
	# Should be the ping, Vasily.
	# But can be the reload, too.
	catch {uplevel \#0 $str}
	return
    }
    set ::H($id) $str
    if {$str eq ""} {
	$::W call Wclear $id 0
	return
    }
    set newfield 0
    set ::ID $id
    if {[string first "#HTML" $str] == 0} {
	set n [string first "\n" $str]
	if {$n > 4} {
	    incr n
	    set str [string range $str $n end]
	}
	$::W call Wraw $id $str 1
	$::W call Inhide $id 1
	set newfield 1
    } elseif {[string first "#MARKDOWN" $str] == 0} {
	set n [string first "\n" $str]
	if {$n > 8} {
	    incr n
	    set str [string range $str $n end]
	}
	set str [Markdown::convert $str]
	$::W call Wraw $id $str 1
	$::W call Inhide $id 1
	set newfield 1
    } elseif {[catch {uplevel \#0 $str} ret opts]} {
	if {[dict get $opts -code] == 4} {
	    # continue
	    $::W call Wclear $id 0
	} else {
	    $::W call Werror $id $::errorInfo
	}
    } else {
	if {$ret ne ""} {
	    $::W call Wresult $id $ret
	} else {
	    $::W call Wclear $id 0
	}
	set newfield 1
    }
    if {$newfield} {
	incr id
	if {![info exists ::H($id)]} {
	    set ::H($id) ""
	    $::W call Field $id
	}
    }
    unset -nocomplain ::ID
}

# Change current (input) field if it consisted only of
# single command "cmd". Useful for "img" and load/save
# when a file dialog is used to select the file name.

proc ::tsb::change_field {cmd str} {
    if {[info exists ::ID]} {
	set id $::ID
	if {[string trim $::H($id)] eq $cmd} {
	    $::W call Winput $id $str
	    set ::H($id) $str
	}
    }
}

# Write raw HTML to current output field.

proc htmlraw {str {hidden 0}} {
    if {[info exists ::ID]} {
	$::W call Wraw $::ID $str
	if {$hidden} {
	    $::W call Inhide $::ID 1
	}
    }
    return
}

# Quote angle brackets et.al. for HTML output.

proc ::tsb::htmlquote {str} {
    return [string map {& \&amp; < \&lt; > \&gt; "\"" \&quot;} $str]
}

# Simple HTML table formatter. If "ncols" is negative,
# make a header, otherwise omit headers. If "ncols" is
# not an integer it's taken as header list.

proc table {ncols data} {
    if {![string is integer $ncols]} {
	set tdata $data
	set data $ncols
	set ncols [expr {0 - [llength $data]}]
    }
    if {$ncols == 0} {
	return
    }
    set ret "\n<table class='tbl'>"
    if {$ncols < 0} {
	set ncols [expr {0 - $ncols}]
	append ret "\n <thead>\n  <tr>"
	for {set i 0} {$i < $ncols} {incr i} {
	    append ret "\n   <th><pre class='tdpre'>" \
		[::tsb::htmlquote [lindex $data $i]] "</pre></th>"
	}
	append ret "\n  </tr>\n </thead>"
	set data [lrange $data $ncols end]
    }
    if {[info exists tdata]} {
	set data $tdata
    }
    append ret "\n <tbody>"
    while {[llength $data]} {
	append ret "\n  <tr>"
	for {set i 0} {$i < $ncols} {incr i} {
	    append ret "\n   <td><pre class='tdpre'>" \
		[::tsb::htmlquote [lindex $data $i]] "</pre></td>"
	}
	append ret "\n  </tr>"
	set data [lrange $data $ncols end]
    }
    append ret "\n </tbody>\n</table><br>\n"
    htmlraw $ret
}

# Insert various headers (<h1>, <h2> ...) and rulers.

proc h1 {str} {
    htmlraw "<h1>[::tsb::htmlquote $str]</h1>" 1
}

proc h2 {str} {
    htmlraw "<h2>[::tsb::htmlquote $str]</h2>" 1
}

proc h3 {str} {
    htmlraw "<h3>[::tsb::htmlquote $str]</h3>" 1
}

proc h4 {str} {
    htmlraw "<h4>[::tsb::htmlquote $str]</h4>" 1
}

proc h5 {str} {
    htmlraw "<h5>[::tsb::htmlquote $str]</h5>" 1
}

proc hr {} {
    set hr "<hr style='margin-left: 1em; margin-right: 1em;"
    append hr " margin-top: 1em; margin-bottom: 1em;"
    append hr " background-color: #000; height: 2px;'>"
    htmlraw $hr 1
}

# Insert image from local file.

proc img {{name {}} {import 0} {mime {}}} {
    set selname 0
    if {$name eq ""} {
	set selname 1
	set name [$::W dialog open "Select Image"]
    }
    if {$name eq ""} {
	return
    }
    # This needs work.
    if {$mime ne ""} {
	set mime [::tsb::htmlquote $mime]
    } else {
	set ext [string tolower [string trimleft [file extension $name] "."]]
	set mime image/$ext
    }
    if {![catch {open $name rb} f]} {
	set ret "<img style='margin: 1em;' src='data:$mime;base64,\n"
	append ret [binary encode base64 -maxlen 78 [read $f]]
	append ret "'>\n"
	close $f
	if {$selname || $import} {
	    set cmd [dict get [info frame -1] cmd]
	    if {$import} {
		set newcmd "#HTML\n$ret"
	    } else {
		set newcmd $cmd
		lappend newcmd $name
	    }
	    ::tsb::change_field $cmd $newcmd
	}
	htmlraw $ret 1
    }
    return
}

# Insert image from byte array.

proc img_from_binary {data mime {hidden 1}} {
    set mime [::tsb::htmlquote $mime]
    set ret "<img style='margin: 1em;' src='data:$mime;base64,\n"
    append ret [binary encode base64 -maxlen 78 $data]
    append ret "'>\n"
    htmlraw $ret $hidden
    return
}

# Load and save functions.

proc ::tsb::load {{name {}}} {
    variable inload
    if {$inload} {
	return
    }
    if {$name eq ""} {
	set name [$::W dialog open "Load From File"]
    }
    if {$name eq ""} {
	return
    }
    set f [open $name r]
    set data [read $f]
    close $f
    array set h $data
    after idle [list ::tsb::loadh $data]
    set ::tsb::file $name
    $::W title "$::tsb::title - [file tail $name]"
    return -code 4 ;# continue
}

proc ::tsb::loadh {data} {
    variable inload
    if {$inload} {
	return
    }
    set inload 1
    unset -nocomplain ::H
    $::W call ClearFields
    foreach {key value} $data {
	if {[string is integer $key]} {
	    set h($key) $value
	}
    }
    foreach key [lsort -integer [array names h]] {
        $::W call InitField $key $h($key)
    }
    foreach key [lsort -integer [array names h]] {
	set ::H($key) ""
	set next $key
	incr next
	set ::H($next) ""
	::tsb::call_from_js "$key $h($key)"
    }
    if {[info exists next]} {
	unset ::H($next)
    }
    $::W call GotoTop
    after cancel ::tsb::reload0
    set inload 0
}

proc ::tsb::save {{name {}}} {
    variable inload
    if {$inload} {
	return
    }
    set selname 0
    if {$name eq ""} {
	set selname 1
	set name [$::W dialog open "Save To File"]
    }
    if {$name eq ""} {
	return
    }
    set f [open $name w]
    puts $f [array get ::H]
    close $f
    if {$selname} {
	set cmd [dict get [info frame -1] cmd]
	set newcmd $cmd
	lappend newcmd $name
	::tsb::change_field $cmd $newcmd
    }
    set ::tsb::file $name
    $::W title "$::tsb::title - [file tail $name]"
    return -code 4 ;# continue
}

# The sin: the ping function, to keep events alive and kicking.

proc ::tsb::ping {args} {
    # Nothing to see here ...
}

# Reload document related functions; used for initial load, too.

proc ::tsb::reload0 {} {
    variable ready
    variable inload
    if {$inload} {
	return
    }
    if {![info exists ::W]} {
	# Can happen during init of webview.
	return
    }
    set ready 1
    $::W call document.write $::tsb::D
    $::W call document.close
    set title $::tsb::title
    if {$::tsb::file ne ""} {
	append title " - [file tail $::tsb::file]"
    }
    $::W title $title
    ::tsb::loadh [array get ::H]
}

proc ::tsb::reload {args} {
    variable inload
    if {$inload} {
	return
    }
    after cancel ::tsb::reload0
    after idle ::tsb::reload0
    return -code 4 ;# continue
}

# Clear page.

proc ::tsb::clear {} {
    variable inload
    if {$inload} {
	return
    }
    after idle ::tsb::clearh
    set ::tsb::file ""
    $::W title $::tsb::title
    return -code 4 ;# continue
}

proc ::tsb::clearh {} {
    unset -nocomplain ::H
    $::W call ClearFields
    set ::H(1) ""
    $::W call InitField 1 $::H(1)
}

# Evaluate field.

proc ::tsb::eval {id} {
    tailcall $::W call Feval $id
}

# Goto field.

proc ::tsb::goto {{id {}}} {
    if {$id eq ""} {
	tailcall $::W call GotoTop
    }
    tailcall $::W call GotoField $id 0
}

# Print page; on Windows seems not to work, but pressing
# <Control-p> opens the printer dialog at least.

proc ::tsb::print {} {
    tailcall $::W call window.print
}

# Dump to HTML; the weird vwait/loop construct needs to be revised.

proc ::tsb::dump {} {
    variable dump
    set dump ""
    $::W call Dump
    set count 10
    while {$count > 0} {
	incr count -1
	$::W loop 0
	if {$dump ne ""} {
	    break
	}
	set var [namespace current]::dump
	after 10 [subst -nocommands {set $var [set $var]}]
	vwait $var
    }
    return $dump
}

# Write HTML to file.

proc ::tsb::wrhtml {{name {}}} {
    variable inload
    variable D_head
    variable D_style
    variable title
    variable file
    set selname 0
    if {$name eq ""} {
	set selname 1
	set name [$::W dialog open "Write HTML To File"]
    }
    if {$name eq ""} {
	return
    }
    if {$selname} {
	set cmd [dict get [info frame -1] cmd]
	set newcmd $cmd
	lappend newcmd $name
	::tsb::change_field $cmd $newcmd
    }
    if {[info exists ::ID]} {
	$::W call Wclear $::ID 0
    }
    set t $title
    if {$file ne ""} {
	append t " - [file tail $file]"
    }
    set t [htmlquote $t]
    set f [open $name w]
    puts $f $D_head
    puts $f "<title>$t</title>"
    # No hover in output
    set style $D_style
    regsub -all -- :hover $style :nohover style
    puts $f $style
    puts $f "</head>\n<body>"
    puts $f [::tsb::dump]
    puts $f "</body>\n</html>"
    close $f
    return -code 4 ;# continue
}

# A minimal canvas emulation for plotchart. The svg method
# produces SVG into the current (output) field.
#
#  set C [::tsb::canvas ...]
#  $C create line 10 10 20 20 -fill black ...
#  ...
#  $C svg ?viewbox?
#  $C destroy

namespace eval ::tsb {

    variable C
    array set C {cid 0}

    proc tagfind {c tags} {
	variable C
	set all $C($c,dlist)
	foreach tt $tags {
	    if {$tt eq "&&"} {
		continue
	    }
	    set sub {}
	    if {[string is integer $tt] && [info exists C($c,$tt)]} {
		set sub $tt
	    } else {
		foreach item $all {
		    set t [dict get $C($c,$item) -tags]
		    if {$tt in $t} {
			lappend sub $item
		    }
		}
	    }
	    set all $sub
	}
	return $all
    }

    proc canvascmd {c cmd args} {
	variable C
	set ret ""
	if {![catch {tcl::prefix match {
	    create bbox cget raise lower destroy delete coords find svg
	} ncmd}]} {
	    set cmd $ncmd
	}
	switch -glob -- $cmd {
	    create {
		set args [lassign $args type]
		set id [incr C($c,id)]
		set coords {}
		set count 0
		foreach arg $args {
		    if {[string is double $arg]} {
			lappend coords $arg
		    } else {
			break
		    }
		    incr count
		}
		dict set C($c,$id) -type $type
		dict set C($c,$id) -coords $coords
		dict set C($c,$id) -tags {}
		foreach {opt val} [lrange $args $count end] {
		    if {[string match -ta* $opt]} {
			set opt -tags
		    }
		    dict set C($c,$id) $opt $val
		}
		lappend C($c,dlist) $id
		set ret $id
	    }
	    bbox {
		# assumes measuring text item with "M"
		set ret {0 0 12 16}
	    }
	    cget {
		if {[catch {tcl::prefix match {
		    -bd -borderwidth -width -height
		} [lindex $args 0]} opt]} {
		    set opt [lindex $args 0]
		}
		switch -glob -- $opt {
		    -bd - -borderwidth {
			set ret 0
		    }
		    -width {
			set ret $C($c,width)
		    }
		    -height {
			set ret $C($c,height)
		    }
		}
	    }
	    raise {
		lassign $args tags
		set all $C($c,dlist)
		foreach t [tagfind $c $tags] {
		    set n [lsearch -exact $all $t]
		    if {$n >= 0} {
			set all [lreplace $all $n $n]
			lappend all $t
		    }
		}
		set C($c,dlist) $all
	    }
	    lower {
		lassign $args tags
		set all $C($c,dlist)
		foreach t [tagfind $c $tags] {
		    set n [lsearch -exact $all $t]
		    if {$n >= 0} {
			set all [lreplace $all $n $n]
			set all [linsert $all 0 $t]
		    }
		}
		set C($c,dlist) $all
	    }
	    destroy {
		set cmd $C($c,cmd)
		foreach name [array names C $c,*] {
		    unset C($name)
		}
		rename "::$cmd" {}
	    }
	    delete {
		lassign $args tags
		set all $C($c,dlist)
		foreach t [tagfind $c $tags] {
		    set n [lsearch -exact $all $t]
		    if {$n >= 0} {
			set all [lreplace $all $n $n]
			unset C($c,$t)
		    }
		}
		set C($c,dlist) $all
	    }
	    coords {
		set coords [lassign $args id]
		if {![info exists C($c,$id)]} {
		    foreach item $C($c,dlist) {
			set t [dict get $C($c,$item) -tags]
			if {$id in $t} {
			    if {[llength $coords]} {
				dict set C($c,$item) -coords $coords
			    } else {
				set ret [dict get $C($c,$item) -coords]
			    }
			}
		    }
		} elseif {[llength $coords]} {
		    dict set C($c,$id) -coords $coords
		} else {
		    set ret [dict get $C($c,$id) -coords]
		}
	    }
	    find {
		# find ?withtag?
		set args [lassign $args tags]
		if {$tags eq "withtag"} {
		    lassign $args tags
		}
		set ret [tagfind $c $tags]
	    }
	    svg {
		set vx ""
		set vy ""
		set vw ""
		set vh ""
		lassign $args vx vy vw vh
		package require can2svg
		set xml "<?xml version='1.0'?>\n"
		append xml "<svg width='$C($c,width)' height='$C($c,height)'"
		if {$vx ne "" && $vy ne "" && $vw ne "" && $vh ne ""} {
		    append xml " viewBox='$vx $vy $vw $vh'"
		}
		append xml " version='1.1'"
		append xml " xmlns='http://www.w3.org/2000/svg'"
		append xml " xmlns:xlink='http://www.w3.org/1999/xlink'>\n"
		foreach item $C($c,dlist) {
		    set type [dict get $C($c,$item) -type]
		    switch -- $type {
			image - window {
			    continue
			}
		    }
		    set cmd [list create $type \
				 {*}[dict get $C($c,$item) -coords]]
		    foreach key [dict keys $C($c,$item)] {
			switch -- $key {
			    -tags - -type - -coords {
				# already done or ignored
			    }
			    default {
				lappend cmd $key [dict get $C($c,$item) $key]
			    }
			}
		    }
		    append xml "  " [can2svg::can2svg $cmd {*}$args] "\n"
		}
		append xml "</svg>\n"
		# If (output) field set, use it; otherwise use
		# the current one from ::ID and remember it.
		if {[info exists C($c,field)]} {
		    $::W call Wclear $C($c,field) 1
		    set fid $C($c,field)
		} elseif {[info exists ::ID]} {
		    set fid $::ID
		    set C($c,field) $fid
		}
		if {[info exists fid]} {
		    $::W call Wraw $fid $xml
		}
	    }
	}
	return $ret
    }

    proc canvas {args} {
	variable C
	set cid $C(cid)
	set cmd tsb_canv_obj$cid
	proc ::$cmd {cmd args} [subst {
	    tailcall [namespace current]::canvascmd $cid \$cmd {*}\$args
	}]
	set C($cid,cmd) $cmd
	set C($cid,dlist) {}
	set C($cid,width) 640
	set C($cid,height) 480
	set C($cid,id) 0
	incr C(cid)
	foreach {key value} $args {
	    if {![catch {tcl::prefix match {-width -height} $key} nkey]} {
		set key $nkey
	    }
	    switch -glob -- $key {
		-width {
		    if {[scan $value %d value]} {
			set C($cid,width) $value
		    }
		}
		-height {
		    if {[scan $value %d value]} {
			set C($cid,height) $value
		    }
		}
	    }
	}
	return $cmd
    }
}

# Replacements for puts, get, read: move originals to tsb namespace.

rename puts ::tsb::puts
rename gets ::tsb::gets
rename read ::tsb::read

proc puts {args} {
    set len [llength $args]
    lassign $args arg1 arg2 arg3
    if {[info exists ::ID]} {
	switch $len {
	    1 {
		$::W call Wresult $::ID "${arg1}\n"
		return
	    }
	    2 {
		switch -- $arg1 {
		    -nonewline {
			$::W call Wresult $::ID $arg2
			return
		    }
		    stdout {
			$::W call Wresult $::ID "${arg2}\n"
			return
		    }
		    stderr {
			$::W call Werror $::ID "${arg2}\n"
			return
		    }
		}
	    }
	    3 {
		if {($arg1 eq "-nonewline") && ($arg2 eq "stdout")} {
		    $::W call Wresult $::ID $arg3
		    return
		} elseif {($arg1 eq "-nonewline") && ($arg2 eq "stderr")} {
		    $::W call Werror $::ID $arg3
		    return
		}
	    }
	}
    }
    if {[catch [linsert $args 0 ::tsb::puts] msg]} {
	return -code error $msg
    }
    return $msg
}

proc gets {args} {
    set len [llength $args]
    lassign $args arg1 arg2
    if {[info exists ::ID]} {
	switch $len {
	    1 {
		if {$arg1 eq "stdin"} {
		    return ""
		}
	    }
	    2 {
		if {$arg1 eq "stdin"} {
		    upvar 1 $arg2 var
		    set var ""
		    return ""
		}
	    }
	}
    }
    if {[catch {uplevel 1 ::tsb::gets {*}$args} msg]} {
	return -code error $msg
    }
    return $msg
}

proc read {args} {
    set len [llength $args]
    lassign $args arg1 arg2
    if {[info exists ::ID]} {
	switch $len {
	    1 {
		if {$arg1 eq "stdin"} {
		    return ""
		}
	    }
	    2 {
		if {($arg1 eq "stdin") && [string is integer $arg2]} {
		    return ""
		}
		if {($arg1 eq "-nonewline") && ($arg2 eq "stdin")} {
		    return ""
		}
	    }
	}
    }
    if {[catch [linsert $args 0 ::tsb::read] msg]} {
	return -code error $msg
    }
    return $msg
}

namespace eval ::tsb {

    # CSS and JavaScript assembled from various pieces into a big string.

    variable D_head
    variable D_style
    variable D_script
    variable D

    # HEAD
    set D_head "<!DOCTYPE html><html lang=\"en\">\n<head>"
    append D_head "\n<meta charset=\"utf-8\">"
    append D_head "\n<meta name=\"viewport\""
    append D_head " content=\"width=device-width, initial-scale=1.0\">\n"

    # STYLE, CSS
    set D_style {
	<style>
	body {
	    margin: 1em 2em 1em 2em;
	    font-family: sans-serif, Arial, Tahoma, Helvetica;
	    font-size: 90%;
	}
	textarea {
	    font-family: Consolas, "Roboto Mono", "Liberation Mono", monospace;
	    font-size: 90%;
	}
	textarea::placeholder {
	    font-style: italic;
	}
	pre {
	    /* overflow-x: auto; */
	    white-space: pre-wrap;
	    white-space: -moz-pre-wrap;
	    white-space: -pre-wrap;
	    white-space: -o-pre-wrap;
	    word-wrap: break-word;
	}
	pre, code {
	    font-family: Consolas, "Roboto Mono", "Liberation Mono", monospace;
	    font-size: 90%;
	}
	img, svg {
	    max-width: 95%;
	}
	.field {
	    border: 1px solid transparent;
	}
	.field:hover {
	    border: 1px dotted #AAAAAA;
	}
	.infield * {
	    vertical-align: middle;
	}
	.tlabel {
	    font-family: Consolas, "Roboto Mono", "Liberation Mono", monospace;
	    font-size: 90%;
	    margin: 1px;
	    padding: 1px;
	    width: 7ex;
	    display: inline-block;
	    text-align: right;
	}
	.tin {
	    width: 85%;
	    resize: none;
	    overflow: auto;
	}
	.tin, .tin:disabled {
	    border: 1px solid #AAAAAA;
	    background: transparent;
	    color: inherit;
	    margin: 4px;
	    outline: none;
	    padding: 4px;
	}
	.tin:focus {
	    border: 3px solid rgba(81, 203, 238, 1);
	    margin: 2px;
	    padding: 4px;
	}
	.tbl {
	    border: 1px solid;
	    border-collapse: collapse;
	    margin-left: 1em;
	    margin-right: 1em;
	    max-width: 100%;
	}
	.tbl th {
	    border: 1px solid;
	    border-collapse: collapse;
	}
	.tbl td {
	    border: 1px solid;
	    border-collapse: collapse;
	}
	.tdpre {
	    margin: 0.25em;
	}
	</style>
    }

    # SCRIPT
    set D_script {
	<script type="text/javascript">

	var asciimath = {};
	
	var msLike = /MSIE|Trident|Edge/i.test(navigator.userAgent);

	if (!msLike && !window.external) {
	    /* See also webview.h */
	    window.external = {
		invoke: function(str) {
		    window.webkit.messageHandlers.external.postMessage(str);
		}
	    };
	}

	var needsClear = new Array();

	var RunTcl = function(str) {
	    window.external.invoke(str);
	};

	/* The 2nd sin: this drives the Tcl event loop. */
	var Gtimer = window.setInterval(function() {
	    window.external.invoke("0 ::tsb::ping");
	}, 20);

	var Wclear = function(id, later) {
	    /* Type coercion! */
	    if (later == true) {
		needsClear[id] = true;
		return;
	    }
	    if (!needsClear[id]) {
		return;
	    }
	    var output =
		document.getElementById('out' + id + '-pre').firstChild;
	    if (output.innerHTML.length > 0) {
		output.style.color = 'inherit';
		output.innerHTML = '';
	    }
	    output = document.getElementById('out' + id + '-raw');
	    if (output.innerHTML.length > 0) {
		output.innerHTML = '';
	    }
	    needsClear[id] = null;
	};

	var Wresult = function(id, str) {
	    var output =
		document.getElementById('out' + id + '-pre').firstChild;
	    str = str.replace(/&/g, '&amp;');
	    str = str.replace(/</g, '&lt;');
	    str = str.replace(/>/g, '&gt;');
	    str = str.replace(/\"/g, '&quot;');
	    output.style.color = 'inherit';
	    if (needsClear[id]) {
		output.innerHTML = str;
		output = document.getElementById('out' + id + '-raw');
		output.innerHTML = '';
	    } else {
		output.innerHTML += str;
	    }
	    needsClear[id] = null;
	};

	var Werror = function(id, str) {
	    var output =
		document.getElementById('out' + id + '-pre').firstChild;
	    str = str.replace(/&/g, '&amp;');
	    str = str.replace(/</g, '&lt;');
	    str = str.replace(/>/g, '&gt;');
	    str = str.replace(/\"/g, '&quot;');
	    output.style.color = 'red';
	    if (needsClear[id]) {
		output.innerHTML = str;
		output = document.getElementById('out' + id + '-raw');
		output.innerHTML = '';
	    } else {
		output.innerHTML += str;
	    }
	    needsClear[id] = null;
	};

	var Wraw = function(id, str) {
	    var output = document.getElementById('out' + id + '-raw');
	    if (needsClear[id]) {
		output.innerHTML = str;
	    } else {
		output.innerHTML += str;
	    }
	    if (!msLike && asciimath.AMprocessNode) {
		asciimath.AMprocessNode(output, false, null);
	    }
	    if (needsClear[id]) {
		output =
		    document.getElementById('out' + id + '-pre').firstChild;
		if (output.innerHTML.length > 0) {
		    output.style.color = 'inherit';
		    output.innerHTML = '';
		}
	    }
	    needsClear[id] = null;
	};

	var ClrSel = function() {
	    var selection = window.getSelection ?
		window.getSelection() : document.selection ?
		document.selection : null;
	    if (!!selection) {
		selection.empty ?
		    selection.empty() : selection.removeAllRanges();
	    }
	};

	var Inhide = function(id, hide) {
	    var input = document.getElementById('in' + id);
	    var outpre = document.getElementById('out' + id + '-pre');
	    var outraw = document.getElementById('out' + id + '-raw');
	    if ((outpre.clientWidth == 0 || outpre.clientHeight == 0) &&
	        (outraw.clientWidth == 0 || outraw.clientHeight == 0)) {
		hide = false;
	    }
	    /* Type coercion! */
	    if (hide == true) {
		input.style.display = 'none';
		var F = function(event) {
		    outraw.removeEventListener('dblclick', F);
		    outpre.removeEventListener('dblclick', F);
		    Inhide(id, 0);
		    ClrSel();
		    event.returnValue = false;
		    return false;
		};
		outpre.addEventListener('dblclick', F);
		outraw.addEventListener('dblclick', F);
	    } else if (input.style.display === 'none') {
		input.style.display = 'inline';
	    }
	};

	var Winput = function(id, str) {
	    var input = document.getElementById('code' + id);
	    input.value = str;
	    var lines = str.split(/\r?\n|\r/);
	    var nlines = (lines.length < 50) ? lines.length : 50;
	    input.rows = nlines;
	};

	var Feval = function(id) {
	    var input = document.getElementById('code' + id);
	    needsClear[id] = true;
	    RunTcl("" + id + " " + input.value);
	};

	var Field = function(id) {
	    var div = document.createElement('div');
	    div.className = 'field';
	    var html = '\n <div class="infield" id="in' + id + '">';
	    html += '\n  <label class="tlabel"';
	    html += ' for="code' + id + '">in(' + id + ')</label>';
	    html += '\n  <textarea class="tin" id="code' + id + '" rows="1"';
	    html += ' wrap="soft" placeholder="Enter Tcl code, #HTML, or #MARKDOWN. Evaluate with &lt;Shift-Return&gt;."';
	    html += '></textarea>\n </div>';
	    html += '\n <div id="out' + id + '-pre">';
	    html += '<pre></pre></div>';
	    html += '\n <div id="out' + id + '-raw"></div>\n';
	    div.innerHTML = html;
	    document.body.appendChild(div);
	    /* For better readability only. */
	    document.body.appendChild(document.createTextNode('\n'));
	    needsClear[id] = null;
	    var input = document.getElementById('code' + id);
	    input.addEventListener('keydown', function(event) {
		var code;
		var shift = false;
		var ctrl = false;
		if (window.event) {
		    code = window.event.keyCode;
		    if (window.event.shiftKey) {
			shift = true;
		    }
		    if (window.event.ctrlKey) {
			ctrl = true;
		    }
		} else {
		    code = event.keyCode;
		    if (event.shiftKey) {
			shift = true;
		    }
		    if (event.ctrlKey) {
			ctrl = true;
		    }
		    if (shift && code == 13) {
			event.stopPropagation();
		    }
		    if (ctrl && code == 9) {
			event.stopPropagation();
		    }
		}
		if (shift && code == 13) {
		    needsClear[id] = true;
		    RunTcl("" + id + " " + input.value);
		    if (event.preventDefault) {
			event.preventDefault();
		    }
		    event.returnValue = false;
		    return false;
		}
		if (ctrl && code == 9) {
		    var top = input.scrollTop;
		    if (input.setSelectionRange) {
			var start = input.selectionStart;	
			var end = input.selectionEnd;
			input.value = input.value.substring(0, start)
			    + '\t' + input.value.substr(end);
			input.setSelectionRange(start + 1, start + 1);
			input.focus();
		    } else if (input.createTextRange) {
			document.selection.createRange().text = '\t';
			event.returnValue = false;
		    }
		    input.scrollTop = top;
		    if (event.preventDefault) {
			event.preventDefault();
		    }
		    return false;
		}
	    });
	    input.addEventListener('input', function(event) {
		var lines = input.value.split(/\r?\n|\r/);
		var nlines = (lines.length < 50) ? lines.length : 50;
		input.rows = nlines;
	    });
	    input.focus();
	    return input;
	};

	var InitField = function(id, str) {
	    var input = Field(id);
	    input.value = str;
	    var lines = str.split(/\r?\n|\r/);
	    var nlines = (lines.length < 50) ? lines.length : 50;
	    input.rows = nlines;
	};

	var GotoTop = function() {
	    var elem = document.getElementById('top');
	    elem.focus();
	    elem.scrollIntoView();
	};

	var GotoField = function(id, isout) {
	    var elem;
	    /* Type coercion! */
	    if (isout == true) {
		elem = document.getElementById('out' + id + '-pre');
	    } else {
		elem = document.getElementById('in' + id);
		if (elem.style.display === 'none') {
		    elem = document.getElementById('out' + id + '-pre');
		}
	    }
	    elem.scrollIntoView();
	};

	var ClearFields = function() {
	    while (1) {
		var fields = document.getElementsByClassName('field');
		if (fields.length > 0) {
		    fields[0].parentNode.removeChild(fields[0]);
		    continue;
		}
		break;
	    }
	};

	var Dump = function() {
	    var i;
	    var values = new Array();
	    var fields = document.getElementsByClassName('tin');
	    for (i = 0; i < fields.length; i++) {
		if (fields[i].parentNode.style.display !== 'none') {
 		    if (msLike) {
			values[i] = fields[i].value;
			fields[i].innerHTML = values[i];
		    } else {
			fields[i].innerHTML = fields[i].value;
		    }
		}
		fields[i].disabled = true;
	    }
	    window.external.invoke(document.body.innerHTML);
	    fields = document.getElementsByClassName('tin');
	    for (i = 0; i < fields.length; i++) {
		fields[i].disabled = false;
		if (fields[i].parentNode.style.display !== 'none') {
		    fields[i].innerHTML = "";
		    if (msLike) {
			fields[i].value = values[i];
		    }
		}
	    }
	};

	var Wcss = function(str) {
	    var t = document.createElement('style');
	    var d = document.head || document.getElementsByTagName('head')[0];
	    t.setAttribute('type', 'text/css');
	    t.styleSheet ?
	        t.styleSheet.cssText = str :
	        t.appendChild(document.createTextNode(str));
	    d.appendChild(t);
	};
	
	var Wjs = function(str) {
	    var t = document.createElement('script');
	    var d = document.head || document.getElementsByTagName('head')[0];
	    t.setAttribute('type', 'text/javascript');
	    t.appendChild(document.createTextNode(str));
	    d.appendChild(t);
	};

	/* Is this really needed? */
	window.addEventListener('unload', function(event) {
	    window.clearInterval(Gtimer);
	    Gtimer = null;
	});

	</script>
    }

    # Assemble pieces with empty BODY.
    set D $D_head
    append D $D_style $D_script "</head>\n" \
	"<body>\n<div id=\"top\" tabindex=\"0\"></div>\n</body>\n" \
        "</html>"

    # The URL to be displayed.
    # Windows:   data:text/html,<html><head><script>window.external.invoke("0 ::tsb::reload");</script></head></html>
    # MacOS:     data:text/html,<html><head><script>window.external.invoke("0 ::tsb::reload");</script></head></html>
    # WebkitGtk: data:text/html,<html><head><script>window.webkit.messageHandlers.extern.postMessage("0 ::tsb::reload");</script></head></html>

    variable U
    set U "data:text/html,%3Chtml%3E%3Chead%3E%3Cscript%3E"
    if {($tcl_platform(platform) eq "windows") ||
	($tcl_platform(os) eq "Darwin")} {
	append U "window.external.invoke%28%220%20::tsb::reload%22%29%3B"
    } else {
	append U "window.webkit.messageHandlers.external.postMessage%28%220%20::tsb::reload%22%29%3B"
    }
    append U "%3C%2Fscript%3E%3C%2Fhead%3E%3C%2Fhtml%3E"

    # The base title
    variable title "Taygete Scrap Book"

    # The current file
    variable file ""
}

# If file name given, load history array from it now, else
# fill the first history item with some links to examples.

set title $::tsb::title
if {[llength $argv] && [file readable [lindex $argv 0]]} {
    catch {
	apply {name {
	    set f [open $name r]
	    set data [read $f]
	    close $f
	    array set ::H $data
	    set ::tsb::file $name
	}} [lindex $argv 0]
    }
} else {
    apply {dir {
	set t ""
	set exdir [file join $dir examples]
	set logo [file join $dir taygete_logo.png]
	foreach n [lsort -dictionary [glob -nocomplain $exdir/*.tsb]] {
	    if {[string match "*\"'*" $n]} {
		continue
	    }
	    append t "<dt>\n <a href='javascript:void(0);'\n"
	    append t "    onclick='RunTcl(\"0 ::tsb::load [list $n]\");'>\n"
	    append t [file tail $n] "\n </a>\n</dt>\n"
	}
	if {$t ne ""} {
	    set h "#HTML\n"
	    if {![catch {open $logo rb} f]} {
		append h "<center>\n<img style='margin: 1em;'"
		append h " src='data:image/png;base64,\n"
		append h [binary encode base64 -maxlen 78 [read $f]]
		append h "'>\n</center>\n"
		close $f
	    }
	    append h "<h3>Welcome!</h3>\n"
	    append h "<dl>\n<dt>\n <a href='javascript:void(0);'\n"
	    append h "    onclick='RunTcl(\"0 ::tsb::clear\");'>\n"
	    append h "Start a new empty document.\n </a>\n</dt>\n</dl>\n"
	    append h "<h4>Example documents:</h4>\n<dl>\n"
	    set e "</dl>"
	    set ::H(1) ${h}${t}${e}
	}
    }} [file dirname [info script]]
}

# Initialize webview; document is loaded indirectly via the URL
# which triggers ::tsb::reload which does the rest, but see
# the ::tsb::ready block below.

set W [::twv::new -width 800 -height 600 -title $::tsb::title \
	   -url $::tsb::U -resizable 1 -debug 1 \
	   -callback ::tsb::call_from_js]

# On Windows this seems the way to load, maybe timing?

if {($tcl_platform(platform) eq "windows") && !$::tsb::ready} {
    after cancel ::tsb::reload0
    ::tsb::reload0
}

# Enter the webview event loop.

$W run

# Done.

exit 0
