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