Browse Source
- 34 documentation-only PUNKARGS blocks added: punk::ansi::codetype (is_sgr_reset, has_sgr_leadingreset, is_cursor_move_in_line, has_all_effective, get_effective_types, is_gx/is_gx_open/is_gx_close, sgr_merge), punk::ansi::sequence_type (is_Fe7/is_Fe/is_Fe8, is_Fp, is_Fs, is_nF, is_3Fp, is_code7/is_code8/is_code, classify), punk::ansi::ta (detect_in_list, detectcode, detectcode_in_list, detect_g0, detect_open, detect_st_open, detect_csi, detect_sgr, split_at_codes, split_codes, split_codes_single, get_codes_single) and punk::ansi::ansistring (VIEW, COUNT, length, trimleft/trimright/trim, INDEX, INDEXCHAR, RANGE, INSERT, INDEXABSOLUTE, INDEXCOLUMNS, COLUMNINDEX). - ::punk::ansi::sequence_type added to the punk::args::register NAMESPACES list - the escape-form classifiers live there (a separate namespace from codetype) and were previously undiscoverable by the doc system. - New characterization suites: ta.test (12 tests - detect vs detectcode lone-CSI distinction, split_codes/split_codes_single/split_at_codes shapes and join round-trip, length/extract), codetype.test (11 tests - reset/leading-reset semantics, effective-state queries incl. bold-via-intensity, sgr_merge, sequence_type classify taxonomy); ansistring.test extended 1 -> 12 tests (INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with merged SGR-prefix tracking incl. end+1 state-after-string, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping via a CJK fixture, INDEXABSOLUTE resolution, styled-whitespace trim, VIEW control-picture substitution). - Tests use literal escape strings (not a+) so results are independent of the punk::console colour state. Origin notes added to the usage-marking suites' local SGR helpers pointing at these canonical mechanisms and their coverage. - src/tests/modules/AGENTS.md child DOX index: entries added for the punk/ansi, punk/args and punk/ns suites (GAP-pin/goal mapping documented). - Verified: 44/44 ansi suite and the full ansi+args+ns sweep (232 passed, 1 pre-existing skip) on Tcl 9.0.3 and 8.7; make.tcl modules clean. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
6 changed files with 1228 additions and 25 deletions
@ -1,3 +1,4 @@ |
|||||||
0.1.1 |
0.1.2 |
||||||
#First line must be a semantic version number |
#First line must be a semantic version number |
||||||
#all other lines are ignored. |
#all other lines are ignored. |
||||||
|
#0.1.2 - documentation-only: PUNKARGS argdoc blocks added for punk::ansi::codetype (is_sgr_reset, has_sgr_leadingreset, is_cursor_move_in_line, has_all_effective, get_effective_types, is_gx/is_gx_open/is_gx_close, sgr_merge), punk::ansi::sequence_type (is_Fe7/is_Fe/is_Fe8, is_Fp, is_Fs, is_nF, is_3Fp, is_code7/is_code8/is_code, classify), punk::ansi::ta (detect_in_list, detectcode, detectcode_in_list, detect_g0, detect_open, detect_st_open, detect_csi, detect_sgr, split_at_codes, split_codes, split_codes_single, get_codes_single) and punk::ansi::ansistring (VIEW, COUNT, length, trimleft/trimright/trim, INDEX, INDEXCHAR, RANGE, INSERT, INDEXABSOLUTE, INDEXCOLUMNS, COLUMNINDEX); ::punk::ansi::sequence_type added to punk::args::register NAMESPACES so its argdocs are discoverable |
||||||
|
|||||||
@ -0,0 +1,163 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
package require punk::ansi |
||||||
|
|
||||||
|
#Characterization of punk::ansi::codetype (single-code tests, effective-state queries, |
||||||
|
#SGR merging) and punk::ansi::sequence_type (escape-form classification) - added |
||||||
|
#2026-07-10 alongside the PUNKARGS documentation pass (punk::ansi 0.1.2). |
||||||
|
#These are the primitives behind the ANSI-in-effect tracking used by ansistring |
||||||
|
#INDEX/INDEXCODE and the usage-marking machinery, so their semantics are pinned directly. |
||||||
|
#ANSI codes are written as literal escape strings (not generated via a+) so the tests are |
||||||
|
#independent of the punk::console colour on/off state. |
||||||
|
|
||||||
|
namespace eval ::testspace { |
||||||
|
namespace import ::tcltest::* |
||||||
|
variable common { |
||||||
|
set result "" |
||||||
|
} |
||||||
|
|
||||||
|
variable RED "\x1b\[31m" |
||||||
|
variable BOLD "\x1b\[1m" |
||||||
|
variable RESET "\x1b\[0m" |
||||||
|
|
||||||
|
#--- single-code tests -------------------------------------------------------------------- |
||||||
|
|
||||||
|
test codetype_is_sgr {is_sgr: trailing SGR detected; non-SGR CSI is not}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::codetype::is_sgr $RED] |
||||||
|
lappend result [punk::ansi::codetype::is_sgr "\x1b\[2A"] |
||||||
|
lappend result [punk::ansi::codetype::is_sgr "plain"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 0] |
||||||
|
|
||||||
|
test codetype_is_sgr_reset {is_sgr_reset: pure trailing reset only - embedded or non-trailing resets not detected}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::codetype::is_sgr_reset $RESET] |
||||||
|
lappend result [punk::ansi::codetype::is_sgr_reset "\x1b\[m"] |
||||||
|
#reset amongst other parameters is not a *pure* reset |
||||||
|
lappend result [punk::ansi::codetype::is_sgr_reset "\x1b\[31;0m"] |
||||||
|
#reset not at the very end is not detected |
||||||
|
lappend result [punk::ansi::codetype::is_sgr_reset "${RESET}x"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1 0 0] |
||||||
|
|
||||||
|
test codetype_has_sgr_leadingreset {has_sgr_leadingreset: first parameter of the leading SGR must be a reset}\ |
||||||
|
-setup $common -body { |
||||||
|
lappend result [punk::ansi::codetype::has_sgr_leadingreset "\x1b\[0;31m"] |
||||||
|
lappend result [punk::ansi::codetype::has_sgr_leadingreset "\x1b\[31;0m"] |
||||||
|
lappend result [punk::ansi::codetype::has_sgr_leadingreset "\x1b\[m"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 1] |
||||||
|
|
||||||
|
test codetype_is_cursor_move_in_line {is_cursor_move_in_line: within-line moves (C/D/G) accepted, vertical moves not}\ |
||||||
|
-setup $common -body { |
||||||
|
lappend result [punk::ansi::codetype::is_cursor_move_in_line "\x1b\[5C"] |
||||||
|
lappend result [punk::ansi::codetype::is_cursor_move_in_line "\x1b\[3G"] |
||||||
|
lappend result [punk::ansi::codetype::is_cursor_move_in_line "\x1b\[2A"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1 0] |
||||||
|
|
||||||
|
test codetype_is_gx_family {is_gx / is_gx_open / is_gx_close for G0/G1 charset sequences}\ |
||||||
|
-setup $common -body { |
||||||
|
lappend result [punk::ansi::codetype::is_gx "\x1b(0abc\x1b(B"] |
||||||
|
lappend result [punk::ansi::codetype::is_gx "\x1b(0abc"] |
||||||
|
lappend result [punk::ansi::codetype::is_gx_open "\x1b(0"] |
||||||
|
lappend result [punk::ansi::codetype::is_gx_close "\x1b(B"] |
||||||
|
lappend result [punk::ansi::codetype::is_gx_open "\x1b(B"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 1 1 0] |
||||||
|
|
||||||
|
#--- effective-state queries --------------------------------------------------------------- |
||||||
|
|
||||||
|
test codetype_has_any_effective {has_any_effective: state must survive to the end of the string}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::codetype::has_any_effective "${RED}text" fg] |
||||||
|
#a trailing reset makes the red ineffective |
||||||
|
lappend result [punk::ansi::codetype::has_any_effective "${RED}text${RESET}" fg] |
||||||
|
#no bg was ever set |
||||||
|
lappend result [punk::ansi::codetype::has_any_effective "${RED}text" bg] |
||||||
|
#any-semantics: one of the requested states suffices |
||||||
|
lappend result [punk::ansi::codetype::has_any_effective "${RED}text" bg fg] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 0 1] |
||||||
|
|
||||||
|
test codetype_has_all_effective {has_all_effective: every requested state must be in effect; bold matches intensity 1}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
lappend result [punk::ansi::codetype::has_all_effective "${RED}${BOLD}x" fg bold] |
||||||
|
lappend result [punk::ansi::codetype::has_all_effective "${RED}x" fg bold] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0] |
||||||
|
|
||||||
|
test codetype_get_effective_types {get_effective_types lists surviving states; a trailing reset clears them (the reset code itself still reports 'sgr')}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::codetype::get_effective_types "${RED}${BOLD}x"] |
||||||
|
lappend result [punk::ansi::codetype::get_effective_types "${RED}x${RESET}"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list {sgr intensity fg bold} sgr] |
||||||
|
|
||||||
|
test codetype_sgr_merge {sgr_merge combines separate SGR codes into a single code}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
lappend result [punk::ansi::codetype::sgr_merge [list $RED $BOLD]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list "\x1b\[1;31m"] |
||||||
|
|
||||||
|
#--- sequence_type classification ----------------------------------------------------------- |
||||||
|
|
||||||
|
test sequence_type_classify {classify recognises Fp/Fe/Fs/nF/3Fp forms and unknown}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::sequence_type::classify $RED] |
||||||
|
lappend result [punk::ansi::sequence_type::classify "\x1b7"] |
||||||
|
lappend result [punk::ansi::sequence_type::classify "\x1bc"] |
||||||
|
lappend result [punk::ansi::sequence_type::classify "\x1b(0"] |
||||||
|
lappend result [punk::ansi::sequence_type::classify "\x1b#4"] |
||||||
|
lappend result [punk::ansi::sequence_type::classify "abc"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list Fe Fp Fs 0Fp 3Fp unknown] |
||||||
|
|
||||||
|
test sequence_type_is_forms {is_Fp / is_Fs / is_nF / is_code form tests}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::sequence_type::is_Fp "\x1b7"] |
||||||
|
lappend result [punk::ansi::sequence_type::is_Fs "\x1bc"] |
||||||
|
lappend result [punk::ansi::sequence_type::is_nF "\x1b(0"] |
||||||
|
lappend result [punk::ansi::sequence_type::is_Fe7 $RED] |
||||||
|
lappend result [punk::ansi::sequence_type::is_code $RED] |
||||||
|
lappend result [punk::ansi::sequence_type::is_code "abc"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1 1 1 1 0] |
||||||
|
} |
||||||
|
tcltest::cleanupTests ;#needed to produce test summary line. |
||||||
@ -0,0 +1,185 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
package require punk::ansi |
||||||
|
|
||||||
|
#Characterization of punk::ansi::ta (text-ansi) detection and splitting - added 2026-07-10 |
||||||
|
#alongside the PUNKARGS documentation pass over ta/codetype/sequence_type/ansistring |
||||||
|
#(punk::ansi 0.1.2). These functions underpin most ANSI processing in punk (including the |
||||||
|
#character-position code tracking that ansistring INDEX/INDEXCODE and opunk::Str INDEXCODE |
||||||
|
#build on), so their splitting invariants are pinned directly here. |
||||||
|
#ANSI codes are written as literal escape strings (not generated via a+) so the tests are |
||||||
|
#independent of the punk::console colour on/off state. |
||||||
|
|
||||||
|
namespace eval ::testspace { |
||||||
|
namespace import ::tcltest::* |
||||||
|
variable common { |
||||||
|
set result "" |
||||||
|
} |
||||||
|
|
||||||
|
variable RED "\x1b\[31m" |
||||||
|
variable BOLD "\x1b\[1m" |
||||||
|
variable RESET "\x1b\[0m" |
||||||
|
|
||||||
|
#--- detection ---------------------------------------------------------------------------- |
||||||
|
|
||||||
|
test ta_detect_basics {detect: complete codes detected, plain text and truncated sequences are not}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::ta::detect "a${RED}b"] |
||||||
|
lappend result [punk::ansi::ta::detect "plain text"] |
||||||
|
#a lone trailing ESC is not a complete code |
||||||
|
lappend result [punk::ansi::ta::detect "abc\x1b"] |
||||||
|
#a lone CSI opening is not a *complete* code either |
||||||
|
lappend result [punk::ansi::ta::detect "abc\x1b\["] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 0 0] |
||||||
|
|
||||||
|
test ta_detectcode_lone_openings {detectcode: detects codes without requiring paired/complete sequences - a lone CSI opening counts}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::ta::detectcode "a${RED}b"] |
||||||
|
lappend result [punk::ansi::ta::detectcode "plain text"] |
||||||
|
#the difference from detect: a lone CSI opening is detected |
||||||
|
lappend result [punk::ansi::ta::detectcode "abc\x1b\["] |
||||||
|
#but a lone ESC alone is not |
||||||
|
lappend result [punk::ansi::ta::detectcode "abc\x1b"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 1 0] |
||||||
|
|
||||||
|
test ta_detect_in_list {detect_in_list/detectcode_in_list: per-element detection}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::ta::detect_in_list [list plain "a${RED}b" more]] |
||||||
|
lappend result [punk::ansi::ta::detect_in_list [list plain more]] |
||||||
|
lappend result [punk::ansi::ta::detectcode_in_list [list plain "abc\x1b\["]] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 1] |
||||||
|
|
||||||
|
test ta_detect_csi_vs_sgr {detect_csi detects any CSI sequence; detect_sgr only those ending in m}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
#cursor-up is a CSI but not an SGR |
||||||
|
lappend result [punk::ansi::ta::detect_csi "\x1b\[2A"] |
||||||
|
lappend result [punk::ansi::ta::detect_sgr "\x1b\[2A"] |
||||||
|
lappend result [punk::ansi::ta::detect_csi "a${RED}b"] |
||||||
|
lappend result [punk::ansi::ta::detect_sgr "a${RED}b"] |
||||||
|
#an SGR reset is also an SGR |
||||||
|
lappend result [punk::ansi::ta::detect_sgr "\x1b\[0m"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 1 1 1] |
||||||
|
|
||||||
|
test ta_detect_g0 {detect_g0 requires a complete G0 open..close group}\ |
||||||
|
-setup $common -body { |
||||||
|
lappend result [punk::ansi::ta::detect_g0 "\x1b(0qqq\x1b(B"] |
||||||
|
lappend result [punk::ansi::ta::detect_g0 "\x1b(0qqq"] |
||||||
|
lappend result [punk::ansi::ta::detect_g0 "plain"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 0 0] |
||||||
|
|
||||||
|
#--- splitting ---------------------------------------------------------------------------- |
||||||
|
|
||||||
|
test ta_split_codes_shape {split_codes: plaintext on even indices, codes on odd; first/last always plaintext; consecutive codes stay together}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::ta::split_codes ""] |
||||||
|
lappend result [punk::ansi::ta::split_codes "a"] |
||||||
|
#string ending in a code gets a trailing empty plaintext element |
||||||
|
lappend result [punk::ansi::ta::split_codes "a${RED}"] |
||||||
|
lappend result [punk::ansi::ta::split_codes "${RED}a${RESET}b"] |
||||||
|
#consecutive codes remain in ONE code element |
||||||
|
lappend result [punk::ansi::ta::split_codes "${RED}${BOLD}b"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list\ |
||||||
|
{}\ |
||||||
|
[list a]\ |
||||||
|
[list a "\x1b\[31m" ""]\ |
||||||
|
[list "" "\x1b\[31m" a "\x1b\[0m" b]\ |
||||||
|
[list "" "\x1b\[31m\x1b\[1m" b]\ |
||||||
|
] |
||||||
|
|
||||||
|
test ta_split_codes_single_shape {split_codes_single: each code in its own element with empty plaintext between consecutive codes}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
lappend result [punk::ansi::ta::split_codes_single "${RED}${BOLD}b"] |
||||||
|
lappend result [punk::ansi::ta::split_codes_single ""] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list\ |
||||||
|
[list "" "\x1b\[31m" "" "\x1b\[1m" b]\ |
||||||
|
{}\ |
||||||
|
] |
||||||
|
|
||||||
|
test ta_split_at_codes_plaintext_only {split_at_codes returns only the plaintext portions}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::ta::split_at_codes "a${RED}b${RESET}c"] |
||||||
|
lappend result [punk::ansi::ta::split_at_codes ""] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list [list a b c] {}] |
||||||
|
|
||||||
|
test ta_get_codes_single {get_codes_single returns just the codes, one per element}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
lappend result [punk::ansi::ta::get_codes_single "a${RED}b${BOLD}c"] |
||||||
|
lappend result [punk::ansi::ta::get_codes_single "plain"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list [list "\x1b\[31m" "\x1b\[1m"] {}] |
||||||
|
|
||||||
|
test ta_split_roundtrip {joining split_codes / split_codes_single output reconstructs the original string}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable BOLD |
||||||
|
variable RESET |
||||||
|
set s "start${RED}${BOLD}mid${RESET}end" |
||||||
|
lappend result [expr {[join [punk::ansi::ta::split_codes $s] ""] eq $s}] |
||||||
|
lappend result [expr {[join [punk::ansi::ta::split_codes_single $s] ""] eq $s}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
#--- length / extract --------------------------------------------------------------------- |
||||||
|
|
||||||
|
test ta_length_excludes_codes {length counts characters excluding ANSI codes}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
lappend result [punk::ansi::ta::length "a${RED}b"] |
||||||
|
lappend result [punk::ansi::ta::length ""] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list 2 0] |
||||||
|
|
||||||
|
test ta_extract_codes_only {extract returns only the ANSI codes (opposite of strip)}\ |
||||||
|
-setup $common -body { |
||||||
|
variable RED |
||||||
|
variable RESET |
||||||
|
lappend result [punk::ansi::ta::extract "a${RED}b${RESET}"] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
}\ |
||||||
|
-result [list "\x1b\[31m\x1b\[0m"] |
||||||
|
} |
||||||
|
tcltest::cleanupTests ;#needed to produce test summary line. |
||||||
Loading…
Reference in new issue