diff --git a/README.md b/README.md index d1a8e89..84ebd20 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,17 @@ on). Syntax highlighting and editing support for Emacs and Sublime Text are in [`doc/edit/`](doc/edit/). +## Provenance + +This repository is a curated snapshot of Klammertext's private development +tree, assembled by a manifest-driven script. Its history is a series of +release snapshots, not a mirror of the development history, and files here +are regenerated on each release — patches cannot be merged directly. +Report problems (or send patches) to the author; accepted changes are +applied to the development tree and appear in a following snapshot. + +This snapshot was assembled from development commit `f6463478da4c`. + ## License See [`LICENSE.md`](LICENSE.md). diff --git a/tst/Makefile b/tst/Makefile index ebcb64c..c6b44ca 100644 --- a/tst/Makefile +++ b/tst/Makefile @@ -1,8 +1,9 @@ # Klammertext distribution test suite (subset). # -# Runs the two shell regression suites: +# Runs the three shell regression suites: # cond_test.sh — @cond argument delimitation # deftype_test.sh — the four klammer definition modes + redefinition table +# escape_test.sh — target character escaping and quoted specials # # Requires KLAMMERTEXT_HOME set and `ktext` on PATH (build it with `make -C com`). @@ -10,3 +11,4 @@ test: ./cond_test.sh ./deftype_test.sh + ./escape_test.sh diff --git a/tst/escape_test.sh b/tst/escape_test.sh new file mode 100755 index 0000000..7ff8cfe --- /dev/null +++ b/tst/escape_test.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# +# escape_test.sh — Regression tests for the Klammermachine's target character +# escaping MECHANISM. +# +# Kept SKS-INDEPENDENT on purpose: tst/ tests the engine only (mac/target.cpp, +# mac/machine.cpp), not the SKS. A target is a MACHINE construct — declared +# with the @@@target system command, not owned by any klammer set — so the +# idiomatic engine-level test defines its own fixture target inline and loads +# no klammer set (`-k none`); it does not "avoid" the SKS so much as have no +# need of it. Target `t` here escapes `& -> AMP`, `_ -> UND`, `\ -> BSL` +# (arbitrary tokens, easy to assert). The SKS's own targets (tex, html) and +# the specific characters they declare are exercised by the SKS suite. +# +# The `:escape` parameter on `@@@target` declares characters special in a +# target's output and their replacements. Escaping must reach writer content +# wherever it appears — including the body of a GENERAL klammer (no target +# suffix), which is target-agnostic writer text — while leaving target-native +# content alone: +# +# - general klammer body -> writer content, ESCAPE it +# - target-specific body (.t) -> already in target form, LEAVE it +# - ^'...'^ literal span -> raw target markup, LEAVE it +# - nested target-native klammer -> e.g. @n@ -> \newline, LEAVE it +# - @eval/@read/@cond arg span -> code/path/predicate, LEAVE it +# - klammer-producing @eval result -> klammer output, LEAVE it +# +# Cases 8-9 pin a regression: escaping a general klammer body once corrupted +# the @eval CODE inside it (an underscore in "offer.Price_list(K)" became a +# KTESC marker -> Python AttributeError). Only the general body's own literal +# writer text is escaped; @eval/@read/@cond argument spans (code, filenames, +# predicates) are skipped, and the Klammertext a nested @eval produces is +# klammer output, not writer text, so it is never escaped. Mechanism: +# Klammer::m_body_generic + the hook in Machine::apply_klammer; see the +# "Target character escaping" section of CLAUDE.md. +# +# Usage: ./escape_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH) +# Exit code: 0 if all tests pass, 1 otherwise. + +PASS=0 +FAIL=0 +KTEXT=ktext +K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set} +ERR=/tmp/escape_test_err.$$ + +red=$'\033[31m' +green=$'\033[32m' +bold=$'\033[1m' +reset=$'\033[0m' + +# strip leading/trailing blank lines and trailing whitespace +trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; } + +# check_eq NAME EXPECTED KTEXT_ARGS... — exit 0 and stdout==EXPECTED. +check_eq() { + local name="$1" expected="$2"; shift 2 + local out status err + out=$("$KTEXT" "$@" 2>"$ERR"); status=$? + err=$(cat "$ERR") + out=$(printf '%s' "$out" | trim) + if [ $status -ne 0 ]; then + echo "${red}FAIL${reset} $name — ktext exited $status" + echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return + fi + if [ "$out" = "$expected" ]; then + echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) + else + echo "${red}FAIL${reset} $name" + echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1)) + fi +} + +echo "${bold}Target character escaping tests (Machine mechanism, fixture target 't')${reset}" +echo "=======================================================================" +echo + +# A self-contained fixture target, defined inline via the @@@target system +# command, escaping & _ \ to distinct tokens. -k none loads no klammer set, +# so nothing below depends on the SKS. +T='@@@target t | test target :escape & AMP _ UND \ BSL @@@' + +check_eq " 1. top-level text: & escaped" 'A AMP B' -k none -t t -s "$T A & B" +check_eq " 2. general klammer body: & escaped" 'A AMP B' -k none -t t -s "$T @@g : A & B @@ @g@" +check_eq " 3. general klammer body: _ escaped" 'AUNDB' -k none -t t -s "$T @@g : A_B @@ @g@" +check_eq " 4. general klammer body: backslash escaped" 'aBSLb' -k none -t t -s "$T @@g : a\\b @@ @g@" +check_eq " 5. target-specific body: NOT escaped" 'A & B' -k none -t t -s "$T @@g.k : d @@ @@g.t :: A & B @@ @g@" +check_eq " 6. general body ^'...'^ literal: NOT escaped" 'a&b' -k none -t t -s "$T @@g : ^'a&b'^ @@ @g@" +check_eq " 7. nested target-native klammer survives escape" 'X AMP Y \newline Z' -k none -t t -s "$T @@n.t : \\newline @@ @@g : X & Y @n@ Z @@ @g@" +# 8-9: @eval inside a general body. Code (with underscores) must not be +# escaped or Python breaks; the Klammertext it returns is klammer output and +# must not be escaped either. chr(64) builds a literal '@' so the returned +# klammer call is not parsed as one in this source line. +check_eq " 8. general body @eval code NOT escaped" '3' -k none -t t -s "$T @@g : @eval (1).__add__(2) @ @@ @g@" +check_eq " 9. general body @eval klammer result NOT escaped" '\textbf{hi}' -k none -t t -s "$T @@b.k s : d @@ @@b.t :: \\textbf{*s*} @@ @@g : @eval chr(64)+'b hi '+chr(64) @ @@ @g@" +# 10-11: an @eval result that still holds klammers is a GENERATOR (Klammertext +# with data) -- its writer text is escaped for the target before the klammers +# are applied; a result with no klammers is a RENDERER (final markup) -- left +# untouched. The signal is "does the read-back result contain a klammer". +check_eq "10. @eval generator: klammer result's data escaped" '[a AMP b]' -k none -t t -s "$T @@wrap z : [*z*] @@ @@g : @eval chr(64)+'wrap a & b '+chr(64) @ @@ @g@" +check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup' -k none -t t -s "$T @@g : @eval 'raw & markup' @ @@ @g@" + +# 12-18: quoted KLAMMERTEXT specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ literal +# regions. The katomizer strips the "^"; hide_special_katoms() and +# mark_literal_katoms() then hold the character as a KTESC marker so it stays +# inert through sub-Machine re-katomization and the @eval result read-back, +# resolving to the literal character at final processing. Regression: after +# the marker mechanism replaced the old hide/restore pass, a quoted "@" +# leaked as a bare apply-end katom into re-read text ("A klammer ends +# without a beginning"). Exact-match expectations also guard against KTESC +# markers leaking into output. +check_eq "12. quoted @ | # resolve to the characters" 'x @ | # y' -k none -t t -s "$T x ^@ ^| ^# y" +check_eq "13. quoted ^ : * resolve to the characters" 'x ^ : * y' -k none -t t -s "$T x ^^ ^: ^* y" +check_eq "14. ^'...'^ region: specials stay literal" 'a @ | b' -k none -t t -s "$T a ^' @ | '^ b" +check_eq "15. general body: quoted @ resolves" 'x @ y' -k none -t t -s "$T @@g : x ^@ y @@ @g@" +# 16: inside an @eval argument span a quoted special reaches the CODE as the +# character (the span is skipped by hide_special_katoms, like the escape pass). +check_eq "16. @eval code: quoted : reaches shell as ':'" 'x:y' -k none -t t -s "$T @@g : @eval :shell echo x^:y @ @@ @g@" +# 17: an @eval result emitting the two characters ^ @ is re-read as a quoted +# special and survives to the output as a literal @ (the generator idiom for +# a literal @; a bare @ in a result is a parse error by design). +check_eq "17. @eval result ^@ survives read-back as @" '@' -k none -t t -s "$T @@g : @eval chr(94)+chr(64) @ @@ @g@" +# 18: a bare-Python :after_apply phase receives the RESOLVED result text +# (K_result) and its return is taken as raw target text, not re-read as +# Klammertext -- a resolved @ in the result must not be re-parsed. +check_eq "18. :after_apply phase: raw result, @ intact" 'A @ B' -k none -t u -s '@@@target u | up :after_apply string.capwords @@@ a ^@ b' + +rm -f "$ERR" + +echo +echo "============================================" +echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}" +[ $FAIL -eq 0 ]