Browse Source
New bare @normalize directive: opts a definition into indent normalization of BLOCK-FORM multi-line field values (first line whitespace-only, as authored by opening a braced literal with a newline). The structural first newline and a whitespace-only trailing line are dropped, the content lines' common leading whitespace is the block's base indent, the first content line is unindented fully and subsequent lines are re-based to the standard 4-space continuation convention (deeper relative indents preserved; whitespace-only inner lines become empty). Fields in a record's -unindentedfields are exempt. Implemented as a resolve pre-pass over the split records (private::normalize_records / rebase_multiline_value); @normalize with options is an error. Head-form values are never altered - their base indent is unknowable (continuations uniformly at 6 may be base-4 with the deliberate +2 relative convention, or base-6 flush; re-basing would flatten the former). The idempotence test caught exactly that on a file-style definition during implementation, narrowing the user-confirmed re-base semantics to block form only - which also makes @normalize a proven no-op on conforming file-style definitions. Consumer proof (punk 0.2.5, per the recorded user decision): ::punk::helptopic::define_docs converts from interim left-margin authoring to indented block-form values under @normalize, dropping its -unindentedfields declarations; 'i help' and 'i help_chunks' verified aligned in punk902z src, including the blank-line separator in the combined basehelp+extra block. Tests: new normalize.test (5 tests: block re-base, block left-margin, head-form boundary, exemption byte-exactness, file-style idempotence); rendering.test P4 characterization stays pinned as the deliberate unopted default with its text updated to reference @normalize. punk::args suite 186 pass / 1 pre-existing skip / 0 fail; punk::ns 53/53; full source-tree suite 806 pass / 13 skip / 1 fail = exec-14.3 only (known baseline) - zero regressions (tclsh 9.0.3). define doc documents the directive. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
9 changed files with 427 additions and 30 deletions
@ -1,4 +1,4 @@
|
||||
[project] |
||||
name = "punkshell" |
||||
version = "0.12.6" |
||||
version = "0.12.7" |
||||
license = "BSD-2-Clause" |
||||
|
||||
@ -0,0 +1,192 @@
|
||||
package require tcltest |
||||
|
||||
package require punk::args |
||||
package require punk::ansi |
||||
|
||||
#@normalize directive (G-045): opt-in indent normalization of BLOCK-FORM multi-line |
||||
#field values for constructed (string-built) definitions. Semantics (user-confirmed |
||||
#re-base 2026-07-12; narrowed to block form during implementation): a value whose |
||||
#first line is whitespace-only is a block - the structural first newline and a |
||||
#whitespace-only trailing line are dropped, the content lines' common leading |
||||
#whitespace is the block's base indent, the first content line is unindented fully |
||||
#and subsequent lines are re-based to the 4-space convention (deeper relative |
||||
#indents preserved). Head-form values are NEVER altered: their base is ambiguous |
||||
#(uniform continuation indent may be the deliberate +2 relative convention over |
||||
#base 4, or a deeper flush base) - which also makes @normalize a no-op on |
||||
#conforming file-style definitions. Fields in a record's -unindentedfields are |
||||
#exempt. Without @normalize, constructed-def behaviour is unchanged - see the P4 |
||||
#characterization in rendering.test. |
||||
|
||||
namespace eval ::testspace { |
||||
namespace import ::tcltest::* |
||||
variable common { |
||||
set result "" |
||||
} |
||||
|
||||
proc render_table {id} { |
||||
return [punk::ansi::ansistrip [punk::args::arg_error "" [punk::args::get_spec $id] -aserror 0]] |
||||
} |
||||
#column (0-based) at which a unique marker string appears in the rendered text, -1 if absent |
||||
proc markercol {rendered marker} { |
||||
foreach ln [split $rendered \n] { |
||||
set ix [string first $marker $ln] |
||||
if {$ix >= 0} { |
||||
return $ix |
||||
} |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
#added 2026-07-12 (agent, G-045) - the head-form boundary: base indent is |
||||
#ambiguous for head-form values so @normalize leaves them alone (deep indents |
||||
#leak exactly as in the unopted P4 characterization: display strips only 4) |
||||
test normalize_headform_untouched {@normalize: head-form values (content on the first line) are not altered - deep continuation indents render as authored}\ |
||||
-setup $common -body { |
||||
set help "NFIRST line\n NFLUSH line\n NPLUS2 line" |
||||
set def "" |
||||
append def "@normalize" \n |
||||
append def "@id -id ::testspace::nz_head" \n |
||||
append def "@cmd -name testspace::nz_head -summary \"Head.\" -help \"$help\"" \n |
||||
append def "@values -min 0 -max 0" |
||||
punk::args::define $def |
||||
set r [render_table ::testspace::nz_head] |
||||
set n0 [markercol $r NFIRST] |
||||
lappend result [expr {[markercol $r NFLUSH] - $n0}] |
||||
lappend result [expr {[markercol $r NPLUS2] - $n0}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::nz_head 1 |
||||
}\ |
||||
-result [list 12 14] |
||||
|
||||
#added 2026-07-12 (agent, G-045) - the block-form ergonomics: a braced value |
||||
#authored as a clean indented block (leading newline, closing-brace line) needs |
||||
#no manual string trim |
||||
test normalize_blockform_leading_newline {@normalize: block-form value (whitespace-only first line) drops the structural newline and renders flush}\ |
||||
-setup $common -body { |
||||
set help { |
||||
BFIRST line |
||||
BFLUSH line |
||||
BPLUS2 line |
||||
} |
||||
set def "" |
||||
append def "@normalize" \n |
||||
append def "@id -id ::testspace::nz_block" \n |
||||
append def "@cmd -name testspace::nz_block -summary \"Block.\" -help \"$help\"" \n |
||||
append def "@values -min 0 -max 0" |
||||
punk::args::define $def |
||||
set r [render_table ::testspace::nz_block] |
||||
set b0 [markercol $r BFIRST] |
||||
lappend result [expr {[markercol $r BFLUSH] - $b0}] |
||||
lappend result [expr {[markercol $r BPLUS2] - $b0}] |
||||
#the structural leading newline is gone: BFIRST appears on the same |
||||
#rendered line as the Description: label |
||||
set desc_line "" |
||||
foreach ln [split $r \n] { |
||||
if {[string first "Description:" $ln] >= 0} { |
||||
set desc_line $ln |
||||
break |
||||
} |
||||
} |
||||
lappend result [expr {[string first "BFIRST" $desc_line] >= 0}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::nz_block 1 |
||||
}\ |
||||
-result [list 0 2 1] |
||||
|
||||
#added 2026-07-12 (agent, G-045) - block form with zero base indent: left-margin |
||||
#content behind a structural leading newline re-bases to the convention (no |
||||
#-unindentedfields declaration needed) |
||||
test normalize_blockform_leftmargin_gains_base {@normalize: a block-form value with left-margin content is re-based and renders flush}\ |
||||
-setup $common -body { |
||||
set help "\nLFIRST line\nLFLUSH line" |
||||
set def "" |
||||
append def "@normalize" \n |
||||
append def "@id -id ::testspace::nz_left" \n |
||||
append def "@cmd -name testspace::nz_left -summary \"Left.\" -help \"$help\"" \n |
||||
append def "@values -min 0 -max 0" |
||||
punk::args::define $def |
||||
set r [render_table ::testspace::nz_left] |
||||
lappend result [expr {[markercol $r LFLUSH] - [markercol $r LFIRST]}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::nz_left 1 |
||||
}\ |
||||
-result [list 0] |
||||
|
||||
#added 2026-07-12 (agent, G-045) - exemption: an -unindentedfields field keeps |
||||
#its block-form value byte-exact (structural newline preserved, no re-base, no |
||||
#display transform) where a non-exempt block would lose the leading blank line |
||||
test normalize_unindentedfields_exempt {@normalize: fields in a record's -unindentedfields are exempt - block-form value kept literally}\ |
||||
-setup $common -body { |
||||
set help "\n EFIRST line\n ESIX line" |
||||
set def "" |
||||
append def "@normalize" \n |
||||
append def "@id -id ::testspace::nz_exempt" \n |
||||
append def "@cmd -name testspace::nz_exempt -summary \"Exempt.\" -unindentedfields {-help} -help \"$help\"" \n |
||||
append def "@values -min 0 -max 0" |
||||
punk::args::define $def |
||||
set r [render_table ::testspace::nz_exempt] |
||||
#leading structural newline preserved: EFIRST is NOT on the Description: line |
||||
set desc_line "" |
||||
foreach ln [split $r \n] { |
||||
if {[string first "Description:" $ln] >= 0} { |
||||
set desc_line $ln |
||||
break |
||||
} |
||||
} |
||||
lappend result [expr {[string first "EFIRST" $desc_line] < 0}] |
||||
#6-space indents kept literally - both lines at the same column |
||||
lappend result [expr {[markercol $r ESIX] - [markercol $r EFIRST]}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::nz_exempt 1 |
||||
}\ |
||||
-result [list 1 0] |
||||
|
||||
#added 2026-07-12 (agent, G-045) - idempotence: @normalize on a conforming |
||||
#file-style braced definition changes nothing |
||||
test normalize_idempotent_on_filestyle {@normalize: a braced file-style definition renders identically with and without the directive}\ |
||||
-setup $common -body { |
||||
punk::args::define { |
||||
@id -id ::testspace::nz_fplain |
||||
@cmd -name testspace::nz_fstyle -summary\ |
||||
"Filestyle."\ |
||||
-help\ |
||||
"FFIRST line. |
||||
FFLUSH line. |
||||
FPLUS2 line." |
||||
@values -min 1 -max 1 |
||||
v1 -type string -help\ |
||||
"val help line1 |
||||
val help line2" |
||||
} |
||||
punk::args::define { |
||||
@normalize |
||||
@id -id ::testspace::nz_fnorm2 |
||||
@cmd -name testspace::nz_fstyle -summary\ |
||||
"Filestyle."\ |
||||
-help\ |
||||
"FFIRST line. |
||||
FFLUSH line. |
||||
FPLUS2 line." |
||||
@values -min 1 -max 1 |
||||
v1 -type string -help\ |
||||
"val help line1 |
||||
val help line2" |
||||
} |
||||
set r_plain [render_table ::testspace::nz_fplain] |
||||
set r_norm [render_table ::testspace::nz_fnorm2] |
||||
#ids deliberately same length (nz_fplain/nz_fnorm2) so table geometry matches |
||||
lappend result [expr {[string map {nz_fnorm2 nz_fplain} $r_norm] eq $r_plain}] |
||||
}\ |
||||
-cleanup { |
||||
punk::args::undefine ::testspace::nz_fplain 1 |
||||
punk::args::undefine ::testspace::nz_fnorm2 1 |
||||
}\ |
||||
-result [list 1] |
||||
|
||||
cleanupTests |
||||
} |
||||
namespace delete ::testspace |
||||
Loading…
Reference in new issue