ussg

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

ussg-page (22653B)


      1 #!/usr/bin/env tclsh8.6
      2 
      3 chan configure stderr -buffering line
      4 
      5 # proc until {test body} {uplevel 1 [list while [concat ! $test] $body]}
      6 
      7 proc opts {{consume {h v H: M: help version headers: markdown:}} argl} {
      8 	# will always output a list n=2.
      9 	# [lindex [opts ...] 0] = a list of lists: option, argument(s)
     10 	# [lindex [opts ...] 1] = a list of arguments not consumed
     11 	# loptpile contains the long options followed by the number of arguments they consume
     12 	# a : not immediately preceded by a backslash means that consumed++
     13 	# two backslashes collapse to one.
     14 	# soptpile contains the options which are one letter (one may write -HM header markdown)
     15 	set loptpile [list]
     16 	set soptpile [list]
     17 	foreach {option} $consume {
     18 		set optname ""
     19 		set colons 0
     20 		set prevl ""
     21 		foreach {l} [split $option {}] {
     22 			if {$l == ":" && $prevl != "\\"} {incr colons}
     23 			if {($l != "\\" || $prevl == "\\") && $colons < 1} {
     24 				append optname $l
     25 			}
     26 			if {$prevl != "\\"} {set prevl $l} {set prevl ""}
     27 		}
     28 		if {[string length $optname] > 1} {lappend loptpile $optname $colons} {lappend soptpile $optname $colons}
     29 	}
     30 	set consumingargs [list]
     31 	set consumedargs [list]
     32 	set unconsumedargs [list]
     33 	set n 0
     34 	set dashdash 0
     35 	while {$n < [llength $argl]} {
     36 		set arg [lindex $argl $n]
     37 		set colons 0
     38 		if {$dashdash} {
     39 			lappend unconsumedargs $arg
     40 		} {
     41 			if {[string index $arg 0] == "-"} {
     42 				if {[string index $arg 1] == "-"} {
     43 					if {[string length $arg] == 2} {set dashdash 1} {
     44 						# long opt; consume argument immediately after, even if it starts with a dash
     45 						# if not a real opt, don't care
     46 						if {[set colons [dict get $loptpile [string range $arg 2 end]]] != ""} {
     47 							set consumedarg [list [string range $arg 2 end]]
     48 							if {$colons != 0} {
     49 								if {[llength [lrange $argl [expr {$n+1}] [expr {$n+$colons}]]] == $colons} {
     50 									lappend consumedarg [lrange $argl [expr {$n+1}] [expr {$n+$colons}]]
     51 									incr n $colons
     52 								} {
     53 									return -code error "Reached end of arguments list while consuming arguments for option $arg ([lrange $argl [expr {$n+1}] [expr {$n+$colons}]])"
     54 								}
     55 							}
     56 							lappend consumedargs $consumedarg
     57 						} {
     58 							lappend unconsumedargs $arg
     59 						}
     60 					}
     61 				} {
     62 					if {[string length $arg] == 1} {lappend unconsumedargs $arg} {
     63 						# short option
     64 						foreach {letter} [split [string range $arg 1 end] {}] {
     65 							if {[catch {dict get $soptpile $letter} colons] == 0} {
     66 								#set colons [dict get $soptpile $letter]
     67 								set consumedarg [list $letter]
     68 								if {$colons != 0} {
     69 									if {[llength [lrange $argl [expr {$n+1}] [expr {$n+$colons}]]] == $colons} {
     70 										foreach {ar} [lrange $argl [expr {$n+1}] [expr {$n+$colons}]] {lappend consumedarg $ar}
     71 										incr n $colons
     72 									} {
     73 										return -code error "Reached end of arguments list while consuming arguments for option $arg ([lrange $argl [expr {$n+1}] [expr {$n+$colons}]])"
     74 									}
     75 								}
     76 								lappend consumedargs $consumedarg
     77 							} {
     78 								# unrecognized dash option - warn user and treat as a separate non-option arg
     79 								puts stderr [format "warning: option -%s not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a \'--\'!" $letter]
     80 								lappend unconsumedargs [join [list - $letter] {}]
     81 							}
     82 							# if there's even zero colons
     83 						}
     84 						# foreach letter
     85 					}
     86 					# if only dash, else short option
     87 				}
     88 				# if two dashes
     89 			} else {
     90 				lappend unconsumedargs $arg
     91 			}
     92 			# if one dash
     93 		}
     94 		# if dashdash
     95 		incr n
     96 	}
     97 	# while
     98 	return [list $consumedargs $unconsumedargs]
     99 }
    100 
    101 set version v0.0-alpha
    102 set programname "ussg-page page generator"
    103 
    104 proc printversion {} {
    105 	puts stderr [format "%s %s (Tcl)" $::programname $::version]
    106 	puts stderr {All Rights Reserved © 2022 by Ellenor Agnes Bjornsdottir
    107 Most activity in the nature of redistributing this application,
    108 unmodified or modified, is permitted.  See LICENCE for more info.
    109 
    110 This program is, to the extent permitted by applicable law, supplied
    111 with no warranty, not even the implied warranties for
    112 merchantability or fitness for purpose.  Where legally possible, my
    113 liability at any remaining warranty claims will be limited to the
    114 amount I received in return for you receiving this software.
    115 
    116 Exceptionally:
    117 For stable releases, I warrant that the software has been
    118 demonstrated to produce usable output on a common UNIX operating
    119 system with Tcl 8.6, and using Orc's Discount markdown engine,
    120 with the included input data. Your recovery is limited to damage
    121 caused by improper operation of the software, which you did not
    122 cause, which doesn't appear to be related to defective input
    123 or hardware. Bug reports are welcome and are preferred over
    124 legal process if possible.
    125 }
    126 }
    127 
    128 
    129 set help {usage: %0 [--..] [-hv] [-H headers] [-M /usr/bin/markdown] [--] input [output]
    130 
    131 Generate a complete HTML page from input and headers to output.
    132 headers and input are as described in interface.md and
    133 ussg-page.1.
    134 
    135 If both input and output are blank, convert stdin to stdout.
    136 
    137 If either input or output are single dashes, convert the respective
    138 standard file.
    139 
    140 A double dash terminates option processing, and all arguments
    141 are taken as non-options thereafter, including other double
    142 dashes.
    143 
    144 Options:
    145   -h, --help
    146       Display this help.
    147   -v, --version
    148       Displays the version of this program.
    149   -H/--headers [headers]
    150       headers file, read before input. input is treated as if
    151       it's catenated after headers.
    152       May be specified more than once.
    153   -M/--markdown [markdown]
    154       'Markdown' command line. Must accept Markdown on standard
    155       input and produce fragmentary HTML on standard output.
    156 
    157       Note: we do not check that you are actually using Markdown.
    158       If you are instead using, e.g. mdoc, this should work, and
    159       exactly this use is in place on Umbrellix.net and is
    160       supported.
    161       Default if not specified: env markdown
    162   -R/--restricted
    163       Restricted mode. Does not scribble on an already existing
    164       output file. Instead, balks with EX_CANTCREAT (73).
    165       This is not default, but it is recommended to use this if
    166       possible.
    167   -N/--noheaders
    168       The input file does not contain headers. It starts
    169       immediately with content.
    170   -Q/--quick
    171       Not applicable if either file is the standard descriptor.
    172       The mtimes of the input and output files are compared.
    173       Should the output file not exist, or should it be older
    174       than the input file, operate normally. Otherwise,
    175       terminate early.
    176 
    177 Specifying multiple short options together will result in them
    178 being interpreted separately, including dash options that
    179 consume arguments. Unknown dash options will be treated as
    180 non-option arguments; you will receive a warning on stderr
    181 if this occurs.
    182 
    183 Example (without unknown options):
    184 
    185   %0 -HM .headers "$HOME/.local/src/discount/markdown [options]"
    186       Run this program, stdin to stdout, with a headers
    187       sidestream of .headers and a Markdown command line
    188       of "$HOME/.local/src/discount/markdown [options]"
    189       (with dquotes, this'll be interpreted by your shell, then
    190       by Tcl in a usage of [open |...])
    191 
    192   %0 -HM .headers "$HOME/.local/src/discount/markdown [options]" -- -malchik -zhenschina
    193       Run this program, ./-malchik to ./-zhenschina, with a
    194       headers sidestream of .headers and a Markdown command
    195       line of "$HOME/.local/src/discount/markdown [options]"
    196       (with dquotes, this'll be interpreted by your shell, then
    197       by Tcl in a usage of [open |...])
    198       The etymology of the nonsense words used is Russian for
    199       boy and woman.
    200 
    201 Example (with unknown options):
    202 
    203   %0 -HMYZ .headers "$HOME/.local/src/discount/markdown [options]"
    204       Run this program, ./-Y to ./-Z, with a headers
    205       sidestream of .headers and a Markdown command line
    206       of "$HOME/.local/src/discount/markdown [options]"
    207 Note: That's probably not what you want, and the behavior will
    208       change if those options ever become recognized.
    209       Do not invoke this program this way except for a joke.
    210 }
    211 
    212 proc printhelp {} {
    213 	puts stderr [string map [list %0 $::argv0] $::help]
    214 }
    215 
    216 set inputfd stdin
    217 set outputfd stdout
    218 set inputfile ""
    219 set outputfile ""
    220 set headersfiles [list]
    221 set headersfds [list]
    222 set markdown "env markdown"
    223 set scribble 1
    224 set moving 0 ;# 1 means that we'll have to move data after.
    225 
    226 ## Begin command routine
    227 
    228 set headers [list]
    229 
    230 # 0 if it can only be set once, 1 if it can be set inf times
    231 # assume 1
    232 # Headers are case insensitive and stored in lowercase, so ...
    233 set headermulti {
    234 	title	0
    235 	title-separator	0
    236 	template	0
    237 	x-site-title	0
    238 	x-site-logo	0
    239 	x-site-description	0
    240 	x-synoptic-title	0
    241 	x-synoptic-text	0
    242 	x-synoptic-image	0
    243 	favicon	0
    244 	style	1
    245 	verbatim	1
    246 	execverbatim	1
    247 	plugin	1
    248 	url-prefix	0
    249 	navbar-prefix	0
    250 }
    251 
    252 set parsedargs [opts [list h v H: M: N Q help version headers: markdown: noheaders quick] $argv]
    253 # opts [list h v H: M: help version headers: markdown:] [list input --markdown /usr/bin/markdown output -HMEQI .headers /usr/bin/markdown -- -EQI -- -- --]
    254 # warning: option -E not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'!
    255 # warning: option -Q not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'!
    256 # warning: option -I not recognized by this program. Treating as a SEPARATE non-option argument - if this wasn't intended, put the argument containing this option after a '--'!
    257 # {{markdown /usr/bin/markdown} {H .headers} {M /usr/bin/markdown}} {input output -E -Q -I -EQI -- -- --}
    258 
    259 set firstblankline 0
    260 set quick 0
    261 
    262 lassign $parsedargs opts arg
    263 
    264 foreach {opt} $opts {
    265 	switch -exact -- [lindex $opt 0] {
    266 		h	-
    267 		help {
    268 			printhelp
    269 			exit 64
    270 		}
    271 
    272 		v	-
    273 		version {
    274 			printversion
    275 			exit 64
    276 		}
    277 
    278 		H	-
    279 		headers {
    280 			lappend ::headersfiles [file normalize [lindex $opt 1]]
    281 		}
    282 
    283 		M	-
    284 		markdown {
    285 			set ::markdown [lindex $opt 1]
    286 		}
    287 
    288 		Q	-
    289 		quick {
    290 			set ::quick 1
    291 		}
    292 
    293 		N	-
    294 		noheaders {
    295 			set ::firstblankline 1
    296 		}
    297 
    298 		R	-
    299 		restricted {
    300 			set ::scribble 0
    301 		}
    302 	}
    303 }
    304 
    305 proc picktmpfilename {filename} {
    306 	# blocks until filename does not exist
    307 	set ext [clock seconds]
    308 	while {[file exists [format "%s.%s" $filename $ext]]} {
    309 		incr ext
    310 	}
    311 	return [format "%s.%s" $filename $ext]
    312 }
    313 
    314 if {[llength $arg] == 2} {
    315 	set inputfile [lindex $arg 0]
    316 	set outputfile [lindex $arg 1]
    317 	if {$outputfile == "-"} {
    318 		set outputfile ""
    319 	} else {
    320 		# In the positive, output file desc is already set correctly.
    321 		# Alternatively...
    322 		if {!$::scribble && [file exists $outputfile]} {
    323 			puts stderr [format "Error: output file \'%s\' already exists. You asked me not to scribble on it, so I am not scribbling on it." $outputfile]
    324 			exit 73
    325 		}
    326 		set outputdir [file dirname $outputfile]
    327 		set outputmkdir [list file mkdir]
    328 		if {![file exists $outputdir]} {
    329 			while {![file exists $outputdir]} {
    330 				lappend outputmkdir $outputdir
    331 				set outputdir [file dirname $outputdir]
    332 			}
    333 		}
    334 		if {[catch $outputmkdir mkdirerr]} {
    335 			puts stderr [format "Error: directories to contain output file \'%s\' could not be created. \[file mkdir\] reports:" $::outputfile]
    336 			puts stderr $mkdirerr
    337 			exit 73
    338 		}
    339 		if {$::quick && $::inputfile != "-"} {
    340 			# spend time to save time
    341 			if {[file exists $::outputfile] &&
    342 			    [file exists $::inputfile]} {
    343 				set ::canexit 1
    344 				if {[set outputmtime [file mtime $::outputfile]] >= [set inputmtime [file mtime $::inputfile]]} {
    345 					puts stderr [format "Info: output file \'%s\' is newer (%s) than input file \'%s\' (%s)" $::outputfile $outputmtime $::inputfile $inputmtime]
    346 					foreach header $::headersfiles {
    347 						if {[set outputmtime [file mtime $::outputfile]] >= [set inputmtime [file mtime $header]]} {
    348 							puts stderr [format "Info: output file \'%s\' is newer (%s) than header file \'%s\' (%s)" $::outputfile $outputmtime $header $inputmtime]
    349 						} else {
    350 							puts stderr [format "Info: output file \'%s\' was modified %s, header file \'%s\' was modified (%s)" $::outputfile $outputmtime $header $inputmtime]
    351 							set ::canexit 0
    352 						}
    353 					}
    354 				} {
    355 					puts stderr [format "Info: output file \'%s\' was modified %s, input file \'%s\' was modified (%s)" $::outputfile $outputmtime $::inputfile $inputmtime]
    356 					set ::canexit 0
    357 				}
    358 			} {
    359 				puts stderr [format "Info: only one of the two files exists"]
    360 				set ::canexit 0
    361 			}
    362 			if {$::canexit} {
    363 				puts stderr [format "Info: output file \'%s\' is considered up to date; exiting." $::outputfile]; exit
    364 			} else {
    365 				puts stderr [format "Info: output file \'%s\' is considered out of date; continuing." $::outputfile]
    366 			}
    367 		} {
    368 			puts stderr [format "Info: quick mode disabled, or input file == -"]
    369 		}
    370 		if {[catch {open [set ::tmpoutputfile [picktmpfilename [set outputfile [file normalize $outputfile]]]] w} provoutputfd]} {
    371 			puts stderr [format "Error: temporary output file \'%s\' could not be opened for writing. \[open\] reports:" $::tmpoutputfile]
    372 			puts stderr $provoutputfd
    373 			exit 73
    374 		} {
    375 			set ::outputfd $provoutputfd
    376 			set ::moving 1
    377 		}
    378 	}
    379 }
    380 #continue
    381 if {[llength $arg] > 0} {
    382 	set inputfile [lindex $arg 0]
    383 	if {$inputfile == "-"} {
    384 		set outputfile ""
    385 	} else {
    386 		# In the positive, the input file desc is already set correctly.
    387 		if {[catch {open [file normalize $inputfile] r} provinputfd]} {
    388 			puts stderr [format "Error: input file \'%s\' could not be opened for reading. \[open\] reports:" $::inputfile]
    389 			puts stderr $provinputfd
    390 			exit 66
    391 		} {
    392 			set ::inputfd $provinputfd
    393 		}
    394 	}
    395 }
    396 foreach headersfile $::headersfiles {
    397 	if {[catch {open [file normalize $headersfile] r} provinputfd]} {
    398 		puts stderr [format "Error: headers file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $::headersfile]
    399 		puts stderr $provinputfd
    400 		exit 66
    401 	} {
    402 		lappend ::headersfds $provinputfd
    403 	}
    404 }
    405 
    406 proc parseheader {lin} {
    407 	set content [string range [join [lassign [split $lin ":"] word] ":"] 1 end]
    408 	set word [string tolower $word]
    409 	puts stderr [format "info: found header %s: %s" $word $content]
    410 	if {[catch {dict get $::headermulti $word} multi] == 0} {
    411 		if {$multi} {
    412 			dict lappend ::headers $word $content
    413 		} {
    414 			dict set ::headers $word $content
    415 			# We will just allow overriding.
    416 		}
    417 	} {
    418 		# Assume 1
    419 		dict lappend ::headers $word $content
    420 	}
    421 }
    422 
    423 foreach headersfd $::headersfds {
    424 	while {![eof $headersfd]} {
    425 		gets $headersfd lin
    426 		if {$lin != ""} {parseheader $lin}
    427 	}
    428 	# we are done, we can close the header now.
    429 	close $headersfd
    430 }
    431 
    432 #firstblankline was set earlier
    433 # if it's already 1 (user specified -N) we skip this.
    434 
    435 while {![eof $inputfd] && !$firstblankline} {
    436 	gets $inputfd lin
    437 	if {$lin == ""} {
    438 		set firstblankline 1
    439 		puts stderr "info: blank line found. Begin processing document as document."
    440 	} {
    441 		parseheader $lin
    442 	}
    443 }
    444 
    445 if {[eof $inputfd]} {
    446 	puts stderr "Error: document file ended without a document. As this would produce an empty document, this is not allowed."
    447 	exit 66
    448 }
    449 # Hold up. Stop accepting lines from inputfd.
    450 
    451 # By this stage, we must have a Template: header.
    452 # In the template, %%article%% must be on a line by itself as it's expected to be quite large, so unsuitable for [string map].
    453 if {[catch {dict get $::headers template} templatehdr]} {
    454 	puts stderr $::headers
    455 	puts stderr [format "Error: template file not specified in headers or document file. Without a template, we cannot create a document."]
    456 	exit 78
    457 } {
    458 	if {[catch {open [file normalize $templatehdr] r} provinputfd]} {
    459 		puts stderr [format "Error: template file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $templatehdr]
    460 		puts stderr $provinputfd
    461 		exit 66
    462 	} {
    463 		set templfd $provinputfd
    464 	}
    465 }
    466 
    467 proc templcmdsrc {script} {
    468 	namespacesrc ::templcmds $script
    469 }
    470 
    471 proc namespacesrc {namespace script} {
    472 	if {[catch {open [file normalize $script] r} provinputfd]} {
    473 		puts stderr [format "Error: namespace \'%s\' file \'%s\' could not be opened for reading. Balking now; this is fatal. \[open\] reports:" $namespace $script]
    474 		puts stderr $provinputfd
    475 		exit 66
    476 	} {
    477 		set fp $provinputfd
    478 	}
    479 	#set fp [open $script r]
    480 	set ev [list namespace eval $namespace [read $fp]]
    481 	close $fp
    482 	uplevel "#0" $ev
    483 }
    484 
    485 # proc markdown: fd inputfd
    486 # attributes: might block
    487 # globals: markdown outputfd
    488 # processfd markdown inputfd outputfd
    489 # side effects: closes inputfd
    490 proc markdown {inputfd} {
    491 	processfd $::markdown $inputfd $::outputfd
    492 	chan close $inputfd
    493 }
    494 
    495 # proc processfd: string markdown, fd inputfd, fd outputfd
    496 # attributes: might block
    497 # process the remainder of inputfd, blocking if no data is available,
    498 # with the program markdown (which must be a suitable Unix filter),
    499 # outputting to outputfd
    500 # leaves inputfd at EOF
    501 proc processfd {markdown inputfd outputfd} {
    502 	# finally, our raison d'etre !
    503 	# we expect to get eof on input.
    504 	if {[catch {open [format "|%s" $markdown] r+} err]} {
    505 		puts stderr [format "Error: processor \'%s\' could not be executed for reading and writing. \[open\] reports:" $markdown]
    506 		puts stderr $err
    507 		exit 70
    508 	} {
    509 		set mkdownfd $err
    510 	}
    511 	chan copy $inputfd $mkdownfd
    512 	chan flush $mkdownfd
    513 	set result [catch {chan close $mkdownfd write} return]
    514 	# begone, input document
    515 	chan copy $mkdownfd $outputfd
    516 	chan flush $outputfd
    517 	set rresult [catch {chan close $mkdownfd read} rreturn]
    518 	# begone, markdown
    519 	if {$result != 0} {puts stderr [format "Warning: On close write, processor reported %s" $return]}
    520 	if {$rresult != 0} {puts stderr [format "Warning: On close read, processor reported %s" $rreturn]}
    521 }
    522 
    523 namespace eval ::templcmds {
    524 	proc title {} {
    525 		# output page title
    526 		if {[catch {dict get $::headers title} title]} {
    527 			if {![catch {dict get $::headers navbar-prefix} navprefix]} {set prefix $navprefix} {set prefix ""}
    528 			if {![catch {dict get $::headers x-site-title} navprefix]} {set title $navprefix} {set title "Site Title Not Set"}
    529 			if {![catch {dict get $::headers title-separator} sep]} {set sep $sep} {set sep "::"}
    530 			set filename [string map [list [file normalize $prefix] ""] $::outputfile]
    531 			append title [format " %s " $sep]
    532 			append title [string map [list "<" "&lt;" ">" "&gt;" [file separator] [format " %s " $sep]] [string trimleft $filename [file separator]]]
    533 		}
    534 		puts $::outputfd [format "<title>%s</title>" $title]
    535 	}
    536 
    537 	proc xsitelogo {} {
    538 		# output Site Logo code
    539 		if {![catch {dict get $::headers x-site-logo} title]} { puts -nonewline $::outputfd $title }
    540 	}
    541 
    542 	proc xsitetitle {} {
    543 		# output Site Title
    544 		if {![catch {dict get $::headers x-site-title} title]} { puts -nonewline $::outputfd $title }
    545 	}
    546 
    547 	proc xsitedescription {} {
    548 		if {![catch {dict get $::headers x-site-description} title]} { puts -nonewline $::outputfd $title }
    549 	}
    550 
    551 	proc heads {} {
    552 		# here we check for synoptic headers, and output them as appropriate
    553 		# e.g. <link rel="stylesheet" href="/pub/style/style.css" type="text/css" media="screen, handheld" title="default">
    554 		if {![catch {dict get $::headers x-synoptic-title} title]} {
    555 			puts $::outputfd [format "<meta property=\"og:title\" content=\"%s\" />" $title]
    556 		}
    557 		if {![catch {dict get $::headers x-synoptic-text} title]} {
    558 			puts $::outputfd [format "<meta property=\"og:description\" content=\"%s\" />" $title]
    559 		}
    560 		if {![catch {dict get $::headers x-synoptic-sitename} title]} {
    561 			puts $::outputfd [format "<meta property=\"og:site_name\" content=\"%s\" />" $title]
    562 		}
    563 		if {![catch {dict get $::headers x-synoptic-image} title]} {
    564 			puts $::outputfd [format "<meta property=\"og:image\" content=\"%s\" />" $title]
    565 		}
    566 		if {![catch {dict get $::headers x-synoptic-url} title]} {
    567 			puts $::outputfd [format "<meta property=\"og:url\" content=\"%s\" />" $title]
    568 		}
    569 		if {![catch {dict get $::headers favicon} title]} {
    570 			puts $::outputfd [format "<link rel=\"shortcut icon\" href=\"/%s\" type=\"image/vnd.microsoft.icon\" />" $title]
    571 		}
    572 		if {![catch {dict get $::headers style} title]} {
    573 			puts $::outputfd [format "<link rel=\"stylesheet\" href=\"/%s\" type=\"text/css\" />" $title]
    574 		}
    575 		if {![catch {dict get $::headers raw-head} rawheadhdr]} {
    576 			foreach {rawhead} $rawheadhdr {
    577 				puts $::outputfd $rawhead
    578 			}
    579 		}
    580 	}
    581 
    582 	proc article {} {
    583 		markdown $::inputfd
    584 	}
    585 
    586 	proc verbatim {num} {
    587 		set verbatimhdr [lindex [dict get $::headers verbatim] $num]
    588 		if {![file exists $verbatimhdr]} {
    589 			puts stderr [format "Warning: verbatim file no. %s \'%s\' does not exist." $num $verbatimhdr]
    590 			return
    591 		}
    592 		if {[catch {open [file normalize $verbatimhdr] r} provinputfd]} {
    593 			puts stderr [format "Warning: verbatim file no. %s \'%s\' could not be opened for reading. \[open\] reports:" $num $verbatimhdr]
    594 			puts stderr $provinputfd
    595 			return
    596 		} {
    597 			chan copy $provinputfd $::outputfd
    598 			chan flush $::outputfd
    599 			close $provinputfd
    600 		}
    601 	}
    602 
    603 	namespace export *
    604 	namespace ensemble create
    605 }
    606 
    607 if {![catch {dict get $::headers plugin} pluginhdr]} {
    608 	foreach {script} $pluginhdr {
    609 		templcmdsrc $script
    610 	}
    611 }
    612 
    613 proc templcmd {command} {
    614 	if {[string is entier [string trimleft $command "%"]]} {
    615 		templcmds verbatim [string trimleft $command "%"]
    616 	} {
    617 		templcmds [string trimleft $command "%"]
    618 	}
    619 	chan flush $::outputfd
    620 }
    621 
    622 # We expect the template to be small enough that reading it, in full, into memory, will not be a problem even on the smallest system we expect to work on.
    623 set template [list]
    624 while {![eof $templfd]} {
    625 	lappend template [gets $templfd]
    626 }
    627 # We're done, we can close the template now
    628 close $templfd
    629 foreach {tplline} $template {
    630 	if {[string first "%" [string trimleft $tplline " \t"]] == 0} {
    631 		# special
    632 		# a single percentage sign, with only tabs or spaces before it, is a template command.
    633 		# if it's "article", then print the article. if it's a number, print that number of verbatim (or nothing if it doesn't exist)
    634 		set tplcmd [string trimleft $tplline " \t"]
    635 		templcmd $tplcmd
    636 	} {
    637 		# just print to $::outputfd
    638 		puts $::outputfd $tplline
    639 	}
    640 }
    641 chan flush $::outputfd
    642 chan close $::outputfd
    643 
    644 # Out of the loop: we're getting there...
    645 # Moving? If not, we're probably done.
    646 if {${moving}} {
    647 	puts stderr [format "Info: Moving temporary file \'%s\' to its permanent location, \'%s\'" $::tmpoutputfile $::outputfile]
    648 	if {[catch {file rename -force $::tmpoutputfile $::outputfile} err]} {
    649 		puts stderr [format "Error: While moving temporary file \'%s\' to its permanent location, \'%s\', \[file rename\] reported:" $::tmpoutputfile $::outputfile]
    650 		puts stderr $err
    651 		exit 74
    652 	}
    653 }