diff --git a/src/scriptapps/tools/punkargs_punknative.tcl b/src/scriptapps/tools/punkargs_punknative.tcl new file mode 100644 index 00000000..f027b8e9 --- /dev/null +++ b/src/scriptapps/tools/punkargs_punknative.tcl @@ -0,0 +1,416 @@ +# punkargs_punknative.tcl - PROTOTYPE (companion to punkargs_to_doctools.tcl) +# Punk-native documentation pipeline: generate html + markdown for one package +# directly from its punk::args definitions, faithful to the punk s-style +# synopsis conventions ([...] optionals, placeholders) - bypassing +# doctools entirely (doctools stays Tcl-standard; no man-page output here). +# +# usage: tclsh punkargs_punknative.tcl ?-boxmap none|light? +# -boxmap none (default): faithful heavy box-drawing glyphs +# -boxmap light: map heavy/mixed box chars to the light set (portability +# fallback for viewers with only minimal mono fonts; _lightbox suffix) +# writes: /_punknative.html terminal-faithful rendering: +# punk::args::usage ANSI output converted to styled HTML spans +# (dark terminal theme, box-drawing tables, colours, italics) +# /_punknative.md GitHub-flavoured markdown: +# punk-style synopsis in code fences (ansistripped - exact +# s-style text), summary/help prose, argument tables in +# argspace order (GFM tables; no colour - github sanitizes +# inline styles, so md is the plain-but-faithful skeleton) +# +# The ansi2html converter here is deliberately minimal (SGR subset punk::args +# output uses: reset/bold/dim/italic/underline/strike/reverse, 16-colour, +# 256-colour, truecolour). If adopted, it belongs in punk::ansi as a proper +# public api (would also enable 'screenshots' of arbitrary repl output in docs). + +lassign [split [info tclversion] .] tcl_major tcl_minor +#script lives at /src/scriptapps/tools/punkargs_punknative.tcl +set project_root [file dirname [file dirname [file dirname [file dirname [file dirname [file normalize [info script]/__]]]]]] +if {![file isdirectory $project_root/src/bootsupport/modules]} { + puts stderr "cannot locate /src/bootsupport/modules from script location (derived project_root: $project_root)" + exit 2 +} +set original_tmlist [tcl::tm::list] +tcl::tm::remove {*}$original_tmlist +tcl::tm::add [file normalize $project_root/src/bootsupport/modules] +tcl::tm::add [file normalize $project_root/src/bootsupport/modules_tcl$tcl_major] +tcl::tm::add {*}[lreverse $original_tmlist] +package require platform +set arch [platform::generic] +foreach libdir [list [file normalize $project_root/src/bootsupport/lib] [file normalize $project_root/src/bootsupport/lib/tcl$tcl_major/$arch]] { + if {$libdir ni $::auto_path} {lappend ::auto_path $libdir} +} +package require punk::args +package require punk::ansi + +lassign $argv pkgname outdir +#-boxmap: heavy box-drawing glyphs (U+2501 etc.) are the punk default and +#render correctly PROVIDED the css font stack is applied to the pre element +#itself (the UA stylesheet's 'pre {font-family:monospace}' overrides body +#inheritance - the original overlong-border bug). The stack includes a +#heavy-box-capable font on every mainstream platform (Consolas on windows, +#DejaVu on linux, Menlo on macos), so faithful heavy output is the default. +#-boxmap light maps heavy/mixed glyphs to the light set (universal coverage +#in even minimal mono fonts like Courier New) as a portability fallback. +#Verified 2026-07-13: heavy renders aligned in chrome + firefox with the +#pre-level font stack. +set boxmap none +foreach {k v} [lrange $argv 2 end] { + switch -- $k { + -boxmap { + if {$v ni {light none}} { + puts stderr "-boxmap must be light or none (got '$v')" + exit 2 + } + set boxmap $v + } + default { + puts stderr "unknown option '$k' (known: -boxmap)" + exit 2 + } + } +} +if {$pkgname eq "" || $outdir eq ""} { + puts stderr "usage: punkargs_punknative.tcl ?-boxmap light|none?" + exit 2 +} +set pkgversion [package require $pkgname] +file mkdir $outdir + +# --------------------------------------------------------------------------- +# minimal ansi (SGR) -> html converter +# --------------------------------------------------------------------------- +namespace eval ansi2html { + #16-colour palette (vscode-ish terminal defaults) + variable pal16 { + #000000 #cd3131 #0dbc79 #e5e510 #2472c8 #bc3fbc #11a8cd #e5e5e5 + #666666 #f14c4c #23d18b #f5f543 #3b8eea #d670d6 #29b8db #ffffff + } + variable default_fg "#d4d4d4" + variable default_bg "#1e1e1e" + + proc color256 {n} { + variable pal16 + if {$n < 16} {return [lindex $pal16 $n]} + if {$n <= 231} { + set n [expr {$n - 16}] + set levels {0 95 135 175 215 255} + set r [lindex $levels [expr {$n / 36}]] + set g [lindex $levels [expr {($n % 36) / 6}]] + set b [lindex $levels [expr {$n % 6}]] + return [format "#%02x%02x%02x" $r $g $b] + } + set v [expr {8 + 10 * ($n - 232)}] + return [format "#%02x%02x%02x" $v $v $v] + } + proc fresh_state {} { + return [dict create bold 0 dim 0 italic 0 underline 0 strike 0 reverse 0 fg "" bg ""] + } + #apply one SGR sequence's parameter list to a state dict + proc apply_params {state params} { + variable pal16 + if {![llength $params]} {set params 0} + for {set i 0} {$i < [llength $params]} {incr i} { + set p [lindex $params $i] + #tolerate leading zeros / empty params + if {$p eq ""} {set p 0} + set p [scan $p %d] + switch -- $p { + 0 {set state [fresh_state]} + 1 {dict set state bold 1} + 2 {dict set state dim 1} + 3 {dict set state italic 1} + 4 {dict set state underline 1} + 7 {dict set state reverse 1} + 9 {dict set state strike 1} + 22 {dict set state bold 0; dict set state dim 0} + 23 {dict set state italic 0} + 24 {dict set state underline 0} + 27 {dict set state reverse 0} + 29 {dict set state strike 0} + 39 {dict set state fg ""} + 49 {dict set state bg ""} + 38 - 48 { + set target [expr {$p == 38 ? "fg" : "bg"}] + set mode [lindex $params [incr i]] + if {$mode == 5} { + dict set state $target [color256 [lindex $params [incr i]]] + } elseif {$mode == 2} { + set r [lindex $params [incr i]] + set g [lindex $params [incr i]] + set b [lindex $params [incr i]] + dict set state $target [format "#%02x%02x%02x" $r $g $b] + } + } + default { + if {$p >= 30 && $p <= 37} { + dict set state fg [lindex $pal16 [expr {$p - 30}]] + } elseif {$p >= 90 && $p <= 97} { + dict set state fg [lindex $pal16 [expr {$p - 90 + 8}]] + } elseif {$p >= 40 && $p <= 47} { + dict set state bg [lindex $pal16 [expr {$p - 40}]] + } elseif {$p >= 100 && $p <= 107} { + dict set state bg [lindex $pal16 [expr {$p - 100 + 8}]] + } + #other params ignored + } + } + } + return $state + } + proc state_css {state} { + variable default_fg + variable default_bg + set fg [dict get $state fg] + set bg [dict get $state bg] + if {[dict get $state reverse]} { + lassign [list [expr {$bg eq "" ? $default_bg : $bg}] [expr {$fg eq "" ? $default_fg : $fg}]] fg bg + } + set css [list] + if {$fg ne ""} {lappend css "color:$fg"} + if {$bg ne ""} {lappend css "background-color:$bg"} + if {[dict get $state bold]} {lappend css "font-weight:bold"} + if {[dict get $state dim]} {lappend css "opacity:.62"} + if {[dict get $state italic]} {lappend css "font-style:italic"} + set deco [list] + if {[dict get $state underline]} {lappend deco underline} + if {[dict get $state strike]} {lappend deco line-through} + if {[llength $deco]} {lappend css "text-decoration:[join $deco { }]"} + return [join $css ";"] + } + proc html_escape {text} { + return [string map {& & < < > >} $text] + } + #map heavy and mixed-weight box-drawing chars to the light set (universal + #monospace font coverage - avoids browser glyph-fallback width blowout) + variable lightboxmap { + ━ ─ ┃ │ ┏ ┌ ┓ ┐ ┗ └ ┛ ┘ + ┣ ├ ┫ ┤ ┳ ┬ ┻ ┴ ╋ ┼ + ┡ ├ ┩ ┤ ╇ ┼ ┢ ├ ┪ ┤ ╈ ┼ + ┠ ├ ┨ ┤ ┯ ┬ ┷ ┴ ┿ ┼ ╂ ┼ + } + proc to_lightbox {text} { + variable lightboxmap + return [string map $lightboxmap $text] + } + #convert ANSI text to html (span-styled, for use inside
)
+    proc convert {text} {
+        set out ""
+        set state [fresh_state]
+        #split_codes returns alternating plaintext,codes,plaintext,... (codes
+        #element may contain several adjacent escape sequences)
+        set parts [punk::ansi::ta::split_codes $text]
+        set is_pt 1
+        foreach part $parts {
+            if {$is_pt} {
+                if {$part ne ""} {
+                    set css [state_css $state]
+                    if {$css ne ""} {
+                        append out "[html_escape $part]"
+                    } else {
+                        append out [html_escape $part]
+                    }
+                }
+            } else {
+                #apply each SGR sequence in the code chunk; ignore non-SGR codes
+                foreach {m body} [regexp -all -inline {\x1b\[([0-9;:]*)m} $part] {
+                    set state [apply_params $state [split [string map {: ;} $body] \;]]
+                }
+            }
+            set is_pt [expr {!$is_pt}]
+        }
+        return $out
+    }
+}
+
+# ---------------------------------------------------------------------------
+# shared spec helpers (subset of the doctools converter's)
+# ---------------------------------------------------------------------------
+proc optvalname {optname} {return [string trimleft $optname -]}
+proc is_solo {arginfo} {
+    return [expr {"none" in [split [lindex [dict get $arginfo -type] 0] |]}]
+}
+proc typedisplay {arginfo} {
+    set t [dict get $arginfo -type]
+    return [lindex [split [lindex $t 0] |] 0]
+}
+proc md_escape {text} {
+    #escape for GFM table cells: pipes and newlines
+    set text [punk::ansi::ansistrip $text]
+    set text [string map {| \\| \r\n " " \n " " \r " "} $text]
+    return $text
+}
+proc md_prose {text} {
+    #reflow to paragraphs: source indentation must not survive - 4-space
+    #indented lines are code blocks in markdown (accidental 
 rendering)
