Compare commits
14 Commits
2157c01f6c
...
22ff5cf802
| Author | SHA1 | Date |
|---|---|---|
|
|
22ff5cf802 | 4 weeks ago |
|
|
5df5f65b4f | 4 weeks ago |
|
|
60dd6094c7 | 4 weeks ago |
|
|
c457d4222a | 4 weeks ago |
|
|
c18ca90a70 | 4 weeks ago |
|
|
c668cb85c1 | 4 weeks ago |
|
|
49ec22a928 | 4 weeks ago |
|
|
4e23a2c373 | 4 weeks ago |
|
|
6af0fd541e | 4 weeks ago |
|
|
ce5108cb09 | 4 weeks ago |
|
|
5da307ebe2 | 4 weeks ago |
|
|
06db38be46 | 4 weeks ago |
|
|
7c49fc844e | 4 weeks ago |
|
|
97ca1ceb1e | 4 weeks ago |
49 changed files with 5944 additions and 230 deletions
@ -0,0 +1,50 @@ |
|||||||
|
{ |
||||||
|
"permissions": { |
||||||
|
"allow": [ |
||||||
|
"Bash(tclsh90 src/tests/runtests.tcl *)", |
||||||
|
"Bash(tclsh src/tests/runtests.tcl *)", |
||||||
|
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 src/tests/runtests.tcl *)", |
||||||
|
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" src/tests/runtests.tcl *)", |
||||||
|
"Bash(./bin/punk91.exe src/tests/runtests.tcl *)", |
||||||
|
"Bash(tclsh src/make.tcl *)", |
||||||
|
"Bash(tclsh90 src/make.tcl *)", |
||||||
|
"Bash(tclsh87 src/make.tcl *)", |
||||||
|
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 src/make.tcl *)", |
||||||
|
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" src/make.tcl *)", |
||||||
|
"Bash(./bin/punk91.exe src/make.tcl *)", |
||||||
|
"Bash(./bin/punk902z.exe src/make.tcl *)", |
||||||
|
"Bash(./bin/punksys.exe src/make.tcl *)", |
||||||
|
"Bash(tclsh scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"Bash(tclsh90 scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"Bash(/c/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh90 scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"Bash(\"C:/Users/sleek/AppData/Local/Apps/Tcl903/bin/tclsh.exe\" scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"PowerShell(tclsh scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"PowerShell(tclsh90 scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"PowerShell(& \"C:\\Users\\sleek\\AppData\\Local\\Apps\\Tcl903\\bin\\tclsh90.exe\" scriptlib/developer/goals_lint.tcl *)", |
||||||
|
"PowerShell(tclsh src/make.tcl *)", |
||||||
|
"PowerShell(tclsh90 src/make.tcl *)", |
||||||
|
"PowerShell(& \"C:\\Users\\sleek\\AppData\\Local\\Apps\\Tcl903\\bin\\tclsh90.exe\" src/make.tcl *)", |
||||||
|
"PowerShell(& c:\\repo\\jn\\shellspy\\bin\\punk902z.exe src/make.tcl *)", |
||||||
|
"PowerShell(Get-Process *)", |
||||||
|
"PowerShell(git status *)", |
||||||
|
"PowerShell(git log *)", |
||||||
|
"PowerShell(git diff *)" |
||||||
|
] |
||||||
|
}, |
||||||
|
"hooks": { |
||||||
|
"PreToolUse": [ |
||||||
|
{ |
||||||
|
"matcher": "Bash|PowerShell", |
||||||
|
"hooks": [ |
||||||
|
{ |
||||||
|
"type": "command", |
||||||
|
"command": "cmd=$(jq -r '.tool_input.command // empty'); if printf '%s' \"$cmd\" | grep -qiE '(^|[|;&(]|\\$\\()[[:space:]]*[^|;&[:space:]]*tclsh[0-9.]*(\\.exe)?\"?[[:space:]]+(-encoding[[:space:]]+[^[:space:]]+[[:space:]]+)?-e([[:space:]]|$)'; then printf '%s' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"tclsh has no -e one-liner flag. An argument starting with - is not treated as a script file: all args land in $argv and tclsh reads commands from stdin instead (hangs forever at an interactive prompt on a console; silently ignores the -e script when stdin is piped). Write the code to a temp .tcl file and run: tclsh path/to/file.tcl - or pipe the script via stdin. See AGENTS.md User Preferences.\"}}'; fi", |
||||||
|
"shell": "bash", |
||||||
|
"timeout": 15, |
||||||
|
"statusMessage": "Checking for tclsh -e misuse" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,89 @@ |
|||||||
|
# G-076 Adopt upstream tcl9 dead-console fix: shared version gate for watchdog and help tcl warning |
||||||
|
|
||||||
|
Status: active |
||||||
|
Scope: src/modules/punk/lib-999999.0a1.0.tm (punk::lib::check), src/modules/punk/repl-999999.0a1.0.tm (repl::start watchdog arming), src/modules/punk/AGENTS.md, src/tests/modules/punk/lib/testsuites/lib/checkbugs.test |
||||||
|
Goal: the tcl9 dead-console defect (upstream ticket f10d91c2d3, root-caused in G-039) is surfaced by 'help tcl' via a has_tclbug_* check, and that check and the repl dead-console watchdog arming share a single version gate, so that runtimes at or past the first released Tcl containing the verified upstream fix neither warn nor arm the watchdog, while all earlier Tcl 9 windows runtimes keep both. |
||||||
|
Acceptance: 'help tcl' on an affected runtime shows the warning with the f10d91c2d3 link and mitigation note; the applicability decision is a pure facts-in/verdict-out classifier proc with tests (libbug_udp_threadexit_applies precedent); repl::start arms the watchdog only when the same classifier reports the runtime affected, with live-console/piped/8.6 behaviour unchanged; the fixed-in constant is set only after the G-039 kill procedure, re-run on a released runtime containing the fix with the watchdog disabled, shows the repl exiting cleanly via the script-visible eof path - until then every Tcl 9 windows runtime is treated as affected. |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
G-039 (achieved 2026-07-12, goals/archive/G-039-orphan-console-spin.md) root-caused an orphaned-shell |
||||||
|
CPU spin to a Tcl 9 core defect pair in win/tclWinConsole.c: (a) ConsoleEventProc drops the error/EOF |
||||||
|
notification for a dead console so the script-level fileevent never fires; (b) ConsoleReaderThread |
||||||
|
busy-loops on the persistent error. The script-level mitigation is repl::console_watchdog (punk::repl |
||||||
|
0.5.0). The upstream ticket https://core.tcl-lang.org/tcl/tktview/f10d91c2d3 was filed 2026-07-12 and |
||||||
|
has since been updated upstream with a fix branch in the Tcl repository (observed 2026-07-13). |
||||||
|
|
||||||
|
This goal tracks adoption of the eventual released fix: surfacing the defect through the existing |
||||||
|
'help tcl' has_tclbug_* auto-discovery, and making the watchdog version-conditional so runtimes |
||||||
|
carrying the released fix don't warn and don't arm the probe. |
||||||
|
|
||||||
|
## Approach |
||||||
|
|
||||||
|
- punk::lib::check gains a pure classifier `tclbug_console_deadspin_applies {platform tclversion}` |
||||||
|
(facts in, verdict out, testable - same pattern as libbug_udp_threadexit_applies from G-036, achieved - |
||||||
|
see goals/archive/G-036-tcl9-udp-console-worker-wedge.md) and a |
||||||
|
`has_tclbug_console_deadspin` wrapper returning the standard buginfo dict (bugref f10d91c2d3, |
||||||
|
level major, description noting punkshell's watchdog mitigation). |
||||||
|
- Detection is version-based only: a behavioural probe is impossible (it would require killing a |
||||||
|
console). The classifier compares against a `tclbug_console_deadspin_fixed_in` namespace variable; |
||||||
|
empty means "no fixed release known" and every Tcl 9 windows runtime classifies as affected. |
||||||
|
- repl::start's watchdog arming consults the same classifier as an additional AND-term, so setting |
||||||
|
the fixed-in constant once flips both the 'help tcl' warning and the watchdog together. |
||||||
|
- Error direction is safe: a trunk/dev build carrying the fix under an old-looking patchlevel merely |
||||||
|
arms a harmless 5s read-only probe. |
||||||
|
- The fixed-in flip is deliberately gated on verification, not on the upstream branch: G-039 found |
||||||
|
TWO defects, and the watchdog is only redundant if the notification defect (a) is fixed - i.e. the |
||||||
|
script actually sees EOF on console death. Before setting fixed_in, re-run the G-039 kill |
||||||
|
procedure (documented in the archived detail file) on the fixed released runtime with the watchdog |
||||||
|
disabled and confirm the repl exits via the normal fileevent/eof path. If upstream fixes only the |
||||||
|
spin, the watchdog stays necessary and the check's description changes instead. |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
- G-039 (archived) recorded the reproduction/kill procedure and the two-defect mechanism - see |
||||||
|
goals/archive/G-039-orphan-console-spin.md. Full ticket text with repro scripts: |
||||||
|
TEMP_REFERENCE/tcl9-dead-console-spin-TICKET-DRAFT.md. |
||||||
|
- 8.6 remains out of scope (different console driver; per G-039 user decision 2026-07-12). |
||||||
|
|
||||||
|
## Progress |
||||||
|
|
||||||
|
### 2026-07-13 classifier + help tcl surfacing + watchdog gating (initial increment) |
||||||
|
|
||||||
|
Landed (punk::lib 0.4.2, punk::repl 0.5.1): |
||||||
|
|
||||||
|
- punk::lib::check: `tclbug_console_deadspin_applies {platform tclversion fixed_in}` pure |
||||||
|
classifier + `has_tclbug_console_deadspin` wrapper (bugref f10d91c2d3, level major, description |
||||||
|
notes the punk::repl watchdog mitigation) + gate variable `tclbug_console_deadspin_fixed_in` |
||||||
|
(empty = all Tcl 9 windows affected). |
||||||
|
- repl::start watchdog arming gained the `[dict get [punk::lib::check::has_tclbug_console_deadspin] bug]` |
||||||
|
AND-term; console_watchdog argdoc and src/modules/punk/AGENTS.md note updated. |
||||||
|
- Tests: checkbugs.test +2 (classifier truth table incl. fixed_in comparisons; live-check/classifier |
||||||
|
consistency + bugref). |
||||||
|
|
||||||
|
Verification (2026-07-13, native tclsh 9.0.3 runtests + punk902z kit 9.0.2 in src mode): |
||||||
|
|
||||||
|
- lib checkbugs.test 5/5 pass; repl consolebackends.test 3/3 pass; `make.tcl modules` builds clean. |
||||||
|
- Live check on tclsh 9.0.3: bug=1; setting fixed_in to the running version flips bug=0. |
||||||
|
- `'help tcl' | punk902z src` (piped, PUNK_PIPE_EOF=exit): major warning displayed with |
||||||
|
description, mitigation note and f10d91c2d3 hyperlink. |
||||||
|
|
||||||
|
### 2026-07-13 mitigated/mitigation buginfo axis (punk::lib 0.4.3, punk 0.2.6) |
||||||
|
|
||||||
|
- buginfo dicts gain an optional axis orthogonal to level: `mitigated` boolean + `mitigation` |
||||||
|
text. 'help tcl' renders a triggered mitigated check as "warning level: <level> (mitigated)" |
||||||
|
in subdued grey (term-grey foreground) instead of the level colour, with an indented |
||||||
|
"mitigated: <text>" block; unmitigated warnings render unchanged. |
||||||
|
- has_tclbug_console_deadspin populates it: mitigated when punk::repl >= 0.5.0 is available to |
||||||
|
the runtime (version discovery without loading, udp-check precedent), mitigation text noting |
||||||
|
the watchdog scope (non-repl console reads remain exposed). Severity stays major. |
||||||
|
- checkbugs.test extended (axis key presence/boolean/consistency rules incl. the generic |
||||||
|
every-check sweep); 5/5 pass under native tclsh 9.0.3. Piped `'help tcl' | punk902z src` |
||||||
|
verified: grey (38;5;8 fg) mitigated rendering with mitigation block and ticket hyperlink. |
||||||
|
|
||||||
|
Remaining for acceptance: |
||||||
|
|
||||||
|
- When a released Tcl contains the merged upstream fix: re-run the G-039 kill procedure on that |
||||||
|
runtime with the watchdog disabled, confirm clean script-visible eof exit, then set |
||||||
|
`punk::lib::check::tclbug_console_deadspin_fixed_in` to that version (one edit flips both |
||||||
|
consumers) and update the AGENTS.md note. |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
# G-077 punk executable -e one-liner support: make the tclsh-agent instinct work |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: src/vfs/_config/punk_main.tcl (top-level arg dispatch), src/lib/app-punkscript/punkscript.tcl (script subcommand arg forms), src/tests/shell/testsuites/punkexe/scriptexec.test |
||||||
|
Goal: `<punkexe> -e <script> ?args...?` and `<punkexe> script -e <script> ?args...?` execute the given Tcl code as a one-liner (args after the script land in `::argv`, `::argv0` = `-e`, non-interactive), so the -e reflex agents carry over from perl/python/node works on punk kits instead of erroring - while stock-tclsh misparse behaviour (argv-swallow + stdin read) is never reproduced. |
||||||
|
Acceptance: `punksys -e {puts hi}` and `punk902z script -e {puts hi}` print hi and exit 0 with and without piped stdin present (the one-liner may itself read stdin); a -e with no following script argument is a usage error, never an interactive fall-through; error in the one-liner prints errorInfo to stderr and exits 1 (matching the script subcommand's file form); scriptexec.test covers the above against the built executable. |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
Agents and harnesses habitually try `tclsh -e "script"` (the one-liner reflex from perl/python/node). |
||||||
|
Stock tclsh has no such flag: only `-encoding name` is recognised, and any other leading-dash |
||||||
|
argument is not treated as a script file - all arguments land in `$argv` and tclsh reads commands |
||||||
|
from stdin instead (hangs at an interactive prompt on a console; silently ignores the supposed |
||||||
|
one-liner when stdin is piped). Root `AGENTS.md` User Preferences documents the pitfall and a |
||||||
|
claude-harness PreToolUse hook (`.claude/settings.json`) denies such invocations with corrective |
||||||
|
guidance. |
||||||
|
|
||||||
|
The punk kits currently fail fast instead (`punk script: script file not found: '-e'`) but offer |
||||||
|
no one-liner form. This goal makes the instinct work on punk executables, removing the failure |
||||||
|
class for the kits entirely. app-punkscript's existing stdin form already provides the execution |
||||||
|
scaffolding (argv0/argv setup, errorInfo-to-stderr, exit-code discipline). |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
# G-078 punk::ansi ANSI->HTML rendering api (developer-surfaced, documented) |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: src/modules/punk/ansi-999999.0a1.0.tm (or a new punk::ansi::html module), src/scriptapps/tools/punkargs_punknative.tcl + scratch prototypes (ansi2html_core.tcl, ansicat_gallery.tcl) as reference implementations to be retired into consumers, src/scriptapps/tools/fontprep/ (asset + geometry contract), new testsuite under src/tests/modules/punk/ansi/ |
||||||
|
Goal: a documented public api converting ANSI/SGR text to html: a fragment converter (spans for a `<pre>`) plus a page assembler with `-assets embed|link|none` (punkdoc-mono data-uri / relative file / stack-only), `-palette vga|campbell|vscode|<list>` (16-colour table - classic art needs VGA where SGR 33 is brown), art mode (bold-as-bright for basic colours, no font-weight, solid-block background sealing, tight pitch + scaleY aspect correction per the settled recipe) and doc mode (text-oriented pitch), and a headless flatten entry for files/captures (ansicat semantics - cursor movement/wrap/SAUCE/cp437 - without repl/console dependencies). |
||||||
|
Acceptance: renders the prototype corpus (src/testansi gallery samples incl. octants, textblock::periodic, punk::args::usage tables) with parity to the settled recipe; runs headless in a plain tclsh with declared package deps only (no punk::console load, no fcat/global-alias assumptions); covered by tests incl. SGR state-machine cases (reset/bold/dim/italic/underline/reverse, 16/256/truecolour, bold-as-bright) and an artifact pin of a small converted sample; the scriptapps prototypes become thin consumers of the api. |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
Prototyped 2026-07-13/14 (scratch/argdoc2man-trial-2026-07-13). The settled empirical |
||||||
|
recipe and its rejected alternatives (larger row pitches, font line-metric |
||||||
|
normalization - wrong-coloured bleed bars) are recorded in |
||||||
|
src/scriptapps/tools/fontprep/README.md and the prototype comments; cell geometry |
||||||
|
contract: punkdoc-mono advance exactly 0.5859375em (integer 9px cells at |
||||||
|
font-size 15.36px), blocks paint the 1.3213em win cell, art aspect restored via |
||||||
|
transform:scaleY(1.3212890625) over a line-height:1.0 layout. |
||||||
|
This api is also the docs-screenshot enabler (ansicat/textblock output embedded in |
||||||
|
generated documentation) identified in the punk::args-as-doc-source-of-truth |
||||||
|
assessment. Related: G-079 (default-colour semantics), G-080 (deterministic |
||||||
|
cell-grid mode), G-081 (docs build pipeline consumer). |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
# G-079 default-colour semantics through renderspace/flattened output |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: src/modules/overtype-999999.0a1.0.tm (renderspace/renderline replay-code emission), punk::ansi flatten consumers (ansicat), ansi2html consumer mapping (G-078) |
||||||
|
Goal: flattened/rendered ANSI output preserves "default foreground/background" as a semantic value (SGR 39/49 or absence) instead of baking in explicit colours (replay/overlay output currently emits literal `0;40` - black background - for cells whose background was never set). Consumers can then map defaults to their own backdrop: html pages theme-match any page background, terminal replays keep the user's colour scheme. |
||||||
|
Acceptance: ansicat/usage flattened output for content that never sets a background contains no explicit SGR 40/black-bg codes for those cells; the punk-native html pages render example-helper top/bottom bars (e.g. grepstr @cmd -help examples) blending with a NON-black page background with no contrasting bars; existing terminal display output is visually unchanged (regression-checked against current renderings). |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
Found 2026-07-14 while reviewing the punk::ansi punknative doc page: example blocks |
||||||
|
produced by the punk::ansi example helper show bars whose "background half" rendered |
||||||
|
as black boxes contrasting with a grey page. Probe evidence: renderspace overlay |
||||||
|
replay emits `replay_codes_overlay: ESC[0;40m`. Interim workaround (shipped in the |
||||||
|
punknative generator): the `<pre>` backdrop is hard black so baked-black blends - |
||||||
|
this constrains doc page theming and leaves lighter antialiasing seams at half-block |
||||||
|
edges against non-matching content. Proper fix is upstream in the renderer's |
||||||
|
replay-code model, then a `-defaultbg`/`-defaultfg` mapping option in the G-078 api. |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
# G-080 deterministic cell-grid html rendering mode (seamless block art) |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: the G-078 punk::ansi html api (additional rendering mode); scratch prototypes as baseline |
||||||
|
Goal: an optional rendering mode that emulates the terminal cell grid explicitly - each styled run emitted as inline-block with exact integer-px cell width/height, inner line-height equal to the cell, overflow hidden, vertical-align top (newlines outside spans) - making row/column geometry fully deterministic instead of dependent on font metrics, inline-background box heights and sub-pixel glyph rasterization. Optionally decompose half-block cells (U+2580/2584/258C/2590 and quadrants) into sized background rectangles for complete seam-proofing. |
||||||
|
Acceptance: the testansi gallery samples show no vertical or horizontal seams in chrome and firefox at 100%/125%/150% zoom (the residual shade/half-block seams accepted in the span-mode recipe are eliminated); cost (page size, loss of text selectability granularity if any) measured and documented; span mode remains the default with cell-grid selectable per block. |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
The 2026-07-14 seam investigation (scratch/argdoc2man-trial-2026-07-13/seam_compare.tcl) |
||||||
|
settled a span-mode recipe (integer 9px advance, tight pitch + scaleY aspect, solid-block |
||||||
|
background sealing, integer 17px doc pitch) that reduces but cannot eliminate seams: |
||||||
|
shade (U+2591-2593) and half-block glyph edges still antialias against differing |
||||||
|
neighbours at fractional device-pixel positions (display scaling), and inline background |
||||||
|
box height is a font-metric browsers don't agree on (metric normalization was tried and |
||||||
|
rejected - see fontprep/README.md). Cell-grid emulation removes every one of those |
||||||
|
degrees of freedom at the cost of heavier markup. Only worth building if/when the |
||||||
|
accepted residual seams stop passing review - the glance test passes today. |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
# G-081 punk::args documentation build pipeline (dev doc.* integration) |
||||||
|
|
||||||
|
Status: proposed |
||||||
|
Scope: src/modules/punk/mix/commandset/doc-999999.0a1.0.tm (doc.* commandset - new generation subcommands), src/scriptapps/tools/punkargs_to_doctools.tcl + punkargs_punknative.tcl (prototypes to productize), src/scriptapps/tools/fontprep/ (shared doc-set asset in link mode), src/doc tree conventions, src/tests coverage |
||||||
|
Goal: 'dev doc.*' generates the external documentation set from punk::args definitions as the source of truth, per package, against the DEV modules (src/modules 999999 versions - not bootsupport snapshots): (a) doctools .man output (Tcl-standard synopsis conventions) validated via dev doc.validate/dtplite and feeding the existing kettle machinery; (b) punk-native html via the G-078 api with a shared punkdoc-mono.woff2 + css (assets link mode, one cached font per doc set) and (c) GFM markdown. Includes the productization items the prototypes deferred: per-form arg tables without repeated panels (prototype has a trim heuristic - production should render per-form tables directly from the spec), deeper-namespace/ensemble command pages (e.g. punk::ansi::ansistring - the prototypes' one-level namespace filter skips them), module/package-level info at page top once G-075 surfaces @package ids, large choice sets as structured lists in markdown, and doctools emitter gaps (@examples -> [example], @seealso/@doc -> [see_also]/[uri], choicelabels). |
||||||
|
Acceptance: a doc.* subcommand generates all three formats for a nominated package list from dev modules in one run; doctools output validates clean (dtplite) and html pages carry the shared font/css by reference; deeper-namespace registered ids appear on the owning package's page (or their own); output tree location and vcs policy decided and documented (generated-vs-committed); at least punk::path + punk::ansi + one ensemble-rich package covered by a regression pin. |
||||||
|
|
||||||
|
## Context |
||||||
|
|
||||||
|
Follows the 2026-07-13 assessment (punk::args -> doctools expressibility) and trials: |
||||||
|
punkargs_to_doctools.tcl (doctools, Tcl-standard by decision - punk s-style is out of |
||||||
|
scope for that target) and punkargs_punknative.tcl (terminal-faithful html + markdown) |
||||||
|
both work per-package against bootsupport modules, invoked manually. The doctools |
||||||
|
comment blocks in source remain interim until this pipeline is validated, after which |
||||||
|
worthwhile prose migrates into punk::args blocks (original long-term plan). dtplite |
||||||
|
validation infrastructure (bin/dtplite.cmd + dev doc.validate) is in place. |
||||||
|
Related: G-075 (@package ids), G-078 (html api), G-079 (default colours), G-014/G-068 |
||||||
|
(argdoc/moduledoc workflows). |
||||||
@ -1,4 +1,4 @@ |
|||||||
[project] |
[project] |
||||||
name = "punkshell" |
name = "punkshell" |
||||||
version = "0.12.20" |
version = "0.12.23" |
||||||
license = "BSD-2-Clause" |
license = "BSD-2-Clause" |
||||||
|
|||||||
@ -1,27 +1,42 @@ |
|||||||
# -*- tcl -*- |
# -*- tcl -*- |
||||||
|
|
||||||
# @@ Meta Begin |
# @@ Meta Begin |
||||||
# Application dtplite 1.0.5 |
# Application dtplite 1.0.5 |
||||||
# Meta platform tcl |
# Meta platform tcl |
||||||
# Meta summary Lightweight DocTools Processor |
# Meta summary Lightweight DocTools Processor |
||||||
# Meta description This application is a simple processor |
# Meta description This application is a simple processor |
||||||
# Meta description for documents written in the doctools |
# Meta description for documents written in the doctools |
||||||
# Meta description markup language. It covers the most |
# Meta description markup language. It covers the most |
||||||
# Meta description common use cases, but is not as |
# Meta description common use cases, but is not as |
||||||
# Meta description configurable as its big brother dtp. |
# Meta description configurable as its big brother dtp. |
||||||
# Meta category Processing doctools documents |
# Meta category Processing doctools documents |
||||||
# Meta subject doctools doctoc docidx |
# Meta subject doctools doctoc docidx |
||||||
# Meta require {dtplite 1.0.5} |
# Meta require {dtplite 1.0.5} |
||||||
# Meta author Andreas Kupries |
# Meta author Andreas Kupries |
||||||
# Meta license BSD |
# Meta license BSD |
||||||
# @@ Meta End |
# @@ Meta End |
||||||
|
|
||||||
package require dtplite 1.0.5 |
if {[catch {package require dtplite 1.0.5}]} { |
||||||
|
# Fall back to the project-vendored tcllib (pure Tcl) when the invoking |
||||||
# dtp lite - Lightweight DocTools Processor |
# tclsh has no tcllib installed. Works both unwrapped (src/scriptapps/dtplite.tcl) |
||||||
# ======== = ============================== |
# and wrapped (<projectroot>/bin/dtplite.cmd) - each is one level below a folder |
||||||
|
# containing (or sibling to) src/vendorlib_tcl9/<arch>/tcllib<ver> |
||||||
exit [dtplite::do $argv] |
set selfdir [file dirname [file dirname [file normalize [info script]/__]]] |
||||||
|
foreach libdir [glob -nocomplain -types d -directory $selfdir\ |
||||||
# ### ### ### ######### ######### ######### |
../vendorlib_tcl9/*/tcllib*\ |
||||||
exit |
../src/vendorlib_tcl9/*/tcllib*] { |
||||||
|
set libdir [file normalize $libdir] |
||||||
|
if {$libdir ni $::auto_path} { |
||||||
|
lappend ::auto_path $libdir |
||||||
|
} |
||||||
|
} |
||||||
|
package require dtplite 1.0.5 |
||||||
|
} |
||||||
|
|
||||||
|
# dtp lite - Lightweight DocTools Processor |
||||||
|
# ======== = ============================== |
||||||
|
|
||||||
|
exit [dtplite::do $argv] |
||||||
|
|
||||||
|
# ### ### ### ######### ######### ######### |
||||||
|
exit |
||||||
|
|||||||
@ -0,0 +1,15 @@ |
|||||||
|
[application] |
||||||
|
template="punk.multishell.cmd" |
||||||
|
as_admin=false |
||||||
|
|
||||||
|
scripts=[ |
||||||
|
"dtplite.tcl", |
||||||
|
] |
||||||
|
|
||||||
|
default_outputfile="dtplite.cmd" |
||||||
|
default_nextshellpath="/usr/bin/env tclsh" |
||||||
|
default_nextshelltype="tcl" |
||||||
|
|
||||||
|
win32.nextshellpath="tclsh" |
||||||
|
win32.nextshelltype="tcl" |
||||||
|
win32.outputfile="dtplite.cmd" |
||||||
@ -0,0 +1,94 @@ |
|||||||
|
Copyright (c) 2019 - Present, Microsoft Corporation, |
||||||
|
with Reserved Font Name Cascadia Code. |
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1. |
||||||
|
This license is copied below, and is also available with a FAQ at: |
||||||
|
http://scripts.sil.org/OFL |
||||||
|
|
||||||
|
|
||||||
|
----------------------------------------------------------- |
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 |
||||||
|
----------------------------------------------------------- |
||||||
|
|
||||||
|
PREAMBLE |
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide |
||||||
|
development of collaborative font projects, to support the font creation |
||||||
|
efforts of academic and linguistic communities, and to provide a free and |
||||||
|
open framework in which fonts may be shared and improved in partnership |
||||||
|
with others. |
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and |
||||||
|
redistributed freely as long as they are not sold by themselves. The |
||||||
|
fonts, including any derivative works, can be bundled, embedded, |
||||||
|
redistributed and/or sold with any software provided that any reserved |
||||||
|
names are not used by derivative works. The fonts and derivatives, |
||||||
|
however, cannot be released under any other type of license. The |
||||||
|
requirement for fonts to remain under this license does not apply |
||||||
|
to any document created using the fonts or their derivatives. |
||||||
|
|
||||||
|
DEFINITIONS |
||||||
|
"Font Software" refers to the set of files released by the Copyright |
||||||
|
Holder(s) under this license and clearly marked as such. This may |
||||||
|
include source files, build scripts and documentation. |
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the |
||||||
|
copyright statement(s). |
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as |
||||||
|
distributed by the Copyright Holder(s). |
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting, |
||||||
|
or substituting -- in part or in whole -- any of the components of the |
||||||
|
Original Version, by changing formats or by porting the Font Software to a |
||||||
|
new environment. |
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical |
||||||
|
writer or other person who contributed to the Font Software. |
||||||
|
|
||||||
|
PERMISSION & CONDITIONS |
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify, |
||||||
|
redistribute, and sell modified and unmodified copies of the Font |
||||||
|
Software, subject to the following conditions: |
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components, |
||||||
|
in Original or Modified Versions, may be sold by itself. |
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled, |
||||||
|
redistributed and/or sold with any software, provided that each copy |
||||||
|
contains the above copyright notice and this license. These can be |
||||||
|
included either as stand-alone text files, human-readable headers or |
||||||
|
in the appropriate machine-readable metadata fields within text or |
||||||
|
binary files as long as those fields can be easily viewed by the user. |
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font |
||||||
|
Name(s) unless explicit written permission is granted by the corresponding |
||||||
|
Copyright Holder. This restriction only applies to the primary font name as |
||||||
|
presented to the users. |
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font |
||||||
|
Software shall not be used to promote, endorse or advertise any |
||||||
|
Modified Version, except to acknowledge the contribution(s) of the |
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written |
||||||
|
permission. |
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole, |
||||||
|
must be distributed entirely under this license, and must not be |
||||||
|
distributed under any other license. The requirement for fonts to |
||||||
|
remain under this license does not apply to any document created |
||||||
|
using the Font Software. |
||||||
|
|
||||||
|
TERMINATION |
||||||
|
This license becomes null and void if any of the above conditions are |
||||||
|
not met. |
||||||
|
|
||||||
|
DISCLAIMER |
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT |
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE |
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL |
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM |
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE. |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
# fontprep - punkdoc-mono web font for the punk-native documentation pipeline |
||||||
|
|
||||||
|
`punkdoc-mono.woff2` is the pinned monospace web font consumed by the |
||||||
|
punk-native html doc generation (`../punkargs_punknative.tcl` and its |
||||||
|
successors). It exists because the ansi->html rendering of punk::args usage |
||||||
|
tables, textblock output and cp437/ansi art (src/testansi) needs glyphs - |
||||||
|
Unicode 16 octants (U+1CC00-1CEBF) and Symbols for Legacy Computing |
||||||
|
(U+1FB00-1FBFF) in particular - that are absent from the monospace fonts |
||||||
|
commonly installed on reader machines. Coverage was measured empirically: |
||||||
|
see the glyph inventory probe notes below. |
||||||
|
|
||||||
|
## What it is |
||||||
|
|
||||||
|
A glyph subset of **Cascadia Mono v2404.23** (variable font, `wght` axis |
||||||
|
200-700 retained so browsers render true bold from this single file), |
||||||
|
renamed to **punkdoc-mono**. Line metrics are STOCK (hhea/typo ascent 1900 / |
||||||
|
descent 480; win ascent 2226 / descent 480). |
||||||
|
|
||||||
|
Cell geometry for css authors: advance = 0.5859375 em exactly (font-size |
||||||
|
15.36px -> integer 9px cell width - integer cell widths avoid vertical seams |
||||||
|
at styled-span boundaries). Block-element glyphs paint -480..2226 units = |
||||||
|
1.3212890625 em (they fill the WINDOWS cell, which is the conhost/windows |
||||||
|
terminal cell height). NOTE - rejected experiment (2026-07-14, see the |
||||||
|
commented-out normalize_line_metrics in make_punkdoc_mono.py): setting |
||||||
|
hhea/typo equal to the win metrics makes browsers' inline content area |
||||||
|
1.3213em, and at row pitches below that every span's BACKGROUND bleeds ~2.5px |
||||||
|
into neighbouring rows (wrong-coloured bars under solid blocks). Empirically |
||||||
|
the best block-art rendering is stock metrics with a tight row pitch |
||||||
|
(line-height:1.0) - optionally aspect-corrected with a transform:scaleY of |
||||||
|
1.3212890625 applied to the whole rendering. |
||||||
|
|
||||||
|
- Renaming is REQUIRED, not cosmetic: Cascadia's SIL OFL 1.1 licence declares |
||||||
|
the Reserved Font Name "Cascadia Code", and a modified version (subsetting |
||||||
|
is modification) must not use it. `punkdoc-mono` avoids the Cascadia name |
||||||
|
entirely. The font's embedded name table carries the original Microsoft |
||||||
|
copyright (nameID 0, preserved verbatim as the OFL requires), the OFL |
||||||
|
licence statement/url (nameID 13/14), and a provenance description |
||||||
|
(nameID 10). The original trademark record was removed (a renamed copy must |
||||||
|
not assert Microsoft's trademark). |
||||||
|
- Licence: SIL OFL 1.1 - full text in `LICENSE-cascadia-OFL.txt` (fetched from |
||||||
|
the v2404.23 source tag). The OFL applies to the font only; it places no |
||||||
|
conditions on punkshell (BSD) or on documents rendered with the font. |
||||||
|
Track in the project licence inventory (G-063). |
||||||
|
- No italic face is included: browsers synthesize oblique for italic spans |
||||||
|
(advance widths are preserved, so terminal-grid alignment holds). |
||||||
|
|
||||||
|
## Provenance (reproducibility) |
||||||
|
|
||||||
|
- Source: https://github.com/microsoft/cascadia-code/releases/download/v2404.23/CascadiaCode-2404.23.zip |
||||||
|
- release zip sha256: A911410626C0E09D03FA3FDDA827188FDA96607DF50FECC3C5FEE5906E33251B |
||||||
|
- input file within zip: ttf/CascadiaMono.ttf (variable) |
||||||
|
- input ttf sha256: 056F33BA2D9DA954750320EC6ECFB7D83CFB5F93AD593722170CD4498F5D4E95 |
||||||
|
- Tooling: python fonttools 4.63.0 + brotli (one-time prep only - the doc |
||||||
|
BUILD never invokes python; it just reads/copies/base64s the .woff2) |
||||||
|
- Command: |
||||||
|
`python make_punkdoc_mono.py CascadiaMono.ttf punkdoc-unicodes.txt <outdir>` |
||||||
|
- Output punkdoc-mono.woff2 sha256: EE068E60E0BE6D96FC97D79123DE3B1A9A35205136A4A5C97648BCA2D9A24DB6 |
||||||
|
(1561 cmap codepoints, 4319 glyphs, 79744 bytes) |
||||||
|
Builds are byte-reproducible: the prep script loads with |
||||||
|
recalcTimestamp=False so head.modified stays that of the source font |
||||||
|
(verified: two consecutive builds hash identically). |
||||||
|
|
||||||
|
## Subset ranges |
||||||
|
|
||||||
|
`punkdoc-unicodes.txt` - whole blocks (with headroom) covering the demand |
||||||
|
measured 2026-07-13 by the glyph inventory probe (scratch trial folder |
||||||
|
argdoc2man-trial-2026-07-13: glyph_inventory.tcl over src/testansi decoded as |
||||||
|
ansicat does + textblock::periodic + all textblock frametypes + |
||||||
|
punk::args::usage = 160 unique non-ascii codepoints), plus the full CP437 |
||||||
|
repertoire and braille (U+2800-28FF) for future plot rendering. |
||||||
|
Verified: the subset covers 157/160 of the measured inventory - identical to |
||||||
|
the full source font. The 3 misses are deliberate non-goals: em-space U+2003 |
||||||
|
(converters should normalize exotic spaces to plain spaces - terminals treat |
||||||
|
them as one cell) and fullwidth forms U+FF30/FF31 (inherently double-width; |
||||||
|
CJK-font fallback territory). |
||||||
|
|
||||||
|
## Regenerating (when the glyph domain grows) |
||||||
|
|
||||||
|
1. Re-run the glyph inventory probe over the current content; if new |
||||||
|
codepoints fall outside `punkdoc-unicodes.txt`, widen the ranges. |
||||||
|
2. `pip install fonttools brotli` (any machine; a venv is fine) |
||||||
|
3. Fetch the pinned Cascadia release (or a newer one - update shas here) |
||||||
|
4. Run the command above; verify coverage (e.g. via |
||||||
|
Windows.Media.GlyphTypeface against the inventory - see font_coverage.ps1 |
||||||
|
in the trial scratch folder) and that name records contain no "Cascadia" |
||||||
|
outside the nameID 10 provenance credit. |
||||||
|
5. Commit the new .woff2 together with updated shas in this README. |
||||||
@ -0,0 +1,145 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
"""make_punkdoc_mono.py - one-time font-prep for the punkshell doc pipeline. |
||||||
|
|
||||||
|
Subsets the Cascadia Mono VARIABLE font (weight axis kept, so browsers get |
||||||
|
true bold from one file) to the punkdoc unicode ranges, renames the family to |
||||||
|
'punkdoc-mono' (REQUIRED: Cascadia's OFL declares Reserved Font Name |
||||||
|
'Cascadia Code' - a modified version must not use the reserved name), and |
||||||
|
writes woff2 (for the web) + ttf (for cmap verification tooling). |
||||||
|
|
||||||
|
usage: python make_punkdoc_mono.py <CascadiaMono.ttf> <unicodes.txt> <outdir> |
||||||
|
""" |
||||||
|
import sys |
||||||
|
import os |
||||||
|
from fontTools import version as fonttools_version |
||||||
|
from fontTools.ttLib import TTFont |
||||||
|
from fontTools.subset import Subsetter, Options |
||||||
|
|
||||||
|
SRC_FAMILY_NAMES = ["Cascadia Mono", "CascadiaMono", "Cascadia Code", "CascadiaCode"] |
||||||
|
NEW_FAMILY = "punkdoc-mono" |
||||||
|
|
||||||
|
def parse_unicodes(path): |
||||||
|
cps = set() |
||||||
|
with open(path, encoding="utf-8") as f: |
||||||
|
for line in f: |
||||||
|
line = line.strip() |
||||||
|
if not line or line.startswith("#"): |
||||||
|
continue |
||||||
|
if "-" in line: |
||||||
|
lo, hi = line.split("-") |
||||||
|
cps.update(range(int(lo, 16), int(hi, 16) + 1)) |
||||||
|
else: |
||||||
|
cps.add(int(line, 16)) |
||||||
|
return cps |
||||||
|
|
||||||
|
# name records that carry the FAMILY NAME and must be renamed (OFL Reserved |
||||||
|
# Font Name requirement). Copyright (0), designer/vendor credits (8,9,11,12) |
||||||
|
# are preserved untouched - the OFL requires the copyright notice be retained. |
||||||
|
FAMILY_NAME_IDS = (1, 3, 4, 6, 16, 17, 21, 22, 25) |
||||||
|
OFL_ID13 = ("This Font Software is licensed under the SIL Open Font License, " |
||||||
|
"Version 1.1.") |
||||||
|
PROVENANCE_ID10 = ("punkdoc-mono is a glyph subset of Cascadia Mono v2404.23 " |
||||||
|
"(c) Microsoft Corporation, renamed as required by the SIL " |
||||||
|
"OFL 1.1 Reserved Font Name condition. Prepared for the " |
||||||
|
"punkshell documentation pipeline.") |
||||||
|
|
||||||
|
def rename(font): |
||||||
|
name = font["name"] |
||||||
|
keep = [] |
||||||
|
for rec in name.names: |
||||||
|
# drop the trademark record entirely: the original text is about the |
||||||
|
# Cascadia name (which this font no longer uses) and a renamed copy |
||||||
|
# must not assert Microsoft's trademark over the new name |
||||||
|
if rec.nameID == 7: |
||||||
|
continue |
||||||
|
if rec.nameID in FAMILY_NAME_IDS: |
||||||
|
s = rec.toUnicode() |
||||||
|
for old in SRC_FAMILY_NAMES: |
||||||
|
s = s.replace(old, NEW_FAMILY) |
||||||
|
s = s.replace(old.replace(" ", ""), NEW_FAMILY) |
||||||
|
if rec.nameID == 6: |
||||||
|
s = s.replace(" ", "") |
||||||
|
rec.string = s |
||||||
|
elif rec.nameID == 13: |
||||||
|
rec.string = OFL_ID13 |
||||||
|
keep.append(rec) |
||||||
|
name.names = keep |
||||||
|
# embedded provenance/description (mentioning the source font as credit is |
||||||
|
# permitted - the RFN restriction is on naming the font, not attribution) |
||||||
|
for platformID, platEncID, langID in ((3, 1, 0x409), (1, 0, 0)): |
||||||
|
name.setName(PROVENANCE_ID10, 10, platformID, platEncID, langID) |
||||||
|
|
||||||
|
def normalize_line_metrics(font): |
||||||
|
"""Make every line-metric set equal the WINDOWS metrics. |
||||||
|
|
||||||
|
Cascadia draws its block-element glyphs to fill the windows cell |
||||||
|
(usWinAscent + usWinDescent = 2706/2048 = 1.3213em) - that box IS the |
||||||
|
terminal cell. But hhea/typo metrics are smaller (2380 units), and |
||||||
|
browsers differ in which set they use for the inline content area, so |
||||||
|
inline backgrounds and 'line-height: normal' vary per browser and can |
||||||
|
leave uncovered bands between rows of block art. |
||||||
|
Setting hhea == typo == win (and the USE_TYPO_METRICS flag) makes the |
||||||
|
content area equal the block paint extent everywhere: with css |
||||||
|
line-height <= 1.3213em, adjacent rows' inline backgrounds and block |
||||||
|
glyphs overlap slightly and horizontal seams become impossible. |
||||||
|
""" |
||||||
|
hhea = font["hhea"] |
||||||
|
os2 = font["OS/2"] |
||||||
|
win_asc = os2.usWinAscent |
||||||
|
win_desc = os2.usWinDescent |
||||||
|
hhea.ascent = win_asc |
||||||
|
hhea.descent = -win_desc |
||||||
|
hhea.lineGap = 0 |
||||||
|
os2.sTypoAscender = win_asc |
||||||
|
os2.sTypoDescender = -win_desc |
||||||
|
os2.sTypoLineGap = 0 |
||||||
|
os2.fsSelection |= 0x80 # USE_TYPO_METRICS |
||||||
|
print(f"line metrics normalized to win: ascent {win_asc} descent {win_desc} " |
||||||
|
f"(cell {(win_asc+win_desc)/font['head'].unitsPerEm:.6f} em)") |
||||||
|
|
||||||
|
def main(): |
||||||
|
src, unifile, outdir = sys.argv[1], sys.argv[2], sys.argv[3] |
||||||
|
os.makedirs(outdir, exist_ok=True) |
||||||
|
cps = parse_unicodes(unifile) |
||||||
|
print(f"fonttools {fonttools_version}; requested codepoints: {len(cps)}") |
||||||
|
|
||||||
|
#recalcTimestamp=False keeps the source font's head.modified so rebuilds |
||||||
|
#from the same input are byte-reproducible |
||||||
|
font = TTFont(src, recalcTimestamp=False) |
||||||
|
opts = Options() |
||||||
|
opts.layout_features = ["*"] # keep default layout behaviour |
||||||
|
opts.name_IDs = ["*"] # keep all name records (license notice ids too) |
||||||
|
opts.name_legacy = True |
||||||
|
opts.name_languages = ["*"] |
||||||
|
opts.notdef_outline = True |
||||||
|
opts.recalc_bounds = True |
||||||
|
opts.recalc_average_width = True |
||||||
|
opts.prune_unicode_ranges = True |
||||||
|
subsetter = Subsetter(options=opts) |
||||||
|
subsetter.populate(unicodes=cps) |
||||||
|
subsetter.subset(font) |
||||||
|
|
||||||
|
covered = set() |
||||||
|
for table in font["cmap"].tables: |
||||||
|
covered.update(table.cmap.keys()) |
||||||
|
print(f"glyphs in subset: {font['maxp'].numGlyphs}; cmap codepoints: {len(covered)}") |
||||||
|
|
||||||
|
rename(font) |
||||||
|
# normalize_line_metrics(font) # REJECTED EXPERIMENT (2026-07-14): making |
||||||
|
# hhea/typo equal the win metrics grows the browsers' inline content area |
||||||
|
# to 1.3213em, so at row pitches below that every span's background bleeds |
||||||
|
# ~2.5px into the neighbouring rows - visible as wrong-coloured bars at |
||||||
|
# the bottom of solid blocks. Stock metrics + squashed pitch (+ optional |
||||||
|
# scaleY for aspect) rendered best in practice. Kept for reference. |
||||||
|
|
||||||
|
ttf_path = os.path.join(outdir, "punkdoc-mono.ttf") |
||||||
|
font.save(ttf_path) |
||||||
|
print(f"wrote {ttf_path} ({os.path.getsize(ttf_path)} bytes)") |
||||||
|
|
||||||
|
font.flavor = "woff2" |
||||||
|
woff2_path = os.path.join(outdir, "punkdoc-mono.woff2") |
||||||
|
font.save(woff2_path) |
||||||
|
print(f"wrote {woff2_path} ({os.path.getsize(woff2_path)} bytes)") |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
Binary file not shown.
@ -0,0 +1,21 @@ |
|||||||
|
# punkdoc-mono subset ranges - whole blocks covering (with headroom) the |
||||||
|
# glyph demand measured by glyph_inventory.tcl over src/testansi + |
||||||
|
# textblock::periodic/frames + punk::args::usage (2026-07-13, 160 codepoints) |
||||||
|
# plus the full CP437 repertoire and braille for future plot rendering. |
||||||
|
0020-007E |
||||||
|
00A0-00FF |
||||||
|
0180-024F |
||||||
|
0370-03FF |
||||||
|
2000-206F |
||||||
|
2070-209F |
||||||
|
20A0-20CF |
||||||
|
2190-21FF |
||||||
|
2200-22FF |
||||||
|
2300-23FF |
||||||
|
2400-243F |
||||||
|
2500-25FF |
||||||
|
2600-26FF |
||||||
|
2700-27BF |
||||||
|
2800-28FF |
||||||
|
1CC00-1CEBF |
||||||
|
1FB00-1FBFF |
||||||
@ -0,0 +1,556 @@ |
|||||||
|
# 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)" |
||||||
@ -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 <package> <outfile.man> |
||||||
|
# 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, <type> 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 <projectroot>/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 <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::ansi |
||||||
|
|
||||||
|
lassign $argv pkgname outfile |
||||||
|
if {$pkgname eq "" || $outfile eq ""} { |
||||||
|
puts stderr "usage: punkargs_to_doctools.tcl <package> <outfile.man>" |
||||||
|
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)" |
||||||
@ -0,0 +1,195 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
package require punk::args |
||||||
|
|
||||||
|
#G-074: punk::args::formcheck - on-demand multiform ambiguity analysis with |
||||||
|
#sanctioned-overlap annotation (@form -overlapallowed). |
||||||
|
#A finding is only reported after a synthetic witness arglist is CONFIRMED by a real |
||||||
|
#single-form parse against both forms, so fully discriminated form pairs cannot |
||||||
|
#false-alarm; witness derivation is the documented miss direction (exotic types, |
||||||
|
#forms requiring options, capped -multiple repetition). The sanction is |
||||||
|
#formcheck-reporting metadata only - parse behaviour (multipleformmatches) is |
||||||
|
#unaffected. The tclcore ::lseq and ::after models are the motivating real cases |
||||||
|
#from the G-041 closeout (goals/archive/G-041-punkargs-form-matching.md). |
||||||
|
|
||||||
|
namespace eval ::testspace { |
||||||
|
namespace import ::tcltest::* |
||||||
|
variable common { |
||||||
|
set result "" |
||||||
|
} |
||||||
|
|
||||||
|
testConstraint have_tclcoredocs [expr {![catch {package require punk::args::moduledoc::tclcore}]}] |
||||||
|
|
||||||
|
#discriminated multiform fixtures - literal/choice/int leading slots (no permissive |
||||||
|
#alignment) and arity-window separation. formcheck must report NOTHING for these. |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::fc_afterish |
||||||
|
@cmd -name testspace::fc_afterish -summary "after-like multiform" -help "discriminated multiform fixture" |
||||||
|
@form -form ms |
||||||
|
@values -min 1 -max -1 |
||||||
|
ms -type int |
||||||
|
script -type string -optional 1 -multiple 1 |
||||||
|
@form -form cancel |
||||||
|
@values -min 2 -max -1 |
||||||
|
cancel -type literal(cancel) |
||||||
|
ids -type string -multiple 1 |
||||||
|
@form -form idle |
||||||
|
@values -min 2 -max -1 |
||||||
|
idle -type literal(idle) |
||||||
|
scripts -type string -multiple 1 |
||||||
|
} |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::fc_sharedform |
||||||
|
@cmd -name testspace::fc_sharedform -summary "shared-prologue multiform" -help "arity-discriminated multiform fixture" |
||||||
|
@form -form {get set} |
||||||
|
@leaders -min 1 -max 1 |
||||||
|
key -type string |
||||||
|
@form -form get |
||||||
|
@values -min 0 -max 0 |
||||||
|
@form -form set |
||||||
|
@values -min 1 -max 1 |
||||||
|
newvalue -type string |
||||||
|
} |
||||||
|
|
||||||
|
#overlapping fixture: aa's typed slot faces bb's permissive slot (type-weakness), |
||||||
|
#with the pair sanctioned on ONE member via @form -overlapallowed |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::fc_sanctioned |
||||||
|
@cmd -name testspace::fc_sanctioned -summary "sanctioned overlap" -help "sanctioned overlapping multiform fixture" |
||||||
|
@form -form go -overlapallowed {run} |
||||||
|
@values -min 2 -max 2 |
||||||
|
go -type literal(go) |
||||||
|
target -type string |
||||||
|
@form -form run |
||||||
|
@values -min 2 -max 2 |
||||||
|
mode -type any |
||||||
|
target -type string |
||||||
|
} |
||||||
|
|
||||||
|
test formcheck_discriminated_no_findings {formcheck reports nothing for fully discriminated multiform definitions (fixtures + the parse withid/withdef pair)}\ |
||||||
|
-setup $common -body { |
||||||
|
foreach id {::testspace::fc_afterish ::testspace::fc_sharedform ::punk::args::parse} { |
||||||
|
set r [punk::args::formcheck $id] |
||||||
|
lappend result [list [dict size [dict get $r findings]] [dict get $r unsanctioned]] |
||||||
|
} |
||||||
|
set result |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list {0 {}} {0 {}} {0 {}}] |
||||||
|
|
||||||
|
test formcheck_finding_classes_and_sanction {an overlap is classified (type-weakness = discriminator vs permissive) and -overlapallowed on one member downgrades the pair to sanctioned}\ |
||||||
|
-setup $common -body { |
||||||
|
set r [punk::args::formcheck ::testspace::fc_sanctioned] |
||||||
|
set finding [dict get $r findings {go run}] |
||||||
|
lappend result [dict get $finding class] |
||||||
|
lappend result [dict get $finding sanctioned] |
||||||
|
lappend result [dict get $r unsanctioned] |
||||||
|
#the reported witness genuinely parses against both forms |
||||||
|
lappend result [dict get [punk::args::parse_status [dict get $finding witness] -form go withid ::testspace::fc_sanctioned] ok] |
||||||
|
lappend result [dict get [punk::args::parse_status [dict get $finding witness] -form run withid ::testspace::fc_sanctioned] ok] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list type_weakness 1 {} 1 1] |
||||||
|
|
||||||
|
test formcheck_overlapallowed_unknown_form_rejected {@form -overlapallowed naming an unknown form is rejected when the definition resolves}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::fc_badsanction |
||||||
|
@form -form aa -overlapallowed {nosuchform} |
||||||
|
@values -min 1 -max 1 |
||||||
|
x -type string |
||||||
|
@form -form bb |
||||||
|
@values -min 1 -max 1 |
||||||
|
y -type any |
||||||
|
} |
||||||
|
if {[catch {punk::args::get_spec ::testspace::fc_badsanction} errmsg]} { |
||||||
|
lappend result [string match "*-overlapallowed*unknown form 'nosuchform'*" $errmsg] |
||||||
|
} else { |
||||||
|
lappend result UNEXPECTED-resolved |
||||||
|
} |
||||||
|
set result |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
catch {punk::args::undefine ::testspace::fc_badsanction 1} |
||||||
|
}\ |
||||||
|
-result [list 1] |
||||||
|
|
||||||
|
test formcheck_return_summary {-return summary returns the human report text only}\ |
||||||
|
-setup $common -body { |
||||||
|
set s [punk::args::formcheck -return summary ::testspace::fc_sanctioned] |
||||||
|
lappend result [string match "punk::args::formcheck ::testspace::fc_sanctioned:*" $s] |
||||||
|
lappend result [string match "*sanctioned type_weakness overlap: go vs run*" $s] |
||||||
|
#dict return carries the same text under the summary key |
||||||
|
lappend result [expr {$s eq [dict get [punk::args::formcheck ::testspace::fc_sanctioned] summary]}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1 1] |
||||||
|
|
||||||
|
test formcheck_lseq_typeweakness {tclcore ::lseq reports the range/start_count expr-typed-end overlap as an unsanctioned type-weakness finding ('lseq 1 count 5' class)}\ |
||||||
|
-constraints have_tclcoredocs\ |
||||||
|
-setup $common -body { |
||||||
|
set r [punk::args::formcheck ::lseq] |
||||||
|
set finding [dict get $r findings {range start_count}] |
||||||
|
lappend result [dict get $finding class] |
||||||
|
lappend result [dict get $finding sanctioned] |
||||||
|
lappend result [expr {{range start_count} in [dict get $r unsanctioned]}] |
||||||
|
#the witness word for the aligned discriminator slot is start_count's |
||||||
|
#literalprefix(count) word, swallowed by range's number|expr end slot |
||||||
|
set discword "" |
||||||
|
foreach slot [dict get $finding slots] { |
||||||
|
if {[dict get $slot relation] eq "discriminator_vs_permissive"} { |
||||||
|
set discword [dict get $slot word] |
||||||
|
} |
||||||
|
} |
||||||
|
lappend result $discword |
||||||
|
#every reported ::lseq witness genuinely parses against both its forms |
||||||
|
dict for {pairkey pf} [dict get $r findings] { |
||||||
|
lassign $pairkey fa fb |
||||||
|
if {![dict get [punk::args::parse_status [dict get $pf witness] -form $fa withid ::lseq] ok] |
||||||
|
|| ![dict get [punk::args::parse_status [dict get $pf witness] -form $fb withid ::lseq] ok]} { |
||||||
|
lappend result [list unconfirmed-witness $pairkey] |
||||||
|
} |
||||||
|
} |
||||||
|
set result |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list type_weakness 0 1 count] |
||||||
|
|
||||||
|
test formcheck_after_cancel_sanctioned {tclcore ::after reports exactly the documented cancelid/cancelscript overlap - structural class, sanctioned via -overlapallowed, leaving no unsanctioned findings}\ |
||||||
|
-constraints have_tclcoredocs\ |
||||||
|
-setup $common -body { |
||||||
|
set r [punk::args::formcheck ::after] |
||||||
|
lappend result [dict keys [dict get $r findings]] |
||||||
|
lappend result [dict get $r findings {cancelid cancelscript} class] |
||||||
|
lappend result [dict get $r findings {cancelid cancelscript} sanctioned] |
||||||
|
lappend result [dict get $r unsanctioned] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list {{cancelid cancelscript}} structural 1 {}] |
||||||
|
|
||||||
|
test formcheck_sanction_does_not_affect_parse {the -overlapallowed sanction never changes parse behaviour - an ambiguous 'after cancel <id-shaped>' still raises multipleformmatches}\ |
||||||
|
-constraints have_tclcoredocs\ |
||||||
|
-setup $common -body { |
||||||
|
if {[catch {punk::args::parse {cancel after#1} withid ::after} errmsg erroropts]} { |
||||||
|
set classinfo [lindex [dict get $erroropts -errorcode] 2] |
||||||
|
lappend result [lindex $classinfo 0] |
||||||
|
} else { |
||||||
|
lappend result UNEXPECTED-parsed |
||||||
|
} |
||||||
|
set result |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list multipleformmatches] |
||||||
|
|
||||||
|
#fixture cleanup |
||||||
|
catch {punk::args::undefine ::testspace::fc_afterish 1} |
||||||
|
catch {punk::args::undefine ::testspace::fc_sharedform 1} |
||||||
|
catch {punk::args::undefine ::testspace::fc_sanctioned 1} |
||||||
|
} |
||||||
|
tcltest::cleanupTests ;#needed to produce test summary line. |
||||||
@ -0,0 +1,231 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
#Behaviour + artifact tests for bin/dtplite.cmd - the scriptwrap-multishell wrapped |
||||||
|
#tcllib dtplite application (payload: src/scriptapps/dtplite.tcl + dtplite_wrap.toml). |
||||||
|
# |
||||||
|
#Context: 'dev doc.validate' (punk::mix::commandset::doc::validate) invokes a bare |
||||||
|
#`dtplite validate <path>` which the punk repl unknown-handler resolves via auto_execok, |
||||||
|
#so a dtplite executable must be findable on PATH. bin/dtplite.cmd provides that |
||||||
|
#cross-platform (cmd.exe on windows dispatching to tclsh; sh/tclsh on unix). |
||||||
|
# |
||||||
|
#Covered: |
||||||
|
# - artifact contract: bin/dtplite.cmd is LF-only and passes scriptwrap checkfile |
||||||
|
# (no 512-byte label location errors) |
||||||
|
# - source sync: re-wrapping the dtplite scriptset from src/scriptapps reproduces the |
||||||
|
# committed bin/dtplite.cmd byte for byte (fix payloads and re-wrap - never the output) |
||||||
|
# - execution usecases (windows via cmd.exe; unix via sh - both need a tclsh with the |
||||||
|
# dtplite package or the project-vendored tcllib fallback): |
||||||
|
# * no args -> usage error, nonzero exit |
||||||
|
# * validate <good .man file> -> exit 0 |
||||||
|
# * validate <bad .man file> -> nonzero exit + doctools error on stderr |
||||||
|
# * validate <directory tree> -> exit 0 (the 'dev doc.validate' usecase) |
||||||
|
# * html generation to an output file |
||||||
|
# |
||||||
|
#NOTE for agents: bin/dtplite.cmd is GENERATED - edit src/scriptapps/dtplite.tcl / |
||||||
|
#dtplite_wrap.toml and re-wrap with punk::mix::commandset::scriptwrap::multishell |
||||||
|
#(see src/scriptapps/AGENTS.md). |
||||||
|
|
||||||
|
namespace eval ::testspace { |
||||||
|
namespace import ::tcltest::* |
||||||
|
variable common { |
||||||
|
set result "" |
||||||
|
} |
||||||
|
|
||||||
|
variable testdir [file dirname [file normalize [info script]]] |
||||||
|
#<projectroot>/src/tests/shell/testsuites/binscripts -> src/tests is 3 levels up |
||||||
|
variable testsroot [file normalize [file join $testdir .. .. ..]] |
||||||
|
variable projectroot [file normalize [file join $testsroot .. ..]] |
||||||
|
|
||||||
|
variable target [file join $projectroot bin dtplite.cmd] |
||||||
|
testConstraint havedtplitecmd [file exists $target] |
||||||
|
testConstraint iswindows [expr {$::tcl_platform(platform) eq "windows"}] |
||||||
|
testConstraint isunix [expr {$::tcl_platform(platform) eq "unix"}] |
||||||
|
testConstraint havescriptwrap [expr {![catch { |
||||||
|
package require Thread ;#punk::fileline textinfo (used by checkfile) calls thread::id |
||||||
|
package require punk::mix::commandset::scriptwrap |
||||||
|
}]}] |
||||||
|
|
||||||
|
proc readbytes {path} { |
||||||
|
set fd [open $path r] |
||||||
|
fconfigure $fd -translation binary |
||||||
|
set data [read $fd] |
||||||
|
close $fd |
||||||
|
return $data |
||||||
|
} |
||||||
|
|
||||||
|
#write with explicit lf translation |
||||||
|
proc writefile_lf {path content} { |
||||||
|
set fd [open $path w] |
||||||
|
fconfigure $fd -translation lf |
||||||
|
puts $fd $content |
||||||
|
close $fd |
||||||
|
} |
||||||
|
|
||||||
|
#run bin/dtplite.cmd with args; returns dict {exit <code> output <stdout+stderr>} |
||||||
|
#on windows via cmd.exe (the way the repl unknown-handler launches it), on unix via sh |
||||||
|
proc run_dtplite {args} { |
||||||
|
variable target |
||||||
|
set output "" |
||||||
|
set code 0 |
||||||
|
if {$::tcl_platform(platform) eq "windows"} { |
||||||
|
set comspec [expr {[info exists ::env(ComSpec)] ? $::env(ComSpec) : "cmd.exe"}] |
||||||
|
set cmdline [list $comspec /c [file nativename $target] {*}$args] |
||||||
|
} else { |
||||||
|
set cmdline [list sh $target {*}$args] |
||||||
|
} |
||||||
|
if {[catch {exec -- {*}$cmdline << "" 2>@1} output opts]} { |
||||||
|
set ec [dict get $opts -errorcode] |
||||||
|
if {[lindex $ec 0] eq "CHILDSTATUS"} { |
||||||
|
set code [lindex $ec 2] |
||||||
|
} else { |
||||||
|
set code -1 |
||||||
|
} |
||||||
|
} |
||||||
|
return [dict create exit $code output $output] |
||||||
|
} |
||||||
|
|
||||||
|
#--- fixtures: minimal good/bad doctools manpages + a small doc tree ------------- |
||||||
|
variable good_man {[manpage_begin sample_good n 1.0] |
||||||
|
[copyright {2026}] |
||||||
|
[moddesc {Sample}] |
||||||
|
[titledesc {A minimal valid doctools manpage}] |
||||||
|
[description] |
||||||
|
[para] This is a minimal valid doctools document used for smoke testing dtplite. |
||||||
|
[section Usage] |
||||||
|
[para] Nothing to see here. |
||||||
|
[manpage_end]} |
||||||
|
variable bad_man {[manpage_begin sample_bad n 1.0] |
||||||
|
[copyright {2026}] |
||||||
|
[moddesc {Sample}] |
||||||
|
[titledesc {An invalid doctools manpage - manpage_end missing}] |
||||||
|
[description] |
||||||
|
[list_begin itemized] |
||||||
|
[item] an item, but the list is never closed and manpage_end is missing} |
||||||
|
|
||||||
|
variable fixdir [makeDirectory dtplite_fixture] |
||||||
|
writefile_lf $fixdir/sample_good.man $good_man |
||||||
|
writefile_lf $fixdir/sample_bad.man $bad_man |
||||||
|
variable treedir [makeDirectory dtplite_fixture_tree] |
||||||
|
file mkdir $treedir/sub |
||||||
|
writefile_lf $treedir/one.man [string map {sample_good tree_one} $good_man] |
||||||
|
writefile_lf $treedir/sub/two.man [string map {sample_good tree_two} $good_man] |
||||||
|
|
||||||
|
#--- artifact contract ------------------------------------------------------------ |
||||||
|
|
||||||
|
test dtplite_cmd_lf_only {the committed bin/dtplite.cmd uses LF line endings exclusively (cmd label scanning depends on it)}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable target |
||||||
|
set data [readbytes $target] |
||||||
|
lappend result [expr {[string first \r $data] == -1}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1] |
||||||
|
|
||||||
|
test dtplite_cmd_checkfile_no_label_errors {the committed bin/dtplite.cmd passes scriptwrap checkfile - no 512-byte label location errors}\ |
||||||
|
-constraints {havedtplitecmd havescriptwrap}\ |
||||||
|
-setup $common -body { |
||||||
|
variable target |
||||||
|
set summary [punk::mix::commandset::scriptwrap::checkfile $target] |
||||||
|
lappend result [string match "*ERROR: label location errors*" $summary] |
||||||
|
#sanity: the analysis actually ran (labels enumerated) |
||||||
|
lappend result [string match "*call-labels-found:*" $summary] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0 1] |
||||||
|
|
||||||
|
test dtplite_cmd_roundtrip_no_drift {re-wrapping the dtplite scriptset from src/scriptapps reproduces the committed bin/dtplite.cmd byte for byte}\ |
||||||
|
-constraints {havedtplitecmd havescriptwrap}\ |
||||||
|
-setup $common -body { |
||||||
|
variable projectroot |
||||||
|
variable target |
||||||
|
set scriptapps [file join $projectroot src scriptapps] |
||||||
|
if {![file exists $scriptapps/dtplite_wrap.toml]} { |
||||||
|
lappend result no_dtplite_sources |
||||||
|
} else { |
||||||
|
set outdir [makeDirectory dtplite_roundtrip] |
||||||
|
set startdir [pwd] |
||||||
|
cd $scriptapps |
||||||
|
set res [punk::mix::commandset::scriptwrap::multishell dtplite -outputfolder $outdir -askme 0 -force 1] |
||||||
|
cd $startdir |
||||||
|
set fresh [dict get $res filename] |
||||||
|
lappend result [expr {[readbytes $fresh] eq [readbytes $target]}] |
||||||
|
#a failure here means either bin/dtplite.cmd was edited directly (fix |
||||||
|
#src/scriptapps/dtplite.tcl or dtplite_wrap.toml instead and re-wrap), |
||||||
|
#or the payload/template changed without regenerating bin/dtplite.cmd |
||||||
|
} |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1] |
||||||
|
|
||||||
|
#--- execution usecases ----------------------------------------------------------- |
||||||
|
|
||||||
|
test dtplite_exec_no_args_usage_error {running dtplite.cmd without arguments exits nonzero with the dtplite usage message}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
set r [run_dtplite] |
||||||
|
lappend result [expr {[dict get $r exit] != 0}] |
||||||
|
lappend result [string match "*wrong#args*" [dict get $r output]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
test dtplite_exec_validate_good {validate of a well-formed .man file exits 0}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set r [run_dtplite validate [file nativename $fixdir/sample_good.man]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0] |
||||||
|
|
||||||
|
test dtplite_exec_validate_bad {validate of a malformed .man file exits nonzero and reports a doctools format error}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set r [run_dtplite validate [file nativename $fixdir/sample_bad.man]] |
||||||
|
lappend result [expr {[dict get $r exit] != 0}] |
||||||
|
lappend result [string match "*FmtError*" [dict get $r output]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
test dtplite_exec_validate_tree {validate of a directory tree of .man files exits 0 - the 'dev doc.validate' usecase}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable treedir |
||||||
|
set r [run_dtplite validate [file nativename $treedir]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 0] |
||||||
|
|
||||||
|
test dtplite_exec_html_generation {html generation from a .man file produces an html output file mentioning the manpage name}\ |
||||||
|
-constraints havedtplitecmd\ |
||||||
|
-setup $common -body { |
||||||
|
variable fixdir |
||||||
|
set outfile $fixdir/sample_good.html |
||||||
|
set r [run_dtplite -o [file nativename $outfile] html [file nativename $fixdir/sample_good.man]] |
||||||
|
lappend result [dict get $r exit] |
||||||
|
lappend result [file exists $outfile] |
||||||
|
if {[file exists $outfile]} { |
||||||
|
set html [readbytes $outfile] |
||||||
|
lappend result [string match "*sample_good*" $html] |
||||||
|
} else { |
||||||
|
lappend result no_output_file |
||||||
|
} |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
file delete -force $fixdir/sample_good.html |
||||||
|
}\ |
||||||
|
-result [list 0 1 1] |
||||||
|
} |
||||||
|
tcltest::cleanupTests ;#needed to produce test summary line. |
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue