1namespace eval ::practcl {} 2 3### 4# Concatenate a file 5### 6proc ::practcl::cat fname { 7 if {![file exists $fname]} { 8 return 9 } 10 set fin [open $fname r] 11 set data [read $fin] 12 close $fin 13 return $data 14} 15 16### 17# Strip the global comments from tcl code. Used to 18# prevent the documentation markup comments from clogging 19# up files intended for distribution in machine readable format. 20### 21proc ::practcl::docstrip text { 22 set result {} 23 foreach line [split $text \n] { 24 append thisline $line \n 25 if {![info complete $thisline]} continue 26 set outline $thisline 27 set thisline {} 28 if {[string trim $outline] eq {}} { 29 continue 30 } 31 if {[string index [string trim $outline] 0] eq "#"} continue 32 set cmd [string trim [lindex $outline 0] :] 33 if {$cmd eq "namespace" && [lindex $outline 1] eq "eval"} { 34 append result [list {*}[lrange $outline 0 end-1]] " " \{ \n [docstrip [lindex $outline end]]\} \n 35 continue 36 } 37 if {[string match "*::define" $cmd] && [llength $outline]==3} { 38 append result [list {*}[lrange $outline 0 end-1]] " " \{ \n [docstrip [lindex $outline end]]\} \n 39 continue 40 } 41 if {$cmd eq "oo::class" && [lindex $outline 1] eq "create"} { 42 append result [list {*}[lrange $outline 0 end-1]] " " \{ \n [docstrip [lindex $outline end]]\} \n 43 continue 44 } 45 append result $outline 46 } 47 return $result 48} 49 50### 51# Append a line of text to a variable. Optionally apply a string mapping. 52# argspec: 53# map {mandatory 0 positional 1} 54# text {mandatory 1 positional 1} 55### 56proc ::putb {buffername args} { 57 upvar 1 $buffername buffer 58 switch [llength $args] { 59 1 { 60 append buffer [lindex $args 0] \n 61 } 62 2 { 63 append buffer [string map {*}$args] \n 64 } 65 default { 66 error "usage: putb buffername ?map? string" 67 } 68 } 69} 70 71### 72# Tool for build scripts to dynamically generate manual files from comments 73# in source code files 74# example: 75# set authors { 76# {John Doe} {jdoe@illustrious.edu} 77# {Tom RichardHarry} {tomdickharry@illustrius.edu} 78# } 79# # Create the object 80# ::practcl::doctool create AutoDoc 81# set fout [open [file join $moddir module.tcl] w] 82# foreach file [glob [file join $srcdir *.tcl]] { 83# set content [::practcl::cat [file join $srcdir $file]] 84# # Scan the file 85# AutoDoc scan_text $content 86# # Strip the comments from the distribution 87# puts $fout [::practcl::docstrip $content] 88# } 89# # Write out the manual page 90# set manout [open [file join $moddir module.man] w] 91# dict set args header [string map $modmap [::practcl::cat [file join $srcdir manual.txt]]] 92# dict set args footer [string map $modmap [::practcl::cat [file join $srcdir footer.txt]]] 93# dict set args authors $authors 94# puts $manout [AutoDoc manpage {*}$args] 95# close $manout 96### 97::oo::class create ::practcl::doctool { 98 constructor {} { 99 my reset 100 } 101 102 ### 103 # Process an argument list into an informational dict. 104 # This method also understands non-positional 105 # arguments expressed in the notation of Tip 471 106 # [uri https://core.tcl-lang.org/tips/doc/trunk/tip/479.md]. 107 # [para] 108 # The output will be a dictionary of all of the fields and whether the fields 109 # are [const positional], [const mandatory], and whether they have a 110 # [const default] value. 111 # [para] 112 # example: 113 # my argspec {a b {c 10}} 114 # 115 # > a {positional 1 mandatory 1} b {positional 1 mandatory 1} c {positional 1 mandatory 0 default 10} 116 ### 117 method argspec {argspec} { 118 set result [dict create] 119 foreach arg $argspec { 120 set name [lindex $arg 0] 121 dict set result $name positional 1 122 dict set result $name mandatory 1 123 if {$name in {args dictargs}} { 124 switch [llength $arg] { 125 1 { 126 dict set result $name mandatory 0 127 } 128 2 { 129 dict for {optname optinfo} [lindex $arg 1] { 130 set optname [string trim $optname -:] 131 dict set result $optname {positional 1 mandatory 0} 132 dict for {f v} $optinfo { 133 dict set result $optname [string trim $f -:] $v 134 } 135 } 136 } 137 default { 138 error "Bad argument" 139 } 140 } 141 } else { 142 switch [llength $arg] { 143 1 { 144 dict set result $name mandatory 1 145 } 146 2 { 147 dict set result $name mandatory 0 148 dict set result $name default [lindex $arg 1] 149 } 150 default { 151 error "Bad argument" 152 } 153 } 154 } 155 } 156 return $result 157 } 158 159 ### 160 # Convert a block of comments into an informational dictionary. 161 # If lines in the comment start with a single word ending in a colon, 162 # all subsequent lines are appended to a dictionary field of that name. 163 # If no fields are given, all of the text is appended to the [const description] 164 # field. 165 # example: 166 # my comment {Does something cool} 167 # > description {Does something cool} 168 # 169 # my comment { 170 # title : Something really cool 171 # author : Sean Woods 172 # author : John Doe 173 # description : 174 # This does something really cool! 175 # } 176 # > description {This does something really cool!} 177 # title {Something really cool} 178 # author {Sean Woods 179 # John Doe} 180 ### 181 method comment block { 182 set count 0 183 set field description 184 set result [dict create description {}] 185 foreach line [split $block \n] { 186 set sline [string trim $line] 187 set fwidx [string first " " $sline] 188 if {$fwidx < 0} { 189 set firstword [string range $sline 0 end] 190 set restline {} 191 } else { 192 set firstword [string range $sline 0 [expr {$fwidx-1}]] 193 set restline [string range $sline [expr {$fwidx+1}] end] 194 } 195 if {[string index $firstword end] eq ":"} { 196 set field [string tolower [string trim $firstword -:]] 197 switch $field { 198 dictargs - 199 arglist { 200 set field argspec 201 } 202 desc { 203 set field description 204 } 205 } 206 if {[string length $restline]} { 207 dict append result $field "$restline\n" 208 } 209 } else { 210 dict append result $field "$line\n" 211 } 212 } 213 return $result 214 } 215 216 method keyword.Annotation {resultvar commentblock type name body} { 217 upvar 1 $resultvar result 218 set name [string trim $name :] 219 if {[dict exists $result $type $name]} { 220 set info [dict get $result $type $name] 221 } else { 222 set info [my comment $commentblock] 223 } 224 foreach {f v} $body { 225 dict set info $f $v 226 } 227 dict set result $type $name $info 228 } 229 230 ### 231 # Process an oo::objdefine call that modifies the class object 232 # itself 233 #### 234 method keyword.Class {resultvar commentblock name body} { 235 upvar 1 $resultvar result 236 set name [string trim $name :] 237 if {[dict exists $result class $name]} { 238 set info [dict get $result class $name] 239 } else { 240 set info [my comment $commentblock] 241 } 242 set commentblock {} 243 foreach line [split $body \n] { 244 append thisline $line \n 245 if {![info complete $thisline]} continue 246 set thisline [string trim $thisline] 247 if {[string index $thisline 0] eq "#"} { 248 append commentblock [string trimleft $thisline #] \n 249 set thisline {} 250 continue 251 } 252 set cmd [string trim [lindex $thisline 0] ":"] 253 switch $cmd { 254 Option - 255 option { 256 my keyword.Annotation info $commentblock option [lindex $thisline 1] [lindex $thisline 2] 257 set commentblock {} 258 } 259 variable - 260 Variable { 261 my keyword.Annotation info $commentblock variable [lindex $thisline 1] [list type scaler default [lindex $thisline 2]] 262 set commentblock {} 263 } 264 Dict - 265 Array { 266 set iinfo [lindex $thisline 2] 267 dict set iinfo type [string tolower $cmd] 268 my keyword.Annotation info $commentblock variable [lindex $thisline 1] $iinfo 269 set commentblock {} 270 } 271 Componant - 272 Delegate { 273 my keyword.Annotation info $commentblock delegate [lindex $thisline 1] [lindex $thisline 2] 274 set commentblock {} 275 } 276 method - 277 Ensemble { 278 my keyword.Class_Method info $commentblock {*}[lrange $thisline 1 end-1] 279 set commentblock {} 280 } 281 } 282 set thisline {} 283 } 284 dict set result class $name $info 285 } 286 287 ### 288 # Process an oo::define, clay::define, etc statement. 289 ### 290 method keyword.class {resultvar commentblock name body} { 291 upvar 1 $resultvar result 292 set name [string trim $name :] 293 if {[dict exists $result class $name]} { 294 set info [dict get $result class $name] 295 } else { 296 set info [my comment $commentblock] 297 } 298 set commentblock {} 299 foreach line [split $body \n] { 300 append thisline $line \n 301 if {![info complete $thisline]} continue 302 set thisline [string trim $thisline] 303 if {[string index $thisline 0] eq "#"} { 304 append commentblock [string trimleft $thisline #] \n 305 set thisline {} 306 continue 307 } 308 set cmd [string trim [lindex $thisline 0] ":"] 309 switch $cmd { 310 Option - 311 option { 312 puts [list keyword.Annotation $cmd $thisline] 313 my keyword.Annotation info $commentblock option [lindex $thisline 1] [lindex $thisline 2] 314 set commentblock {} 315 } 316 variable - 317 Variable { 318 my keyword.Annotation info $commentblock variable [lindex $thisline 1] [list default [lindex $thisline 2]] 319 set commentblock {} 320 } 321 Dict - 322 Array { 323 set iinfo [lindex $thisline 2] 324 dict set iinfo type [string tolower $cmd] 325 my keyword.Annotation info $commentblock variable [lindex $thisline 1] $iinfo 326 set commentblock {} 327 } 328 Componant - 329 Delegate { 330 my keyword.Annotation info $commentblock delegate [lindex $thisline 1] [lindex $thisline 2] 331 set commentblock {} 332 } 333 superclass { 334 dict set info ancestors [lrange $thisline 1 end] 335 set commentblock {} 336 } 337 classmethod - 338 class_method - 339 Class_Method { 340 my keyword.Class_Method info $commentblock {*}[lrange $thisline 1 end-1] 341 set commentblock {} 342 } 343 destructor - 344 constructor { 345 my keyword.method info $commentblock {*}[lrange $thisline 0 end-1] 346 set commentblock {} 347 } 348 method - 349 Ensemble { 350 my keyword.method info $commentblock {*}[lrange $thisline 1 end-1] 351 set commentblock {} 352 } 353 } 354 set thisline {} 355 } 356 dict set result class $name $info 357 } 358 359 ### 360 # Process a statement for a clay style class method 361 ### 362 method keyword.Class_Method {resultvar commentblock name args} { 363 upvar 1 $resultvar result 364 set info [my comment $commentblock] 365 if {[dict exists $info show_body] && [dict get $info show_body]} { 366 dict set info internals [lindex $args end] 367 } 368 if {[dict exists $info ensemble]} { 369 dict for {method minfo} [dict get $info ensemble] { 370 dict set result Class_Method "${name} $method" $minfo 371 } 372 } else { 373 switch [llength $args] { 374 1 { 375 set argspec [lindex $args 0] 376 } 377 0 { 378 set argspec dictargs 379 #set body [lindex $args 0] 380 } 381 default {error "could not interpret method $name {*}$args"} 382 } 383 if {![dict exists $info argspec]} { 384 dict set info argspec [my argspec $argspec] 385 } 386 dict set result Class_Method [string trim $name :] $info 387 } 388 } 389 390 ### 391 # Process a statement for a tcloo style object method 392 ### 393 method keyword.method {resultvar commentblock name args} { 394 upvar 1 $resultvar result 395 set info [my comment $commentblock] 396 if {[dict exists $info show_body] && [dict get $info show_body]} { 397 dict set info internals [lindex $args end] 398 } 399 if {[dict exists $info ensemble]} { 400 dict for {method minfo} [dict get $info ensemble] { 401 dict set result method "\"${name} $method\"" $minfo 402 } 403 } else { 404 switch [llength $args] { 405 1 { 406 set argspec [lindex $args 0] 407 } 408 0 { 409 set argspec dictargs 410 #set body [lindex $args 0] 411 } 412 default {error "could not interpret method $name {*}$args"} 413 } 414 if {![dict exists $info argspec]} { 415 dict set info argspec [my argspec $argspec] 416 } 417 dict set result method "\"[split [string trim $name :] ::]\"" $info 418 } 419 } 420 421 ### 422 # Process a proc statement 423 ### 424 method keyword.proc {commentblock name argspec} { 425 set info [my comment $commentblock] 426 if {![dict exists $info argspec]} { 427 dict set info argspec [my argspec $argspec] 428 } 429 return $info 430 } 431 432 ### 433 # Reset the state of the object and its embedded coroutine 434 ### 435 method reset {} { 436 my variable coro 437 set coro [info object namespace [self]]::coro 438 oo::objdefine [self] forward coro $coro 439 if {[info command $coro] ne {}} { 440 rename $coro {} 441 } 442 coroutine $coro {*}[namespace code {my Main}] 443 } 444 445 ### 446 # Main body of the embedded coroutine for the object 447 ### 448 method Main {} { 449 450 my variable info 451 set info [dict create] 452 yield [info coroutine] 453 set thisline {} 454 set commentblock {} 455 set linec 0 456 while 1 { 457 set line [yield] 458 append thisline $line \n 459 if {![info complete $thisline]} continue 460 set thisline [string trim $thisline] 461 if {[string index $thisline 0] eq "#"} { 462 append commentblock [string trimleft $thisline #] \n 463 set thisline {} 464 continue 465 } 466 set cmd [string trim [lindex $thisline 0] ":"] 467 switch $cmd { 468 dictargs::proc { 469 set procinfo [my keyword.proc $commentblock [lindex $thisline 1] [list args [list dictargs [lindex $thisline 2]]]] 470 if {[dict exists $procinfo show_body] && [dict get $procinfo show_body]} { 471 dict set procinfo internals [lindex $thisline end] 472 } 473 dict set info proc [string trim [lindex $thisline 1] :] $procinfo 474 set commentblock {} 475 } 476 tcllib::PROC - 477 PROC - 478 Proc - 479 proc { 480 set procinfo [my keyword.proc $commentblock {*}[lrange $thisline 1 2]] 481 if {[dict exists $procinfo show_body] && [dict get $procinfo show_body]} { 482 dict set procinfo internals [lindex $thisline end] 483 } 484 dict set info proc [string trim [lindex $thisline 1] :] $procinfo 485 set commentblock {} 486 } 487 oo::objdefine { 488 if {[llength $thisline]==3} { 489 lassign $thisline tcmd name body 490 my keyword.Class info $commentblock $name $body 491 } else { 492 puts "Warning: bare oo::define in library" 493 } 494 } 495 oo::define { 496 if {[llength $thisline]==3} { 497 lassign $thisline tcmd name body 498 my keyword.class info $commentblock $name $body 499 } else { 500 puts "Warning: bare oo::define in library" 501 } 502 } 503 tao::define - 504 clay::define - 505 tool::define { 506 lassign $thisline tcmd name body 507 my keyword.class info $commentblock $name $body 508 set commentblock {} 509 } 510 oo::class { 511 lassign $thisline tcmd mthd name body 512 my keyword.class info $commentblock $name $body 513 set commentblock {} 514 } 515 default { 516 if {[lindex [split $cmd ::] end] eq "define"} { 517 lassign $thisline tcmd name body 518 my keyword.class info $commentblock $name $body 519 set commentblock {} 520 } 521 set commentblock {} 522 } 523 } 524 set thisline {} 525 } 526 } 527 528 ### 529 # Generate the manual page text for a method or proc 530 ### 531 method section.method {keyword method minfo} { 532 set result {} 533 set line "\[call $keyword \[cmd $method\]" 534 if {[dict exists $minfo argspec]} { 535 dict for {argname arginfo} [dict get $minfo argspec] { 536 set positional 1 537 set mandatory 1 538 set repeating 0 539 dict with arginfo {} 540 if {$mandatory==0} { 541 append line " \[opt \"" 542 } else { 543 append line " " 544 } 545 if {$positional} { 546 append line "\[arg $argname" 547 } else { 548 append line "\[option \"$argname" 549 if {[dict exists $arginfo type]} { 550 append line " \[emph [dict get $arginfo type]\]" 551 } else { 552 append line " \[emph value\]" 553 } 554 append line "\"" 555 } 556 append line "\]" 557 if {$mandatory==0} { 558 if {[dict exists $arginfo default]} { 559 append line " \[const \"[dict get $arginfo default]\"\]" 560 } 561 append line "\"\]" 562 } 563 if {$repeating} { 564 append line " \[opt \[option \"$argname...\"\]\]" 565 } 566 } 567 } 568 append line \] 569 putb result $line 570 if {[dict exists $minfo description]} { 571 putb result [dict get $minfo description] 572 } 573 if {[dict exists $minfo example]} { 574 putb result "\[para\]Example: \[example [list [dict get $minfo example]]\]" 575 } 576 if {[dict exists $minfo internals]} { 577 putb result "\[para\]Internals: \[example [list [dict get $minfo internals]]\]" 578 } 579 return $result 580 } 581 582 method section.annotation {type name iinfo} { 583 set result "\[call $type \[cmd $name\]\]" 584 if {[dict exists $iinfo description]} { 585 putb result [dict get $iinfo description] 586 } 587 if {[dict exists $iinfo example]} { 588 putb result "\[para\]Example: \[example [list [dict get $minfo example]]\]" 589 } 590 return $result 591 } 592 593 ### 594 # Generate the manual page text for a class 595 ### 596 method section.class {class_name class_info} { 597 set result {} 598 putb result "\[subsection \{Class $class_name\}\]" 599 if {[dict exists $class_info ancestors]} { 600 set line "\[emph \"ancestors\"\]:" 601 foreach {c} [dict get $class_info ancestors] { 602 append line " \[class [string trim $c :]\]" 603 } 604 putb result $line 605 putb result {[para]} 606 } 607 dict for {f v} $class_info { 608 if {$f in {Class_Method method description ancestors example option variable delegate}} continue 609 putb result "\[emph \"$f\"\]: $v" 610 putb result {[para]} 611 } 612 if {[dict exists $class_info example]} { 613 putb result "\[example \{[list [dict get $class_info example]]\}\]" 614 putb result {[para]} 615 } 616 if {[dict exists $class_info description]} { 617 putb result [dict get $class_info description] 618 putb result {[para]} 619 } 620 dict for {f v} $class_info { 621 if {$f ni {option variable delegate}} continue 622 putb result "\[class \{[string totitle $f]\}\]" 623 #putb result "Methods on the class object itself." 624 putb result {[list_begin definitions]} 625 dict for {item iinfo} [dict get $class_info $f] { 626 putb result [my section.annotation $f $item $iinfo] 627 } 628 putb result {[list_end]} 629 putb result {[para]} 630 } 631 if {[dict exists $class_info Class_Method]} { 632 putb result "\[class \{Class Methods\}\]" 633 #putb result "Methods on the class object itself." 634 putb result {[list_begin definitions]} 635 dict for {method minfo} [dict get $class_info Class_Method] { 636 putb result [my section.method classmethod $method $minfo] 637 } 638 putb result {[list_end]} 639 putb result {[para]} 640 } 641 if {[dict exists $class_info method]} { 642 putb result "\[class {Methods}\]" 643 putb result {[list_begin definitions]} 644 dict for {method minfo} [dict get $class_info method] { 645 putb result [my section.method method $method $minfo] 646 } 647 putb result {[list_end]} 648 putb result {[para]} 649 } 650 return $result 651 } 652 653 ### 654 # Generate the manual page text for the commands section 655 ### 656 method section.command {procinfo} { 657 set result {} 658 putb result "\[section \{Commands\}\]" 659 putb result {[list_begin definitions]} 660 dict for {method minfo} $procinfo { 661 putb result [my section.method proc $method $minfo] 662 } 663 putb result {[list_end]} 664 return $result 665 } 666 667 ### 668 # Generate the manual page. Returns the completed text suitable for saving in .man file. 669 # The header argument is a block of doctools text to go in before the machine generated 670 # section. footer is a block of doctools text to go in after the machine generated 671 # section. authors is a list of individual authors and emails in the form of AUTHOR EMAIL ?AUTHOR EMAIL?... 672 # 673 # argspec: 674 # header {mandatory 0 positional 0} 675 # footer {mandatory 0 positional 0} 676 # authors {mandatory 0 positional 0 type list} 677 ### 678 method manpage args { 679 my variable info 680 set map {%version% 0.0 %module% {Your_Module_Here}} 681 set result {} 682 set header {} 683 set footer {} 684 set authors {} 685 dict with args {} 686 dict set map %keyword% comment 687 putb result $map {[%keyword% {-*- tcl -*- doctools manpage}] 688[vset PACKAGE_VERSION %version%] 689[manpage_begin %module% n [vset PACKAGE_VERSION]]} 690 putb result $map $header 691 692 dict for {sec_type sec_info} $info { 693 switch $sec_type { 694 proc { 695 putb result [my section.command $sec_info] 696 } 697 class { 698 putb result "\[section Classes\]" 699 dict for {class_name class_info} $sec_info { 700 putb result [my section.class $class_name $class_info] 701 } 702 } 703 default { 704 putb result "\[section [list $sec_type $sec_name]\]" 705 if {[dict exists $sec_info description]} { 706 putb result [dict get $sec_info description] 707 } 708 } 709 } 710 } 711 if {[llength $authors]} { 712 putb result {[section AUTHORS]} 713 foreach {name email} $authors { 714 putb result "$name \[uri mailto:$email\]\[para\]" 715 } 716 } 717 putb result $footer 718 putb result {[manpage_end]} 719 return $result 720 } 721 722 # Scan a block of text 723 method scan_text {text} { 724 my variable linecount coro 725 set linecount 0 726 foreach line [split $text \n] { 727 incr linecount 728 $coro $line 729 } 730 } 731 732 # Scan a file of text 733 method scan_file {filename} { 734 my variable linecount coro 735 set fin [open $filename r] 736 set linecount 0 737 while {[gets $fin line]>=0} { 738 incr linecount 739 $coro $line 740 } 741 close $fin 742 } 743} 744 745