1#! /bin/sh
2# -*- tcl -*- \
3exec tclsh "$0" ${1+"$@"}
4
5# @@ Meta Begin
6# Application dtplite 1.0
7# Meta platform     tcl
8# Meta summary      Lightweight DocTools Processor
9# Meta description  This application is a simple processor
10# Meta description  for documents written in the doctools
11# Meta description  markup language. It covers the most
12# Meta description  common use cases, but is not as
13# Meta description  configurable as its big brother dtp.
14# Meta category     Processing doctools documents
15# Meta subject      doctools doctoc docidx
16# Meta require      {doctools 1}
17# Meta require      {doctools::idx 1}
18# Meta require      {doctools::toc 1}
19# Meta require      fileutil
20# Meta require      textutil::repeat
21# Meta author       Andreas Kupries
22# Meta license      BSD
23# @@ Meta End
24
25package provide dtplite 1.0.2
26
27# dtp lite - Lightweight DocTools Processor
28# ======== = ==============================
29#
30# Use cases
31# ---------
32# 
33# (1)	Validation of a single manpage, i.e. checking that it is valid
34#	doctools format.
35# 
36# (1a)	Getting a preliminary version of the formatted output, for
37#	display in a browser, nroff, etc., proofreading the
38#	formatting.
39# 
40# (2)	Generate documentation for a single package, i.e. all the
41#	manpages, plus index and table of contents.
42# 
43# (3)	Generation of unified documentation for several
44#	packages. Especially unified keyword index and table of
45#	contents. This may additionally generate per-package TOCs
46#	as well (Per-package indices don't make sense IMHO).
47# 
48# Command syntax
49# --------------
50# 
51# Ad 1)	dtplite -o output format file
52# 
53#	The option -o specifies where to write the output to. Using
54#	the string "-" as name of the output file causes the tool to
55#	write the generated data to stdout. If $output is a directory
56#	then a file named [[file rootname $file].$format] is written
57#	to the directory.
58
59# Ad 1a)	dtplite validate file
60#
61#	The "validate" format does not generate output at all, only
62#	syntax checking is performed.
63# 
64# Ad 2)	dtplite -o output format directory
65# 
66#	I.e. we distinguish (2) from (1) by the type of the input,
67#	file, or directory. In this situation output has to be a
68#	directory. Use the path "." to place the results into the
69#	current directory.
70# 
71#	We locate _all_ files under directory, i.e. all subdirectories
72#	are scanned as well. We replicate the found directory
73#	structure in the output (See example below). The index and
74#	table of contents are written to the toplevel directory in the
75#	output. The names are hardwired to "toc.$format" and
76#	"index.$format".
77# 
78# Ad 3)	dtplite -merge -o output format directory
79# 
80#	This can be treated as special case of (2). The -merge option
81#	tells it that the output is nested one level deeper, to keep a
82#	global toc and index in the toplevel and to merge the package
83#	toc and index into them.
84# 
85#	This way the global documents are built up incrementally. This
86#	can help us in a future extended installer as well!, extending
87#	a global documentation tree of all installed packages.
88# 
89# Additional features.
90# 
91# *	As described above the format name is used as the extension
92#	for the generated files. Does it make sense to introduce an
93#	option with which we can overide this, or should we simply
94#	extect that a calling script does a proper renaming of all the
95#	files ?  ... The option is better. In HTML output we have
96#	links between the files, and renaming from the outside just
97#	breaks the links. This option is '-ext'. It is ignored if the
98#	output is a single file (fully specified via -o), or stdout.
99# 
100#	-ext extension
101# 
102# *	Most of the formats don't need much/none of customizability.
103#	I.e. text, nroff, wiki, tmml, ...  For HTML however some
104#	degree of customizability is required for good output.  What
105#	should we given to the user ?
106# 
107#	- Allow setting of a stylesheet.
108#	- Allow integration of custom body header and footer html.
109#	- Allow additional links for the navigation bar.
110# 
111#	Note: The tool generates standard navigation bars to link the
112#	all tocs, indices, and pages together.
113# 
114#	-style file
115#	-header file
116#	-footer file
117#	-nav label url
118# 
119# That should be enough to allow the creation of good looking formatted
120# documentation without getting overly complex in both implementation
121# and use.
122
123package require doctools      1.4.7 ; # 'image' support 
124package require doctools::idx 1.0.4 ;
125package require doctools::toc 1.1.3 ;
126package require fileutil
127package require textutil::repeat
128
129# ### ### ### ######### ######### #########
130## Internal data and status
131
132namespace eval ::dtplite {
133
134    # Path to where the output goes to. This is a file in case of mode
135    # 'file', irrelevant for mode 'file.stdout', and the directory for
136    # all the generated files for the two directory modes. Specified
137    # through the mandatory option '-o'.
138
139    variable  output ""
140
141    # Path to where the documents to convert come from. This is a
142    # single file in the case of the two file modes, and a directory
143    # for the directory modes. In the later case all files under that
144    # directory are significant, including links, if identifiable as
145    # in doctools format (fileutil::fileType). Specified through the
146    # last argument on the command line. The relative path of a file
147    # under 'input' also becomes its relative path under 'output'.
148
149    variable  input  ""
150
151    # The extension to use for the generated files. Ignored by the
152    # file modes, as for them they either don't generate a file, or
153    # know its full name already, i.e. including any wanted
154    # extension. Set via option '-ext'. Defaults to the format name if
155    # '-ext' was not used.
156
157    variable  ext    ""
158
159    # Optional. HTML specific, requires engine parameter 'meta'. Path
160    # to a stylesheet file to use in the output. The file modes link
161    # to it using the original location, but the directory modes copy
162    # the file into the 'output' and link it there (to make the
163    # 'output' more selfcontained). Initially set via option '-style'.
164
165    variable  style  ""
166
167    # Optional. Path to a file. Contents of the file are assigned to
168    # engine parameter 'header', if present. If navigation buttons
169    # were defined their HTML will be appended to the file contents
170    # before doing the assignment. A specification is ignored if the
171    # engine does not support the parameter 'header'. Set via option
172    # '-header'.
173
174    variable  header ""
175
176    # Like header, but for the document footer, and no navigation bar
177    # insert. Set via option '-footer', requires engine parameter
178    # 'footer'.
179
180    variable  footer ""
181
182    # List of buttons/links for a navigation bar. No navigation bar is
183    # created if this is empty. HTML specific, requires engine
184    # parameter 'header' (The navigation bar is merged with the
185    # 'header' data, see above). Each element of the list is a
186    # 2-element list, containing the button label and url, in this
187    # order. Initial data comes from the command line, via option
188    # '-nav'. The commands 'Navbutton(Push|Pop)' then allow the
189    # programmatic addition and removal of buttons at the left (stack
190    # like, top at index 0). This is used for the insertion of links
191    # to TOC and Index into each document, if applicable.
192
193    variable  nav    {}
194
195    # An array caching the result of merging header and navbar data,
196    # keyed by the navbar definition (list). This allows us to quickly
197    # access the complete header for a navbar, without having to
198    # generate it over and over again. Its usefulness is a bit limited
199    # by the fact that the navbar itself can be generated on a
200    # file-by-file basis (required to get the relative links
201    # correct. It helps only if the generated navbars are identical to
202    # each other.
203
204    variable  navcache
205    array set navcache {}
206
207    # The name of the format to convert the doctools documents
208    # into. Set via the next-to-last argument on the command
209    # line. Used as extension for the generated files as well by the
210    # directory modes, and if not overridden via '-ext'. See 'ext'
211    # above.
212
213    variable  format ""
214
215    # Boolean flag. Set by the option '-merge'. Ignored when a file
216    # mode is detected, but for a directory it determines the
217    # difference between the two directory modes, i.e. plain
218    # generation, or incremental merging of many inputs into one
219    # output.
220
221    variable  merge  0
222
223    # Boolean flag. Automatically set by code distinguishing between
224    # file and directory modes. Set for a the file modes, unset for
225    # the directory modes.
226
227    variable  single 1
228
229    # Boolean flag. Automatically set by the code processing the '-o'
230    # option. Set if output is '-', unset otherwise. Ignored for the
231    # directory modes. Distinguished between the two file modes, i.e.
232    # writing to a file (unset), or stdout (set).
233
234    variable  stdout 0
235
236    # Name of the found processing mode. Derived from the values of
237    # the three boolean flags (merge, single, stdout). This value is
238    # used during the dispatch to the command implementing the mode,
239    # after processing the command line.
240    #
241    # Possible/Legal values:	Meaning
242    # ---------------------	-------
243    # File			File mode. Write result to a file.
244    # File.Stdout		File mode. Write result to stdout.
245    # Directory			Directory mode. Plain processing of one set.
246    # Directory.Merge		Directory mode. Merging of multiple sets into
247    #				one output.
248    # ---------------------	-------
249
250    variable  mode   ""
251
252    # Name of the module currently processed. Derived from the 'input'
253    # (last element of this path, without extension).
254
255    variable  module ""
256
257    # Crossreference data. Extracted from the processed documents, a
258    # rearrangement and filtration of the full meta data (See 'meta'
259    # below). Relevant only to the directory modes. I.e. the file
260    # modes don't bother with its extraction and use.
261
262    variable  xref
263    array set xref   {}
264
265    # Index data. Mapping from keyword (label) to the name of its
266    # anchor in the index output. Requires support for the engine
267    # parameter 'kwid' in the index engine.
268
269    variable  kwid
270    array set kwid {}
271
272    # Cache. This array maps from the path of an input file/document
273    # (relative to 'input'), to the paths of the file to generate
274    # (relative to 'output', including extension and such). In other
275    # words we derive the output paths from the inputs only once and
276    # then simply get them here.
277
278    variable  out
279    array set out  {}
280
281    # Meta data cache. Stores the meta data extracted from the input
282    # files/documents, per input. The meta data is a dictionary and
283    # processed several ways to get: Crossreferences (See 'xref'
284    # above), Table Of Contents, and Keyword Index. The last two are
285    # not cached, but ephemeral.
286
287    variable  meta
288    array set meta {}
289
290    # Cache of input documents. When we read an input file we store
291    # its contents here, keyed by path (relative to 'input') so that
292    # we don't have to go to the disk when we we need the file again.
293    # The directory modes need each input twice, for metadata
294    # extraction, and the actual conversion.
295
296    variable  data
297    array set data {}
298
299    # Database of image files for use by dt_imap.
300
301    variable  imap
302    array set imap {}
303}
304
305# ### ### ### ######### ######### #########
306## External data and status
307#
308## Only the directory merge mode uses external data, saving the
309## internal representations of current toc, index. and xref
310## information for use by future mergers. It uses three files,
311## described below. The files are created if they don't exist.
312## Remove them when the merging is complete.
313#
314## .toc
315## Contains the current full toc in form of a dictionary.
316#  Keys are division labels, values the lists of toc items.
317#
318## .idx
319## Contains the current full index, plus keyword id map.  Is a list of
320#  three elements, index, start id for new kwid entries, and the
321#  keyword id map (kwid). Index and Kwid are both dictionaries, keyed
322#  by keywords. Index value is a list of 2-tuples containing symbolic
323#  file plus label, in this order. Kwid value is the id of the anchor
324#  for that keyword in the index.
325#
326## .xrf
327## Contains the current cross reference database, a dictionary. Keys
328#  are tags the formatter can search for (keywords, keywrds with
329#  prefixes, keywords with suffixces), values a list containing either
330#  the file to refer to to, or both file and an anchor in that
331#  file. The latter is for references into the index.
332
333# ### ### ### ######### ######### #########
334## Option processing.
335## Validate command line.
336## Full command line syntax.
337##
338# dtplite	-o outputpath	\
339#		?-merge?	\
340#		?-ext ext?	\
341#		?-style file?	\
342#		?-header file?	\
343#		?-footer file?	\
344#		?-nav label url?... \
345#		format inputpath
346##
347
348proc ::dtplite::processCmdline {} {
349    global argv
350
351    variable output ; variable style  ; variable stdout
352    variable format ; variable header ; variable single
353    variable input  ; variable footer ; variable mode
354    variable ext    ; variable nav    ; variable merge  
355    variable module
356
357    # Process the options, perform basic validation.
358
359    while {[llength $argv]} {
360	set opt [lindex $argv 0]
361	if {![string match "-*" $opt]} break
362
363	if {[string equal $opt "-o"]} {
364	    if {[llength $argv] < 2} Usage
365	    set output [lindex $argv 1]
366	    set argv   [lrange $argv 2 end]
367	} elseif {[string equal $opt "-merge"]} {
368	    set merge 1
369	    set argv [lrange $argv 1 end]
370	} elseif {[string equal $opt "-ext"]} {
371	    if {[llength $argv] < 2} Usage
372	    set ext  [lindex $argv 1]
373	    set argv [lrange $argv 2 end]
374	} elseif {[string equal $opt "-style"]} {
375	    if {[llength $argv] < 2} Usage
376	    set style [lindex $argv 1]
377	    set argv  [lrange $argv 2 end]
378	} elseif {[string equal $opt "-header"]} {
379	    if {[llength $argv] < 2} Usage
380	    set header [lindex $argv 1]
381	    set argv   [lrange $argv 2 end]
382	} elseif {[string equal $opt "-footer"]} {
383	    if {[llength $argv] < 2} Usage
384	    set footer [lindex $argv 1]
385	    set argv   [lrange $argv 2 end]
386	} elseif {[string equal $opt "-nav"]} {
387	    if {[llength $argv] < 3} Usage
388	    lappend nav [lrange $argv 1 2]
389	    set argv    [lrange $argv 3 end]
390	} else {
391	    Usage
392	}
393    }
394
395    # Additional validation, and extraction of the non-option
396    # arguments.
397
398    if {[llength $argv] != 2} Usage
399
400    set format [lindex $argv 0]
401    set input  [lindex $argv 1]
402
403    if {[string equal $format validate]} {
404	set format null
405    }
406
407    # Final validation across the whole configuration.
408
409    if {[string equal $format ""]} {
410	ArgError "Illegal empty format specification"
411
412    } else {
413	# Early check: Is the chosen format ok ? For this we have
414	# create and configure a doctools object.
415
416	doctools::new dt
417	if {[catch {dt configure -format $format}]} {
418	    ArgError "Unknown format \"$format\""
419	}
420	dt configure -deprecated 1
421
422	# Check style, header, and footer options, if present.
423
424	CheckInsert header {Header file}
425	CheckInsert footer {Footer file}
426
427	if {[llength $nav] && ![in [dt parameters] header]} {
428	    ArgError "-nav not supported by format \"$format\""
429	}
430	if {![string equal $style ""]} {
431	    if {![in [dt parameters] meta]} {
432		ArgError "-style not supported by format \"$format\""
433	    } elseif {![file exists $style]} {
434		ArgError "Unable to find style file \"$style\""
435	    }
436	}
437    }
438
439    # Set up an extension based on the format, if no extension was
440    # specified.  also compute the name of the module, based on the
441    # input. [SF Tcllib Bug 1111364]. Has to come before the line
442    # marked with a [*], or a filename without extension is created.
443
444    if {[string equal $ext ""]} {
445	set ext $format
446    }
447
448    CheckInput $input {Input path}
449    if {[file isfile $input]} {
450	# Input file. Merge mode is not possible. Output can be file
451	# or directory, or "-" for stdout. The output may exist, but
452	# does not have to. The directory it is in however does have
453	# to exist, and has to be writable (if the output does not
454	# exist yet). An existing output has to be writable.
455
456	if {$merge} {
457	    ArgError "-merge illegal when processing a single input file."
458	}
459	if {![string equal $output "-"]} {
460	    CheckTheOutput
461
462	    # If the output is an existing directory then we have to
463	    # ensure that the actual output is a file in that
464	    # directory, and we derive its name from the name of the
465	    # input file (and -ext, if present).
466
467	    if {[file isdirectory $output]} {
468		# [*] [SF Tcllib Bug 1111364]
469		set output [file join $output [file tail [Output $input]]]
470	    }
471	} else {
472	    set stdout 1
473	}
474    } else {
475	# Input directory. Merge mode is possible. Output has to be a
476	# directory. The output may exist, but does not have to. The
477	# directory it is in however does have to exist. An existing
478	# output has to be writable.
479
480	set single 0
481	CheckTheOutput 1
482    }
483
484    # Determine the operation mode from the flags
485
486    if {$single} {
487	if {$stdout} {
488	    set mode File.Stdout
489	} else {
490	    set mode File
491	}
492    } elseif {$merge} {
493	set mode Directory.Merge
494    } else {
495	set mode Directory
496    }
497
498    set module [file rootname [file tail [file normalize $input]]]
499    return
500}
501
502# ### ### ### ######### ######### #########
503## Option processing.
504## Helpers: Generation of error messages.
505## I.  General usage/help message.
506## II. Specific messages.
507#
508# Both write their messages to stderr and then
509# exit the application with status 1.
510##
511
512proc ::dtplite::Usage {} {
513    global argv0
514    puts stderr "$argv0 wrong#args, expected:\
515	    -o outputpath ?-merge? ?-ext ext?\
516	    ?-style file? ?-header file?\
517	    ?-footer file? ?-nav label url?...\
518	    format inputpath"
519    exit 1
520}
521
522proc ::dtplite::ArgError {text} {
523    global argv0
524    puts stderr "$argv0: $text"
525    exit 1
526}
527
528proc in {list item} {
529    expr {([lsearch -exact $list $item] >= 0)}
530}
531
532# ### ### ### ######### ######### #########
533## Helper commands. File paths.
534## Conversion of relative paths
535## to absolute ones for input
536## and output. Derivation of
537## output file name from input.
538
539proc ::dtplite::Pick {f} {
540    variable input
541    return [file join $input $f]
542}
543
544proc ::dtplite::Output {f} {
545    variable ext
546    return [file rootname $f].$ext
547}
548
549proc ::dtplite::At {f} {
550    variable output
551    set of     [file normalize [file join $output $f]]
552    file mkdir [file dirname $of]
553    return $of
554}
555
556# ### ### ### ######### ######### #########
557## Check existence and permissions of an input/output file or
558## directory.
559
560proc ::dtplite::CheckInput {f label} {
561    if {![file exists $f]} {
562	ArgError "Unable to find $label \"$f\""
563    } elseif {![file readable $f]} {
564	ArgError "$label \"$f\" not readable (permission denied)"
565    }
566    return
567}
568
569proc ::dtplite::CheckTheOutput {{needdir 0}} {
570    variable output
571    variable format
572
573    if {[string equal $format null]} {
574	# The format does not generate output, so not specifying an
575	# output file is ok for that case.
576	return
577    }
578
579    if {[string equal $output ""]} {
580	ArgError "No output path specified"
581    }
582
583    set base [file dirname $output]
584    if {[string equal $base ""]} {set base [pwd]}
585
586    if {![file exists $output]} {
587	if {![file exists $base]} {
588	    ArgError "Output base path \"$base\" not found"
589	}
590	if {![file writable $base]} {
591	    ArgError "Output base path \"$base\" not writable (permission denied)"
592	}
593    } else {
594	if {![file writable $output]} {
595	    ArgError "Output path \"$output\" not writable (permission denied)"
596	}
597	if {$needdir && ![file isdirectory $output]} {
598	    ArgError "Output path \"$output\" not a directory"
599	}
600    }
601    return
602}
603
604proc ::dtplite::CheckInsert {option label} {
605    variable format
606    variable $option
607    upvar 0  $option opt
608
609    if {![string equal $opt ""]} {
610	if {![in [dt parameters] $option]} {
611	    ArgError "-$option not supported by format \"$format\""
612	}
613	CheckInput $opt $label
614	set opt [Get $opt]
615    }
616    return
617}
618
619# ### ### ### ######### ######### #########
620## Helper commands. File reading and writing.
621
622proc ::dtplite::Get {f} {
623    variable data
624    if {[info exists data($f)]} {return $data($f)}
625    return [set data($f) [fileutil::cat $f]]
626}
627
628proc ::dtplite::Write {f data} {
629    # An empty filename is acceptable, the format will be 'null'
630    if {[string equal $f ""]} return
631    fileutil::writeFile $f $data
632    return
633}
634
635# ### ### ### ######### ######### #########
636## Dump accumulated warnings.
637
638proc ::dtplite::Warnings {} {
639    set warnings [dt warnings]
640    if {[llength $warnings] > 0} {
641	puts stderr [join $warnings \n]
642    }
643    return
644}
645
646# ### ### ### ######### ######### #########
647## Configuation phase, validate command line.
648
649::dtplite::processCmdline
650
651# ### ### ### ######### ######### #########
652## We can assume that we have from here on a command 'dt', which is a
653## doctools object command, and already configured for the format to
654## generate.
655# ### ### ### ######### ######### #########
656
657# ### ### ### ######### ######### #########
658## Commands implementing the main functionality.
659
660proc ::dtplite::Do.File {} {
661    # Process a single input file, write the result to a single outut file.
662
663    variable input
664    variable output
665
666    SinglePrep
667    Write $output [dt format [Get $input]]
668    Warnings
669    return
670}
671
672proc ::dtplite::Do.File.Stdout {} {
673    # Process a single input file, write the result to stdout.
674
675    variable input
676
677    SinglePrep
678    puts  stdout [dt format [Get $input]]
679    close stdout
680    Warnings
681    return
682}
683
684proc ::dtplite::Do.Directory {} {
685    # Process a directory of input files, through all subdirectories.
686    # Generate index and toc, but no merging with an existing index
687    # and toc. I.e. any existing index and toc files are overwritten.
688
689    variable input
690    variable out
691    variable module
692    variable meta
693    variable format
694
695    # Phase 0. Find the documents to convert.
696    # Phase I. Collect meta data, and compute the map from input to
697    # ........ output files. This is also the map for the symbolic
698    # ........ references. We extend an existing map (required for use
699    # ........ in merge op.
700    # Phase II. Build index and toc information from the meta data.
701    # Phase III. Convert each file, using index, toc and meta
702    # .......... information.
703
704    MapImages
705    set files [LocateManpages $input]
706    if {![llength $files]} {
707	ArgError "Module \"$module\" has no files to process."	
708    }
709
710    MetadataGet $files
711    StyleMakeLocal
712
713    TocWrite toc index [TocGenerate [TocGet $module toc]]
714    IdxWrite index toc [IdxGenerate $module [IdxGet]]
715
716    dt configure -module $module
717    XrefGet
718    XrefSetup   dt
719    FooterSetup dt
720    MapSetup    dt
721
722    foreach f [lsort -dict $files] {
723	puts stdout \t$f
724
725	set o $out($f)
726	dt configure -file [At $o]
727
728	NavbuttonPush {Keyword Index}     [Output index] $o
729	NavbuttonPush {Table Of Contents} [Output toc]   $o
730	HeaderSetup dt
731	NavbuttonPop
732	NavbuttonPop
733	StyleSetup dt $o
734
735	if {[string equal $format null]} {
736	    dt format [Get [Pick $f]]
737	} else {
738	    Write [At $o] [dt format [Get [Pick $f]]]
739	}
740	Warnings
741    }
742    return
743}
744
745proc ::dtplite::Do.Directory.Merge {} {
746    # See Do.Directory, but merge the TOC/Index information from this
747    # set of input files into an existing TOC/Index.
748
749    variable input
750    variable out
751    variable module
752    variable meta
753    variable output
754    variable format
755
756    # Phase 0. Find the documents to process.
757    # Phase I. Collect meta data, and compute the map from input to
758    # ........ output files. This is also the map for the symbolic
759    # ........ references. We extend an existing map (required for use
760    # ........ in merge op.
761    # Phase II. Build module local toc from the meta data, insert it
762    # ......... into the main toc as well, and generate a global
763    # ......... index.
764    # Phase III. Process each file, using cross references, and links
765    # .......... to boths tocs and the index.
766
767    MapImages
768    set files [LocateManpages $input]
769    if {![llength $files]} {
770	ArgError "Module \"$module\" has no files to process."	
771    }
772
773    MetadataGet $files $module
774    StyleMakeLocal     $module
775
776    set localtoc [TocGet $module $module/toc]
777    TocWrite $module/toc index [TocGenerate $localtoc] [TocMap $localtoc]
778    TocWrite toc         index [TocGenerate [TocMergeSaved $localtoc]]
779    IdxWrite index       toc   [IdxGenerate {} [IdxGetSaved index]]
780
781    dt configure -module $module
782    XrefGetSaved
783    XrefSetup   dt
784    FooterSetup dt
785    MapSetup    dt
786
787    foreach f [lsort -dict $files] {
788	puts stdout \t$f
789
790	set o $out($f)
791	dt configure -file $o
792
793	NavbuttonPush {Keyword Index}          [Output index]       $o
794	NavbuttonPush {Table Of Contents}      [Output $module/toc] $o
795	NavbuttonPush {Main Table Of Contents} [Output toc]         $o
796	HeaderSetup dt
797	NavbuttonPop
798	NavbuttonPop
799	NavbuttonPop
800	StyleSetup dt $o
801
802	if {[string equal $format null]} {
803	    dt format [Get [Pick $f]]
804	} else {
805	    Write [At $o] [dt format [Get [Pick $f]]]
806	}
807	Warnings
808    }
809    return
810}
811
812# ### ### ### ######### ######### #########
813## Helper commands. Preparations shared between the two file modes.
814
815proc ::dtplite::SinglePrep {} {
816    variable input
817    variable module
818
819    MapImages
820    StyleSetup  dt
821    HeaderSetup dt
822    FooterSetup dt
823    MapSetup    dt
824
825    dt configure -module $module -file $input
826    return
827}
828
829# ### ### ### ######### ######### #########
830## Get the base meta data out of the listed documents.
831
832proc ::dtplite::MetadataGet {files {floc {}}} {
833    # meta :: map (symbolicfile -> metadata)
834    # metadata = dict (key -> value)
835    # key      = set { desc, fid, file, keywords,
836    #                  module, section, see_also,
837    #                  shortdesc, title, version }
838    # desc      :: string 'document title'
839    # fid       :: string           'file name, without path/extension'
840    # file      :: string           'file name, without path'
841    # keywords  :: list (string...) 'key phrases'
842    # module    :: string           'module the file is in'
843    # section   :: string           'manpage section'
844    # see_also  :: list (string...) 'related files'
845    # shortdesc :: string           'module description'
846    # title     :: string           'manpage file name intended'
847    # version   :: string           'file/package version'
848    variable meta
849
850    variable out
851
852    doctools::new meta -format list -deprecated 1
853    foreach f $files {
854	meta configure -file $f
855	set o [Output [file join $floc files $f]]
856	set out($f)  $o
857	set meta($o) [lindex [string trim [meta format [Get [Pick $f]]]] 1]
858    }
859    meta destroy
860    return
861}
862
863# ### ### ### ######### ######### #########
864## Handling Tables of Contents:
865## - Get them out of the base meta data.
866## - As above, and merging them with global toc.
867## - Conversion of internals into doctoc.
868## - Processing doctoc into final formatting.
869
870proc ::dtplite::TocGet {desc {f toc}} {
871    # Generate the intermediate form of a TOC for the current document
872    # set. This generates a single division.
873
874    # Get toc out of the meta data.
875    variable meta
876    set res {}
877    foreach {k item} [array get meta] {
878	lappend res [TocItem $k $item]
879    }
880    return [list $desc [list $f $res]]
881}
882
883proc ::dtplite::TocMap {toc {base {}}} {
884    if {$base == {}} {
885	set base  [lindex [lindex $toc 1] 0]
886    }
887    set items [lindex [lindex $toc 1] 1]
888
889    set res {}
890    foreach i $items {
891	foreach {f label desc} $i break
892	lappend res $f [fileutil::relativeUrl $base $f]
893    }
894    return $res
895}
896
897proc ::dtplite::TocItem {f meta} {
898    array set md $meta
899    set desc    $md(desc)
900    set label   $md(title)
901    return [list $f $label $desc]
902}
903
904proc ::dtplite::TocMergeSaved {sub} {
905    # sub is the TOC of the current doc set (local toc). Merge this
906    # into the main toc (as read from the saved global state), and
907    # return the resulting internal rep for further processing.
908
909    set fqn [At .toc]
910    if {[file exists $fqn]} {
911	array set _ [Get $fqn]
912    }
913    array set _ $sub
914    set thetoc [array get _]
915
916    # Save extended toc for next merge.
917    Write $fqn $thetoc
918
919    return $thetoc
920}
921
922proc ::dtplite::TocGenerate {data} {
923    # Handling single and multiple divisions.
924    # single div => div is full toc
925    # multi div  => place divs into the toc in alpha order.
926    #
927    # Sort toc (each division) by label.
928    # Write as doctoc.
929
930    array set toc $data
931
932    TagsBegin
933    if {[array size toc] < 2} {
934	# Empty, or single division. The division is the TOC, toplevel.
935
936	unset toc
937	set desc [lindex $data 0]
938	set data [lindex [lindex $data 1] 1]
939	TocAlign mxf mxl $data
940
941	Tag+ toc_begin [list {Table Of Contents} $desc]
942	foreach item [lsort -dict -index 2 $data] {
943	    foreach {symfile label desc} $item break
944	    Tag+ item \
945		    [FmtR mxf $symfile] \
946		    [FmtR mxl $label] \
947		    [list $desc]
948	}
949    } else {
950	Tag+ toc_begin [list {Table Of Contents} Modules]
951	foreach desc [lsort -dict [array names toc]] {
952	    foreach {ref div} $toc($desc) break
953	    TocAlign mxf mxl $div
954
955	    Tag+ division_start [list $desc [Output $ref]]
956	    foreach item [lsort -dict -index 2 $div] {
957		foreach {symfile label desc} $item break
958		Tag+ item \
959			[FmtR mxf $symfile] \
960			[FmtR mxl $label] \
961			[list $desc]
962	    }
963	    Tag+ division_end
964	}
965    }
966
967    Tag+ toc_end
968
969    #puts ____________________\n[join $lines \n]\n_________________________
970    return [join $lines \n]\n
971}
972
973proc ::dtplite::TocWrite {ftoc findex text {map {}}} {
974    variable format
975
976    if {[string equal $format null]} return
977    Write [At .tocdoc] $text
978
979    set ft [Output $ftoc]
980
981    doctools::toc::new toc -format $format -file $ft
982
983    NavbuttonPush {Keyword Index} [Output $findex] $ftoc
984    HeaderSetup  toc
985    NavbuttonPop
986    FooterSetup  toc
987    StyleSetup   toc $ftoc
988
989    foreach {k v} $map {toc map $k $v}
990
991    Write [At $ft] [toc format $text]
992    toc destroy
993    return
994}
995
996proc ::dtplite::TocAlign {fv lv div} {
997    upvar 1 $fv mxf $lv mxl
998    set mxf 0
999    set mxl 0
1000    foreach item $div {
1001	foreach {symfile label desc} $item break
1002	Max mxf $symfile
1003	Max mxl $label
1004    }
1005    return
1006}
1007
1008# ### ### ### ######### ######### #########
1009## Handling Keyword Indices:
1010## - Get them out of the base meta data.
1011## - As above, and merging them with global index.
1012## - Conversion of internals into docidx.
1013## - Processing docidx into final formatting.
1014
1015proc ::dtplite::IdxGet {{f index}} {
1016    # Get index out of the meta data.
1017    array set keys {}
1018    array set kdup {}
1019    return [lindex [IdxExtractMeta] 1]
1020}
1021
1022proc ::dtplite::IdxGetSaved {{f index}} {
1023    # Get index out of the meta data, merge into global state.
1024    variable meta
1025    variable kwid
1026
1027    array set keys {}
1028    array set kwid {}
1029    array set kdup {}
1030    set start 0
1031
1032    set fqn [At .idx]
1033    if {[file exists $fqn]} {
1034	foreach {kw kd start ki} [Get $fqn] break
1035	array set keys $kw
1036	array set kwid $ki
1037	array set kdup $kd
1038    }
1039
1040    foreach {start theindex} [IdxExtractMeta $start] break
1041
1042    # Save extended index for next merge.
1043    Write $fqn [list $theindex [array get kdup] $start [array get kwid]]
1044
1045    return $theindex
1046}
1047
1048proc ::dtplite::IdxExtractMeta {{start 0}} {
1049    # Get index out of the meta data.
1050    variable meta
1051    variable kwid
1052
1053    upvar keys keys kdup kdup
1054    foreach {k item} [array get meta] {
1055	foreach {symfile keywords label} [IdxItem $k $item] break
1056	# Store inverted file - keyword relationship
1057	# Kdup is used to prevent entering of duplicates.
1058	# Checks full (keyword file label).
1059	foreach k $keywords {
1060	    set kx [list $k $symfile $label]
1061	    if {![info exists kdup($kx)]} {
1062		lappend keys($k) [list $symfile $label]
1063		set kdup($kx) .
1064	    }
1065	    if {[info exist kwid($k)]} continue
1066	    set kwid($k) key$start
1067	    incr start
1068	}
1069    }
1070    return [list $start [array get keys]]
1071}
1072
1073proc ::dtplite::IdxItem {f meta} {
1074    array set md $meta
1075    set keywords $md(keywords)
1076    set title    $md(title)
1077    return [list $f $keywords $title]
1078}
1079
1080proc ::dtplite::IdxGenerate {desc data} {
1081    # Sort by keyword label.
1082    # Write as docidx.
1083
1084    array set keys $data
1085
1086    TagsBegin
1087    Tag+ index_begin [list {Keyword Index} $desc]
1088
1089    foreach k [lsort -dict [array names keys]] {
1090	IdxAlign mxf $keys($k)
1091
1092	Tag+ key [list $k]
1093	foreach v [lsort -dict -index 0 $keys($k)] {
1094	    foreach {file label} $v break
1095	    Tag+ manpage [FmtR mxf $file] [list $label]
1096	}
1097    }
1098
1099    Tag+ index_end
1100    #puts ____________________\n[join $lines \n]\n_________________________
1101    return [join $lines \n]\n
1102}
1103
1104proc ::dtplite::IdxWrite {findex ftoc text} {
1105    variable format
1106
1107    if {[string equal $format null]} return
1108    Write [At .idxdoc] $text
1109
1110    set fi [Output $findex]
1111
1112    doctools::idx::new idx -format $format -file $fi
1113
1114    NavbuttonPush {Table Of Contents} [Output $ftoc] $findex
1115    HeaderSetup   idx
1116    NavbuttonPop
1117    FooterSetup   idx
1118    StyleSetup    idx $findex
1119    XrefSetupKwid idx
1120
1121    Write [At $fi] [idx format $text]
1122    idx destroy
1123    return
1124}
1125
1126proc ::dtplite::IdxAlign {v keys} {
1127    upvar 1 $v mxf
1128    set mxf 0
1129    foreach item $keys {
1130	foreach {symfile label} $item break
1131	Max mxf $symfile
1132    }
1133    return
1134}
1135
1136# ### ### ### ######### ######### #########
1137## Column sizing
1138
1139proc ::dtplite::Max {v str} {
1140    upvar 1 $v max
1141    set l [string length [list $str]]
1142    if {$max < $l} {set max $l}
1143    return
1144}
1145
1146proc ::dtplite::FmtR {v str} {
1147    upvar 1 $v max
1148    return [list $str][textutil::repeat::blank \
1149	    [expr {$max - [string length [list $str]]}]]
1150}
1151
1152# ### ### ### ######### ######### #########
1153## Code generation.
1154
1155proc ::dtplite::Tag {n args} {
1156    if {[llength $args]} {
1157	return "\[$n [join $args]\]"
1158    } else {
1159	return "\[$n\]"
1160    }
1161    #return \[[linsert $args 0 $n]\]
1162}
1163
1164proc ::dtplite::Tag+ {n args} {
1165    upvar 1 lines lines
1166    lappend lines [eval [linsert $args 0 ::dtplite::Tag $n]]
1167    return
1168}
1169
1170proc ::dtplite::TagsBegin {} {
1171    upvar 1 lines lines
1172    set lines {}
1173    return
1174}
1175
1176# ### ### ### ######### ######### #########
1177## Collect all files for possible use as image
1178
1179proc ::dtplite::MapImages {} {
1180    variable input
1181    variable output
1182    variable single
1183    variable stdout
1184
1185    # Ignore images when writing results to a pipe.
1186    if {$stdout} return
1187
1188    set out  [file normalize $output]
1189    set path [file normalize $input]
1190    set res  {}
1191
1192    if {$single} {
1193	# output is file, image directory is sibling to it.
1194	set imgbase [file join [file dirname $output] image]
1195	# input to search is director the input file is in, and below
1196	set path    [file dirname $path]
1197    } else {
1198	# output is directory, image directory is inside.
1199	set imgbase [file join $out image]
1200    }
1201
1202    set n [llength [file split $path]]
1203
1204    foreach f [::fileutil::find $path] {
1205	MapImage \
1206	    [::fileutil::stripN $f $n] \
1207	    $f [file join $imgbase [file tail $f]]
1208    }
1209    return
1210}
1211
1212proc ::dtplite::MapImage {path orig dest} {
1213    # A file a/b/x.y is stored under
1214    # a/b/x.y, b/x.y, and x.y
1215
1216    variable imap
1217    set plist [file split $path]
1218    while {[llength $plist]} {
1219	set imap([join $plist /]) [list $orig $dest]
1220	set plist [lrange $plist 1 end]
1221    }
1222    return
1223}
1224
1225proc ::dtplite::MapSetup {dt} {
1226    # imap :: map (symbolicfile -> list (originpath,destpath)))
1227    variable imap
1228    # Skip if no data available
1229
1230    #puts MIS|[array size imap]|
1231    if {![array size imap]} return
1232
1233    foreach sf [array names imap] {
1234	foreach {origin destination} $imap($sf) break
1235	$dt img $sf $origin $destination
1236    }
1237    return
1238}
1239
1240# ### ### ### ######### ######### #########
1241## Find the documents to process.
1242
1243proc ::dtplite::LocateManpages {path} {
1244    set path [file normalize $path]
1245    set n    [llength [file split $path]]
1246    set res  {}
1247    foreach f [::fileutil::find $path ::dtplite::IsDoctools] {
1248	lappend res [::fileutil::stripN $f $n]
1249    }
1250    return $res
1251}
1252
1253proc ::dtplite::IsDoctools {f} {
1254    set res [in [::fileutil::fileType $f] doctools]
1255    #puts ...$f\t$res\t[fileutil::fileType $f]
1256    return $res
1257}
1258
1259# ### ### ### ######### ######### #########
1260## Handling a style sheet
1261## - Decoupling output from input location.
1262## - Generate HTML to insert into a generated document.
1263
1264proc ::dtplite::StyleMakeLocal {{pfx {}}} {
1265    variable style
1266    if {[string equal $style ""]} return
1267    set base [file join $pfx [file tail $style]]
1268
1269    # TODO input == output does what here ?
1270
1271    file copy -force $style [At $base]
1272    set style $base
1273    return
1274}
1275
1276proc ::dtplite::StyleSetup {o {f {}}} {
1277    variable style
1278    if {[string equal $style ""]}   return
1279    if {![in [$o parameters] meta]} return
1280
1281    if {![string equal $f ""]} {
1282	set dst [fileutil::relativeUrl $f $style]
1283    } else {
1284	set dst $style
1285    }
1286    set value "<link\
1287	    rel=\"stylesheet\"\
1288	    href=\"$dst\"\
1289	    type=\"text/css\">"
1290
1291    $o setparam meta $value
1292    return
1293}
1294
1295# ### ### ### ######### ######### #########
1296## Handling the cross references
1297## - Getting them out of the base meta data.
1298## - ditto, plus merging with saved xref information.
1299## - Insertion into processor, cached list.
1300## - Setting up the keyword-2-anchor map.
1301
1302proc ::dtplite::XrefGet {} {
1303    variable meta
1304    variable xref
1305    variable kwid
1306
1307    array set keys {}
1308    foreach {symfile item} [array get meta] {
1309	array set md $item
1310	# Cross-references ... File based, see-also
1311
1312	set t  $md(title)
1313	set ts ${t}($md(section))
1314	set td $md(desc)
1315
1316	set xref(sa,$t)  [set _ [list $symfile]]
1317	set xref(sa,$ts) $_
1318	set xref($t)     $_ ; # index on manpage file name
1319	set xref($ts)    $_ ; # ditto, with section added
1320	set xref($td)    $_ ; # index on document title
1321
1322	# Store an inverted file - keyword relationship, for the index
1323	foreach kw $md(keywords) {
1324	    lappend keys($kw) $symfile
1325	}
1326    }
1327
1328    set if [Output index]
1329    foreach k [array names keys] {
1330	if {[info exists xref(kw,$k)]} continue
1331
1332	set frag $kwid($k)
1333	set xref(kw,$k) [set _ [list $if $frag]]
1334	set xref($k)    $_
1335    }
1336    return
1337}
1338
1339proc ::dtplite::XrefGetSaved {} {
1340    # xref :: map (xrefid -> list (symbolicfile))
1341    variable  xref
1342    array set xref {}
1343
1344    # Load old cross references, from a previous run
1345    set fqn [At .xrf]
1346    if {[file exists $fqn]} {
1347	array set xref [set s [Get $fqn]]
1348    }
1349
1350    # Add any new cross references ...
1351    XrefGet
1352    Write $fqn [array get xref]
1353    return
1354}
1355
1356proc ::dtplite::XrefSetup {o} {
1357    # xref :: map (xrefid -> list (symbolicfile))
1358    variable xref
1359    # Skip if no data available
1360    if {![array size xref]}         return
1361    # Skip if backend doesn't support an index
1362    if {![in [$o parameters] xref]} return
1363
1364    # Transfer index data to the backend. The data we keep has to be
1365    # re-formatted from a dict into a list of tuples with leading
1366    # xrefid.
1367
1368    # xrefl :: list (list (xrefid symbolicfile...)...)
1369    variable xrefl
1370    if {![info exist xrefl]} {
1371	set xrefl {}
1372	foreach k [array names xref] {
1373	    lappend xrefl [linsert $xref($k) 0 $k]
1374	    set f [lindex $xref($k) 0]
1375	    dt map $f [At $f]
1376	}
1377    }
1378    $o setparam xref $xrefl
1379    return
1380}
1381
1382proc ::dtplite::XrefSetupKwid {o} {
1383    # kwid :: map (label -> anchorname)
1384    variable kwid
1385    # Skip if no data available
1386    if {![array size kwid]}         return
1387    # Skip if backend doesn't support an index
1388    if {![in [$o parameters] kwid]} return
1389    # Transfer index data to the backend
1390    $o setparam kwid [array get kwid]
1391    return
1392}
1393
1394# ### ### ### ######### ######### #########
1395## Extending and shrinking the navigation bar.
1396
1397proc ::dtplite::NavbuttonPush {label file ref} {
1398    # nav = list (list (label reference) ...)
1399    variable nav
1400    set      nav [linsert $nav 0 [list $label [fileutil::relativeUrl $ref $file]]]
1401    return
1402}
1403
1404proc ::dtplite::NavbuttonPop {} {
1405    # nav = list (list (label reference) ...)
1406    variable nav
1407    set      nav [lrange $nav 1 end]
1408    return
1409}
1410
1411# ### ### ### ######### ######### #########
1412## Header/Footer mgmt
1413## Header is merged from regular header, plus nav bar.
1414## Caching the merge result for quicker future access.
1415
1416proc ::dtplite::HeaderSetup {o} {
1417    variable header
1418    variable nav
1419    variable navcache
1420
1421    if {[string equal $header ""] && ![llength $nav]} return
1422    if {![in [$o parameters] header]}                 return
1423
1424    if {![info exists navcache($nav)]} {
1425	set sep 0
1426	set hdr ""
1427	if {![string equal $header ""]} {
1428	    append hdr $header
1429	    set sep 1
1430	}
1431	if {[llength $nav]} {
1432	    if {$sep} {append hdr <br>\n}
1433	    append hdr <hr>\ \[\n
1434
1435	    set first 1
1436	    foreach item $nav {
1437		if {!$first} {append hdr "| "} else {append hdr "  "}
1438		set first 0
1439		foreach {label url} $item break
1440		append hdr "<a href=\"" $url "\">" $label "</a>\n"
1441	    }
1442	    append hdr \]\ <hr>\n
1443	}
1444	set navcache($nav) $hdr
1445    } else {
1446	set hdr $navcache($nav)
1447    }
1448
1449    $o setparam header $hdr
1450    return
1451}
1452
1453proc ::dtplite::FooterSetup {o} {
1454    variable footer
1455    if {[string equal $footer ""]}    return
1456    if {![in [$o parameters] footer]} return
1457    $o setparam footer $footer
1458    return
1459}
1460
1461# ### ### ### ######### ######### #########
1462## Invoking the functionality.
1463
1464if {[catch {
1465    set mode $::dtplite::mode
1466    ::dtplite::Do.$mode
1467} msg]} {
1468    ## puts $::errorInfo
1469    ::dtplite::ArgError $msg
1470}
1471
1472# ### ### ### ######### ######### #########
1473exit
1474