+    set text [punk::ansi::ansistrip $text]
+    set paras [list]
+    set current ""
+    foreach ln [split $text \n] {
+        if {[string trim $ln] eq ""} {
+            if {$current ne ""} {lappend paras $current; set current ""}
+        } else {
+            append current [string trim $ln] " "
+        }
+    }
+    if {$current ne ""} {lappend paras $current}
+    return [join $paras "\n\n"]
+}
+#punk s-style synopsis text (ansistripped), minus the '# summary' preamble line;
+#'## FORM n' headers kept only for multi-form commands
+proc punk_synopsis_text {id nforms} {
+    set s [punk::ansi::ansistrip [punk::args::synopsis $id]]
+    set keep [list]
+    foreach ln [split $s \n] {
+        if {[string match "# *" $ln]} {continue}
+        if {[string match "## FORM*" $ln] && $nforms < 2} {continue}
+        lappend keep $ln
+    }
+    return [join $keep \n]
+}
+
+# ---------------------------------------------------------------------------
+# gather
+# ---------------------------------------------------------------------------
+punk::args::update_definitions [list ::$pkgname]
+set ids [lsort [punk::args::get_ids ::${pkgname}::*]]
+set cmd_ids [list]
+foreach id $ids {
+    set tail [string range $id [string length ::${pkgname}::] end]
+    if {[string first :: $tail] < 0} {lappend cmd_ids $id}
+}
+if {![llength $cmd_ids]} {
+    puts stderr "no punk::args ids found for ::${pkgname}::*"
+    exit 1
+}
+#suffix distinguishes these from doctools-engine renderings of the same package
+set pkgfile "[string map {:: _} $pkgname]_punknative"
+if {$boxmap eq "light"} {
+    append pkgfile "_lightbox"
+}
+puts "punk-native docgen: [llength $cmd_ids] command definitions for $pkgname $pkgversion"
+
+# ---------------------------------------------------------------------------
+# html: terminal-faithful - ansi2html of punk::args::usage per command
+# ---------------------------------------------------------------------------
+set html ""
+append html "\n\n"
+append html "$pkgname $pkgversion - punk::args reference\n"
+append html "\n"
+append html "

