You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
556 lines
25 KiB
556 lines
25 KiB
# 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, <type> placeholders) - bypassing |
|
# doctools entirely (doctools stays Tcl-standard; no man-page output here). |
|
# |
|
# usage: tclsh punkargs_punknative.tcl <package> <outdir> ?-boxmap none|light? ?-assets embed|link|none? |
|
# -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) |
|
# -assets embed (default): data-uri the pinned punkdoc-mono web font |
|
# (fontprep/punkdoc-mono.woff2) into the page - fully self-contained |
|
# single file; every glyph (incl. octants/legacy computing) renders |
|
# identically on any machine (~106KB page overhead) |
|
# -assets link: reference punkdoc-mono.woff2 relatively and copy it to |
|
# <outdir> - one cached font shared by a whole generated doc set |
|
# -assets none: no font shipped; rely on the viewer's installed fonts |
|
# via the css stack (octants/legacy-computing glyphs may be missing) |
|
# writes: <outdir>/<pkg>_punknative.html terminal-faithful rendering: |
|
# punk::args::usage ANSI output converted to styled HTML spans |
|
# (dark terminal theme, box-drawing tables, colours, italics) |
|
# <outdir>/<pkg>_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 <projectroot>/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 <projectroot>/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::lib ;#punk::args::lib::tstr placeholder resolution uses punk::lib::undent |
|
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 |
|
set assets embed |
|
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 |
|
} |
|
-assets { |
|
if {$v ni {embed link none}} { |
|
puts stderr "-assets must be embed, link or none (got '$v')" |
|
exit 2 |
|
} |
|
set assets $v |
|
} |
|
default { |
|
puts stderr "unknown option '$k' (known: -boxmap -assets)" |
|
exit 2 |
|
} |
|
} |
|
} |
|
if {$pkgname eq "" || $outdir eq ""} { |
|
puts stderr "usage: punkargs_punknative.tcl <package> <outdir> ?-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" |
|
#terminal backdrop for rendered blocks: BLACK. overtype::renderspace |
|
#overlay output uses explicit SGR 40 (black bg) and art/example helpers |
|
#treat 'default background' as the terminal's - on a black pre they blend |
|
#exactly as in a black terminal (a grey pre showed contrasting black bars |
|
#inside example blocks). The page body stays lighter for contrast. |
|
variable default_bg "#000000" |
|
|
|
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 <pre>) |
|
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 "<span style=\"$css\">[html_escape $part]</span>" |
|
} 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 <pre> 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 |
|
# --------------------------------------------------------------------------- |
|
#pinned web font (see fontprep/README.md): guarantees every glyph in the punk |
|
#domain - incl. unicode 16 octants and legacy computing blocks that no |
|
#commonly-installed font provides. Variable (wght 200-700) so bold spans use |
|
#real bold. OFL-subset of Cascadia Mono renamed punkdoc-mono (RFN condition). |
|
set fontfile [file join [file dirname [file dirname [file normalize [info script]/__]]] fontprep punkdoc-mono.woff2] |
|
set fontface "" |
|
if {$assets ne "none"} { |
|
if {![file exists $fontfile]} { |
|
puts stderr "WARNING: -assets $assets requested but $fontfile not found - falling back to -assets none" |
|
set assets none |
|
} else { |
|
if {$assets eq "embed"} { |
|
set fd [open $fontfile rb] |
|
set fontb64 [binary encode base64 [read $fd]] |
|
close $fd |
|
set fontsrc "data:font/woff2;base64,$fontb64" |
|
} else { |
|
#link mode: ship the font next to the page(s) - shared+cached |
|
file copy -force $fontfile [file join $outdir punkdoc-mono.woff2] |
|
set fontsrc "punkdoc-mono.woff2" |
|
} |
|
set fontface "@font-face {font-family:\"punkdoc-mono\"; src:url($fontsrc) format(\"woff2\"); font-weight:200 700; font-display:block;}\n" |
|
} |
|
} |
|
|
|
set html "" |
|
append html "<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">\n" |
|
append html "<title>$pkgname $pkgversion - punk::args reference</title>\n" |
|
append html "<style>\n" |
|
append html $fontface |
|
#fallback stack: fonts with FULL box-drawing coverage (incl. heavy + |
|
#mixed-weight chars) first - Courier New (firefox's default mono) lacks the |
|
#heavy set. lang=en above steers chrome away from CJK ambiguous-width |
|
#(double-width) fallback for box chars when no stack font matches. |
|
set fontstack "'Cascadia Mono','Cascadia Code','DejaVu Sans Mono','Noto Sans Mono','JetBrains Mono','Liberation Mono',Consolas,Menlo,monospace" |
|
if {$fontface ne ""} { |
|
set fontstack "'punkdoc-mono',$fontstack" |
|
} |
|
append html "body {background:#1e1e1e; color:$ansi2html::default_fg; font-family:$fontstack;}\n" |
|
append html "h1,h2,h3 {font-family:inherit; color:#e5e510;}\n" |
|
append html "nav.cmdindex {background:#000; padding:8px 12px; display:inline-block;}\n" |
|
append html "nav.cmdindex a {color:#29b8db; text-decoration:none; display:inline-block; margin-right:1.5em;}\n" |
|
append html "nav.cmdindex a:hover {text-decoration:underline;}\n" |
|
#IMPORTANT: pre needs its own font-family - the UA stylesheet's |
|
#'pre {font-family:monospace}' overrides inheritance from body, so without |
|
#this the pre renders in the browser's default mono font regardless of the |
|
#body stack (the original cause of overlong heavy-box border lines) |
|
#punkdoc-mono cell geometry (see fontprep/README.md): advance is exactly |
|
#0.5859375em - font-size 15.36px gives an integer 9px cell width (fractional |
|
#advances round differently at styled-span boundaries -> vertical seams |
|
#through block/border glyphs). Row pitch: browsers size inline BACKGROUNDS |
|
#to the font's hhea content area = 1.1621em = 17.85px here; a fractional |
|
#pitch (1.15em = 17.664px) put row boundaries at fractional positions and |
|
#showed periodic hairline gaps through solid-bg runs (~every 2 lines). |
|
#line-height:17px = integer pitch just under the bg height, so adjacent |
|
#rows' backgrounds overlap ~0.85px - sealed at any zoom/dpi (cost: bg rows |
|
#overpaint <1px of the row above's descenders - imperceptible in tables). |
|
#If the font stack falls back past punkdoc-mono this exactness is lost but |
|
#text-only content tolerates it. |
|
append html "pre.punkterm {font-family:$fontstack; line-height:17px; font-size:15.36px; overflow-x:auto; padding:8px; background:$ansi2html::default_bg; font-variant-ligatures:none;}\n" |
|
append html "hr {border:0; border-top:1px solid #444;}\n" |
|
append html "</style></head><body>\n" |
|
append html "<h1>$pkgname $pkgversion</h1>\n" |
|
append html "<p>Command reference generated from punk::args definitions (terminal-faithful rendering).</p>\n" |
|
|
|
#module/package-level info when @package directives are registered (none in |
|
#older module versions - the hook is here for when dev modules surface them) |
|
set package_notes [list] |
|
foreach id $cmd_ids { |
|
set spec [punk::args::get_spec $id] |
|
#package_info key absent in older punk::args spec dicts (pre-@package) |
|
if {![dict exists $spec package_info]} {break} |
|
set pinfo [dict get $spec package_info] |
|
if {[llength $pinfo] && $pinfo ni $package_notes} { |
|
lappend package_notes $pinfo |
|
} |
|
} |
|
foreach pinfo $package_notes { |
|
append html "<p>package: [ansi2html::html_escape [punk::ansi::ansistrip $pinfo]]</p>\n" |
|
} |
|
|
|
#command index navigation (doctools-style jump list) |
|
append html "<nav class=\"cmdindex\">\n" |
|
foreach id $cmd_ids { |
|
set cmdname [string trimleft $id :] |
|
set esc [ansi2html::html_escape $cmdname] |
|
append html "<a href=\"#$esc\">$esc</a>\n" |
|
} |
|
append html "</nav>\n" |
|
|
|
#helper: one converted usage block. |
|
#-trim 1: keep only the Arg table (from its header separator down) - used for |
|
#per-form blocks so they don't repeat the whole command/description/synopsis |
|
#panel and drown the per-form differences (renderspace output lines are |
|
#colour-self-contained, so slicing at a line boundary is safe) |
|
proc usage_block {id args} { |
|
global boxmap |
|
set trim 0 |
|
set idx [lsearch -exact $args -trim] |
|
if {$idx >= 0} { |
|
set trim [lindex $args $idx+1] |
|
set args [lreplace $args $idx $idx+1] |
|
} |
|
if {[catch {punk::args::usage {*}$args $id} u]} { |
|
return "<p>(no usage rendering: [ansi2html::html_escape $u])</p>\n" |
|
} |
|
if {$trim} { |
|
set lines [split $u \n] |
|
set argidx -1 |
|
set i 0 |
|
foreach ln $lines { |
|
if {[string match "┃Arg *" [punk::ansi::ansistrip $ln]]} { |
|
set argidx $i |
|
break |
|
} |
|
incr i |
|
} |
|
if {$argidx > 0} { |
|
#include the separator line above the Arg header as the top border |
|
set u [join [lrange $lines $argidx-1 end] \n] |
|
} |
|
} |
|
if {$boxmap eq "light"} { |
|
set u [ansi2html::to_lightbox $u] |
|
} |
|
return "<pre class=\"punkterm\">[ansi2html::convert $u]</pre>\n" |
|
} |
|
|
|
foreach id $cmd_ids { |
|
set cmdname [string trimleft $id :] |
|
append html "<hr><h2 id=\"[ansi2html::html_escape $cmdname]\">$cmdname</h2>\n" |
|
set aliastarget [punk::args::get_idalias $id] |
|
if {$aliastarget ne ""} { |
|
append html "<p>Takes the same arguments as <b>[ansi2html::html_escape [string trimleft $aliastarget :]]</b> (shared argument definition).</p>\n" |
|
} |
|
#the default usage table shows description + all form synopses, but for |
|
#multiform commands its Arg table covers only the default form - render |
|
#an additional per-form ARG TABLE (trimmed - no repeated header panel) |
|
#for each form that has arguments (mirrors 'i -form N <cmd>'). |
|
append html [usage_block $id] |
|
set spec [punk::args::get_spec $id] |
|
set formnames [dict get $spec form_names] |
|
if {[llength $formnames] > 1} { |
|
foreach formname $formnames { |
|
set form [dict get $spec FORMS $formname] |
|
if {![dict size [dict get $form ARG_INFO]]} { |
|
#form takes no arguments - the synopsis line says it all |
|
continue |
|
} |
|
append html "<h3>form: [ansi2html::html_escape $formname]</h3>\n" |
|
append html [usage_block $id -form $formname -trim 1] |
|
} |
|
} |
|
} |
|
append html "</body></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)"
|
|
|