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