$pkgname $pkgversion

\n" +append html "

Command reference generated from punk::args definitions (terminal-faithful rendering).

\n" +foreach id $cmd_ids { + set cmdname [string trimleft $id :] + append html "

$cmdname

\n" + set aliastarget [punk::args::get_idalias $id] + if {$aliastarget ne ""} { + append html "

Takes the same arguments as [ansi2html::html_escape [string trimleft $aliastarget :]] (shared argument definition).

\n" + } + if {[catch {punk::args::usage $id} u]} { + append html "

(no usage rendering: [ansi2html::html_escape $u])

\n" + continue + } + if {$boxmap eq "light"} { + set u [ansi2html::to_lightbox $u] + } + append html "
[ansi2html::convert $u]
\n" +} +append html "\n" +set fd [open [file join $outdir $pkgfile.html] w] +fconfigure $fd -translation lf -encoding utf-8 +puts -nonewline $fd $html +close $fd +puts "wrote [file join $outdir $pkgfile.html] ([string length $html] bytes)" + +# --------------------------------------------------------------------------- +# markdown: GFM - punk-style synopsis fences + argument tables (argspace order) +# (-boxmap only affects the html rendering - only the default run writes md, +# so an opt-in heavybox run doesn't duplicate an identical file) +# --------------------------------------------------------------------------- +if {$boxmap ne "none"} { + exit 0 +} +proc md_argrow {argname arginfo} { + set type [md_escape [typedisplay $arginfo]] + if {[is_solo $arginfo]} {set type "(solo flag)"} + set default "" + if {[dict exists $arginfo -default]} {set default [md_escape [dict get $arginfo -default]]} + if {$default eq ""} {set default " "} + set multi " " + if {[dict exists $arginfo -multiple] && [dict get $arginfo -multiple]} {set multi "yes"} + set help "" + if {[dict exists $arginfo -help]} {set help [md_escape [dict get $arginfo -help]]} + set choices [list] + if {[dict exists $arginfo -choices]} {lappend choices {*}[dict get $arginfo -choices]} + if {[dict exists $arginfo -choicegroups]} { + dict for {grp members} [dict get $arginfo -choicegroups] {lappend choices {*}$members} + } + if {[llength $choices]} { + append help " Choices: [md_escape [join $choices {, }]]." + } + return "| `[md_escape $argname]` | $type | `$default` | $multi | $help |" +} +set md "" +append md "# $pkgname $pkgversion\n\n" +append md "Command reference generated from punk::args definitions (punk s-style synopses).\n" +foreach id $cmd_ids { + set spec [punk::args::get_spec $id] + set cmd_info [dict get $spec cmd_info] + set cmdname [string trimleft $id :] + append md "\n---\n\n## $cmdname\n\n" + set aliastarget [punk::args::get_idalias $id] + if {[dict exists $cmd_info -summary] && [dict get $cmd_info -summary] ne ""} { + append md "*[md_escape [dict get $cmd_info -summary]]*\n\n" + } + set nforms [llength [dict get $spec form_names]] + append md "```\n[punk_synopsis_text $id $nforms]\n```\n" + if {$aliastarget ne ""} { + set atail [string trimleft $aliastarget :] + set aanchor [string map {:: -} [string tolower $atail]] + append md "\nTakes the same arguments as \[`$atail`\](#$aanchor) (shared argument definition).\n" + continue + } + if {[dict exists $cmd_info -help] && [dict get $cmd_info -help] ne ""} { + append md "\n[md_prose [dict get $cmd_info -help]]\n" + } + foreach formname [dict get $spec form_names] { + set form [dict get $spec FORMS $formname] + if {$nforms > 1} { + append md "\n**form: $formname**\n" + } + set leaders [dict get $form LEADER_NAMES] + set opts [dict get $form OPT_NAMES] + set vals [dict get $form VAL_NAMES] + if {[llength $leaders] + [llength $opts] + [llength $vals] == 0} {continue} + append md "\n| Arg | Type | Default | Multi | Help |\n" + append md "| --- | --- | --- | --- | --- |\n" + #argspace order: leaders, options, trailing values + foreach argname $leaders { + append md [md_argrow $argname [dict get $form ARG_INFO $argname]] \n + } + foreach optname $opts { + append md [md_argrow $optname [dict get $form ARG_INFO $optname]] \n + } + foreach argname $vals { + append md [md_argrow $argname [dict get $form ARG_INFO $argname]] \n + } + } +} +set fd [open [file join $outdir $pkgfile.md] w] +fconfigure $fd -translation lf -encoding utf-8 +puts -nonewline $fd $md +close $fd +puts "wrote [file join $outdir $pkgfile.md] ([string length $md] bytes)" diff --git a/src/scriptapps/tools/punkargs_to_doctools.tcl b/src/scriptapps/tools/punkargs_to_doctools.tcl new file mode 100644 index 00000000..d676242c --- /dev/null +++ b/src/scriptapps/tools/punkargs_to_doctools.tcl @@ -0,0 +1,318 @@ +# punkargs_to_doctools.tcl - PROTOTYPE (G-? candidate: punk::args as doc source of truth) +# Generate a doctools .man page for one package from its punk::args definitions. +# +# usage: tclsh punkargs_to_doctools.tcl +# e.g: tclsh punkargs_to_doctools.tcl punk::path punk_path.man +# +# Output is deliberately Tcl-standard doctools style ([arg]/[opt] markup; engines +# render optionals as ?...? and placeholders italic). Rendering the punk::args +# s-style synopsis conventions ([...] optionals, placeholders, ANSI +# styling) is out of scope for the doctools target - doctools engines hardcode +# the Tcl conventions and cannot carry ANSI. Punk-style output belongs to a +# separate direct html/markdown pipeline (see punkargs_to_html/markdown +# prototypes and the assessment discussion 2026-07-13). +# +# Trial-quality notes (2026-07-13): +# - runs against the bootsupport modules (like src/tests/runtests.tcl toplevel) +# - covers: @cmd summary/help, per-form [call] synopses (leaders/opts/values, +# solo flags, -multiple, -optional), argument detail lists in argspace order +# (leaders, then options, then trailing values - matching the synopsis and the +# punk::args usage table) with -default/-choices/-choicegroups/ +# -choicerestricted/-choiceprefix/-minsize/-maxsize/-range constraint +# sentences, id aliases (cross-reference entry), @cmd keywords accumulation +# - prose is ansistripped (punk::args -help fields can contain literal ESC from +# %B%/%I% define-time maps) and [ ]-escaped to [lb]/[rb] +# - NOT yet covered: @examples -> [example], @seealso/@doc -> [see_also]/[uri], +# choicelabels as sub-lists, grouped choice display, tables (doctools has +# none), colour (doctools has none) + +lassign [split [info tclversion] .] tcl_major tcl_minor +#script lives at /src/scriptapps/tools/punkargs_to_doctools.tcl +set project_root [file dirname [file dirname [file dirname [file dirname [file dirname [file normalize [info script]/__]]]]]] +if {![file isdirectory $project_root/src/bootsupport/modules]} { + puts stderr "cannot locate /src/bootsupport/modules from script location (derived project_root: $project_root)" + exit 2 +} +set original_tmlist [tcl::tm::list] +tcl::tm::remove {*}$original_tmlist +tcl::tm::add [file normalize $project_root/src/bootsupport/modules] +tcl::tm::add [file normalize $project_root/src/bootsupport/modules_tcl$tcl_major] +tcl::tm::add {*}[lreverse $original_tmlist] +package require platform +set arch [platform::generic] +foreach libdir [list [file normalize $project_root/src/bootsupport/lib] [file normalize $project_root/src/bootsupport/lib/tcl$tcl_major/$arch]] { + if {$libdir ni $::auto_path} {lappend ::auto_path $libdir} +} +package require punk::args +package require punk::ansi + +lassign $argv pkgname outfile +if {$pkgname eq "" || $outfile eq ""} { + puts stderr "usage: punkargs_to_doctools.tcl " + exit 2 +} +set pkgversion [package require $pkgname] + +namespace eval argdoc2man { + #escape doctools-special brackets in prose; strip ANSI (punk::args -help fields + #may contain literal ESC sequences from %B%/%I% define-time maps) + proc prose {text} { + set text [punk::ansi::ansistrip $text] + return [string map {[ [lb] ] [rb]} $text] + } + #multi-line help -> doctools flowed text with [para] breaks on blank lines + proc prose_paras {text} { + set text [prose $text] + set paras [list] + set current "" + foreach ln [split $text \n] { + if {[string trim $ln] eq ""} { + if {$current ne ""} {lappend paras $current; set current ""} + } else { + append current [string trim $ln] " " + } + } + if {$current ne ""} {lappend paras $current} + return [join $paras "\n\[para\]\n"] + } + #display name for the value an option takes, from its argname + proc optvalname {optname} { + return [string trimleft $optname -] + } + #primary type word for display (first alternative, first clause member) + proc typedisplay {arginfo} { + set t [dict get $arginfo -type] + set t0 [lindex $t 0] + set t0 [lindex [split $t0 |] 0] + return $t0 + } + proc is_solo {arginfo} { + return [expr {"none" in [split [lindex [dict get $arginfo -type] 0] |]}] + } + #synopsis markup for one positional (leader or value) argument + proc positional_markup {form argname} { + set ainfo [dict get $form ARG_INFO $argname] + set a "\[arg [list $argname]\]" + if {[dict exists $ainfo -multiple] && [dict get $ainfo -multiple]} { + append a " \[opt \[arg [list $argname...]\]\]" + } + if {[dict exists $ainfo -optional] && [dict get $ainfo -optional]} { + set a "\[opt \"$a\"\]" + } + return $a + } + #build the [call ...] synopsis markup for one form + proc call_markup {cmdname form} { + set bits [list] + foreach argname [dict get $form LEADER_NAMES] { + lappend bits [positional_markup $form $argname] + } + foreach optname [dict get $form OPT_NAMES] { + set ainfo [dict get $form ARG_INFO $optname] + if {[is_solo $ainfo]} { + set a "\[opt \[option [list $optname]\]\]" + } else { + set a "\[opt \"\[option [list $optname]\] \[arg [list [optvalname $optname]]\]\"\]" + } + lappend bits $a + } + foreach argname [dict get $form VAL_NAMES] { + lappend bits [positional_markup $form $argname] + } + return "\[call \[cmd [list $cmdname]\] [join $bits { }]\]" + } + #constraint sentences for one argument from its resolved ARG_INFO entry + proc constraint_sentences {arginfo isopt} { + set out [list] + if {[dict exists $arginfo -default]} { + set def [dict get $arginfo -default] + if {$def eq ""} {set def "(empty)"} + lappend out "Defaults to '[prose $def]'." + } + set choices [list] + if {[dict exists $arginfo -choices]} {lappend choices {*}[dict get $arginfo -choices]} + if {[dict exists $arginfo -choicegroups]} { + dict for {grp members} [dict get $arginfo -choicegroups] {lappend choices {*}$members} + } + if {[llength $choices]} { + set restricted 1 + if {[dict exists $arginfo -choicerestricted]} {set restricted [dict get $arginfo -choicerestricted]} + set s "Choices: [prose [join $choices {, }]]" + if {!$restricted} {append s " (other values accepted)"} + if {[dict exists $arginfo -choiceprefix] && [dict get $arginfo -choiceprefix]} { + append s " (unique prefixes accepted)" + } + lappend out "$s." + } + if {[dict exists $arginfo -multiple] && [dict get $arginfo -multiple]} { + if {$isopt} { + lappend out "May be given multiple times." + } else { + lappend out "Accepts multiple values." + } + } + foreach {k label} {-minsize "Minimum size" -maxsize "Maximum size" -range "Range"} { + if {[dict exists $arginfo $k]} { + lappend out "$label: [prose [dict get $arginfo $k]]." + } + } + return [join $out " "] + } + #help + constraint body for one argument's detail entry + proc argitem_body {arginfo isopt} { + set body "" + if {[dict exists $arginfo -help]} { + set body [prose_paras [dict get $arginfo -help]] + } + set extra [constraint_sentences $arginfo $isopt] + if {$extra ne ""} { + if {$body ne ""} {append body "\n\[para\]\n"} + append body $extra + } + if {$body eq ""} {set body "No description."} + return $body + } + #argument detail lists for one form, in argspace order: leaders, options, + #trailing values (matching the synopsis and the punk::args usage table). + #Group headings are emitted only when more than one group is present. + proc arg_details {form} { + set out "" + set leaders [dict get $form LEADER_NAMES] + set opts [dict get $form OPT_NAMES] + set vals [dict get $form VAL_NAMES] + set ngroups [expr {([llength $leaders]>0) + ([llength $opts]>0) + ([llength $vals]>0)}] + if {[llength $leaders]} { + if {$ngroups > 1} { + append out "\[para\]\[emph {Leading arguments:}\]\n" + } + append out "\[list_begin arguments\]\n" + foreach argname $leaders { + set ainfo [dict get $form ARG_INFO $argname] + append out "\[arg_def [list [typedisplay $ainfo]] [list $argname]\]\n" + append out [argitem_body $ainfo 0] \n + } + append out "\[list_end\]\n" + } + if {[llength $opts]} { + if {$ngroups > 1} { + append out "\[para\]\[emph {Options:}\]\n" + } + append out "\[list_begin options\]\n" + foreach optname $opts { + set ainfo [dict get $form ARG_INFO $optname] + if {[is_solo $ainfo]} { + append out "\[opt_def [list $optname]\]\n" + } else { + append out "\[opt_def [list $optname] \[arg [list [optvalname $optname]]\]\]\n" + } + append out [argitem_body $ainfo 1] \n + } + append out "\[list_end\]\n" + } + if {[llength $vals]} { + if {$ngroups > 1} { + append out "\[para\]\[emph {Trailing values:}\]\n" + } + append out "\[list_begin arguments\]\n" + foreach argname $vals { + set ainfo [dict get $form ARG_INFO $argname] + append out "\[arg_def [list [typedisplay $ainfo]] [list $argname]\]\n" + append out [argitem_body $ainfo 0] \n + } + append out "\[list_end\]\n" + } + return $out + } +} + +#---- gather definitions for the package ------------------------------------ +punk::args::update_definitions [list ::$pkgname] +set ids [lsort [punk::args::get_ids ::${pkgname}::*]] +#keep the page focused on the package's direct commands: skip ids with further +#:: segments below the package namespace (sub-ensembles could get their own pages) +set cmd_ids [list] +foreach id $ids { + set tail [string range $id [string length ::${pkgname}::] end] + if {[string first :: $tail] < 0} { + lappend cmd_ids $id + } +} +if {![llength $cmd_ids]} { + puts stderr "no punk::args ids found for ::${pkgname}::*" + exit 1 +} +puts "converting [llength $cmd_ids] command definitions for package $pkgname $pkgversion" + +#---- emit doctools ---------------------------------------------------------- +set man "" +append man "\[comment {--- generated from punk::args definitions by punkargs_to_doctools.tcl (prototype) - do not edit ---}\]\n" +append man "\[manpage_begin [list $pkgname] n [list $pkgversion]\]\n" +append man "\[moddesc {punkshell - generated argdoc}\]\n" +append man "\[titledesc {Command reference for [list $pkgname] (generated from punk::args definitions)}\]\n" +append man "\[require [list $pkgname] \[opt [list $pkgversion]\]\]\n" +append man "\[description\]\n" +append man "\[para\] Command reference for package [list $pkgname], generated from its punk::args runtime argument definitions.\n" +append man "\[section Commands\]\n" +append man "\[list_begin definitions\]\n" + +set all_keywords [list] +foreach id $cmd_ids { + set spec [punk::args::get_spec $id] + set cmd_info [dict get $spec cmd_info] + set cmdname [string trimleft $id :] + #an id alias is a distinct command sharing another command's definition + #(e.g punk::path::treefilenames_zipfs -> punk::path::treefilenames): + #emit its own synopsis under its own name with a cross-reference body, + #not a full duplicate of the target's documentation + set aliastarget [punk::args::get_idalias $id] + if {$aliastarget ne ""} { + foreach formname [dict get $spec form_names] { + set form [dict get $spec FORMS $formname] + append man [argdoc2man::call_markup $cmdname $form] \n + } + if {[dict exists $cmd_info -summary] && [dict get $cmd_info -summary] ne ""} { + append man "\[emph {[argdoc2man::prose [dict get $cmd_info -summary]]}\]\n\[para\]\n" + } + append man "Takes the same arguments as \[cmd [list [string trimleft $aliastarget :]]\] (shared argument definition) - see above/below for details.\n" + continue + } + if {[dict exists $cmd_info -name]} { + set cmdname [dict get $cmd_info -name] + } + #one [call] per form + foreach formname [dict get $spec form_names] { + set form [dict get $spec FORMS $formname] + append man [argdoc2man::call_markup $cmdname $form] \n + } + #summary + main help + if {[dict exists $cmd_info -summary] && [dict get $cmd_info -summary] ne ""} { + append man "\[emph {[argdoc2man::prose [dict get $cmd_info -summary]]}\]\n\[para\]\n" + } + if {[dict exists $cmd_info -help] && [dict get $cmd_info -help] ne ""} { + append man [argdoc2man::prose_paras [dict get $cmd_info -help]] \n "\[para\]\n" + } + #argument details per form + set formnames [dict get $spec form_names] + foreach formname $formnames { + set form [dict get $spec FORMS $formname] + if {[llength $formnames] > 1} { + append man "\[para\]\[emph \"form: [argdoc2man::prose $formname]\"\]\n" + } + append man [argdoc2man::arg_details $form] + } + #seealso/keywords accumulate to page level + foreach kw [dict get $spec keywords_info] { + if {[dict exists $kw -name]} {lappend all_keywords [dict get $kw -name]} + } +} +append man "\[list_end\]\n" +if {[llength $all_keywords]} { + append man "\[keywords [join [lsort -unique $all_keywords] { }]\]\n" +} +append man "\[manpage_end\]\n" + +set fd [open $outfile w] +fconfigure $fd -translation lf +puts -nonewline $fd $man +close $fd +puts "wrote $outfile ([string length $man] bytes)"