Verbatim-safe typography, ^-punctuation quoting, full-range ^UUUU^, polyglot html
Typographic transforms (---, quote pairs, ~) no longer touch verbatim text: @c/@code/@source_listing content and ^'...'^ spans show exactly the characters written. "^" before any punctuation character quotes it in every target (the apostrophe excepted: ^' opens a literal span), with the new :resolve option on @@@target declaring per-target renderings. The ^UUUU^ code-point form accepts 4-6 hex digits, the full Unicode range. The html output and transform spellings are polyglot (XML-valid), in preparation for an EPUB target. New suites: transform_test, character_test (engine), typography_test (SKS). (from dev 07ce5ea86a0a)
This commit is contained in:
@@ -14,8 +14,12 @@ test:
|
||||
./check_test.sh
|
||||
./deftype_test.sh
|
||||
./escape_test.sh
|
||||
./transform_test.sh
|
||||
./character_test.sh
|
||||
./filename_test.sh
|
||||
./alone_test.sh
|
||||
./state_test.sh
|
||||
./target_test.sh
|
||||
./modulepath_test.sh
|
||||
./klammerset_test.sh
|
||||
./option_set_test.sh
|
||||
|
||||
@@ -87,7 +87,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -233,6 +233,14 @@ check_eq \
|
||||
"[]" \
|
||||
--klammersets none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||
|
||||
echo
|
||||
# --- The pattern itself is validated at definition time (gallery review) ---
|
||||
|
||||
check_error \
|
||||
"22. an invalid :pattern is a definition-time error" \
|
||||
"is not a valid regular expression" \
|
||||
--klammersets none -s '@@@argtype bad | d :pattern ([ @@@' -d
|
||||
|
||||
echo
|
||||
echo "================================"
|
||||
echo "Passed: $PASS Failed: $FAIL"
|
||||
|
||||
81
tst/character_test.sh
Executable file
81
tst/character_test.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# character_test.sh — Regression tests for the ^-character forms the engine
|
||||
# resolves at read time: the ^UUUU^ Unicode code point (4-6 hex digits,
|
||||
# since 2026-08-23 covering the full range to U+10FFFF), and the character
|
||||
# tables (mnemonics, diacritics) it must not disturb.
|
||||
#
|
||||
# SKS-independent: character handling is katomizer/engine machinery
|
||||
# (mac/ktype.h nonascii katom, mac/character.cpp), so everything runs with
|
||||
# --klammersets none under the default (general) target.
|
||||
#
|
||||
# The 2026-08-23 changes these cases pin:
|
||||
# - a 5-digit code point converts WHOLE: ^13000^ (EGYPTIAN HIEROGLYPH
|
||||
# A001) rendered as U+1300 followed by a literal "0" before, because
|
||||
# process_unicode_codepoint() re-scanned its capture at a fixed 4-digit
|
||||
# width;
|
||||
# - 6 digits are accepted (^10FFFD^ is the top of the range; the katom
|
||||
# regex capped at {1,5});
|
||||
# - a value above U+10FFFF renders as the "Invalid Unicode" text, the
|
||||
# same treatment as a surrogate.
|
||||
#
|
||||
# Note: expected strings use $'\xHH' UTF-8 BYTE escapes, not $'\U...' code
|
||||
# points -- macOS ships bash 3.2, which does not expand \U (it failed there
|
||||
# exactly as a naive check would).
|
||||
#
|
||||
# Usage: ./character_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=$(mktemp)
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
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 NAME EXPECTED INPUT — render INPUT (no klammer set, default
|
||||
# target); exit 0 and stdout == EXPECTED.
|
||||
check() {
|
||||
local name="$1" expected="$2" input="$3"
|
||||
local out status
|
||||
out=$("$KTEXT" --klammersets none -s "$input" -d 2>"$ERR"); status=$?
|
||||
out=$(printf '%s' "$out" | trim)
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"
|
||||
head -3 "$ERR" | sed 's/^/ /'; 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}Character form tests (engine: ^UUUU^ code points and the tables)${reset}"
|
||||
echo "================================================================="
|
||||
echo
|
||||
|
||||
check " 1. 4-digit BMP code point" $'A ☺ B' 'A ^263A^ B'
|
||||
check " 2. 5-digit code point, whole" $'A \xf0\x93\x80\x80 B' 'A ^13000^ B'
|
||||
check " 3. 5-digit emoji" $'\xf0\x9f\x98\x80' '^1F600^'
|
||||
check " 4. 6-digit top of the range" $'\xf4\x8f\xbf\xbd' '^10FFFD^'
|
||||
check " 5. above U+10FFFF is invalid" 'Invalid Unicode: 16777215' '^FFFFFF^'
|
||||
check " 6. surrogate is invalid" 'Invalid Unicode: 55296' '^D800^'
|
||||
check " 7. mnemonic undisturbed" 'ß' '^s^'
|
||||
# The diacritic tables compose base + COMBINING mark (a U+0308), not the
|
||||
# precomposed letter -- the expected string is built the same way.
|
||||
check " 8. diacritic undisturbed" $'Ma\xcc\x88dchen' 'M^a"dchen'
|
||||
check " 9. code point beside quoted punctuation" $'☺ --' '^263A^ ^-^-'
|
||||
|
||||
echo
|
||||
echo "================================================================="
|
||||
echo "${bold}Results: $PASS passed, $FAIL failed${reset}"
|
||||
rm -f "$ERR"
|
||||
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
||||
@@ -49,7 +49,7 @@ error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | grep -qF -- "$pattern"; then
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF -- "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected [$pattern]"
|
||||
@@ -83,6 +83,20 @@ error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t
|
||||
error " 5. the message names the option" "-v <level>" kdesc -v
|
||||
error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v
|
||||
|
||||
echo
|
||||
echo "-- an option value that fails its pattern (gallery review) --"
|
||||
error "21. a non-integer verbosity is an error" "is not a verbosity level" kdesc -v abc
|
||||
# The location must be absent, not a C++ source file: the default Locator
|
||||
# captures the throw site (argv.cpp, line N) unless the throw says
|
||||
# Locator::none() -- the leak class the error gallery found 2026-08-22.
|
||||
out=$(kdesc -v abc 2>&1)
|
||||
if printf '%s' "$out" | grep -q '\.cpp, line'; then
|
||||
echo "${red}FAIL${reset} 22. the error leaks a C++ source location"
|
||||
echo " got: $(printf '%s' "$out" | grep '\.cpp, line' | head -1)"; FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "${green}PASS${reset} 22. no C++ source location in the error"; PASS=$((PASS+1))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the same options WITH a value still work --"
|
||||
# NOT "kdesc -v 1": show_usage() treats exactly "<command> -v <n>" as a
|
||||
@@ -103,7 +117,7 @@ echo "-- two required positional arguments --"
|
||||
if [ -x "$TSTDIR/argv_test" ]; then
|
||||
out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
|
||||
for expect in "<input> : first" "<input2> : second"; do
|
||||
if printf '%s' "$out" | grep -qF "$expect"; then
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$expect"; then
|
||||
echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1))
|
||||
|
||||
@@ -79,7 +79,7 @@ check_contains() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$needle"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -105,7 +105,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
@@ -106,7 +106,7 @@ check_error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if echo "$out" | grep -qF "$pattern"; then
|
||||
if echo "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
|
||||
11
tst/editor/indent_literal_c.kt
Normal file
11
tst/editor/indent_literal_c.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
@document :text
|
||||
@i before @c @right c@ after @ @x@
|
||||
@note
|
||||
inside the note
|
||||
@
|
||||
@c content with basic@ inside c@ and after
|
||||
@c
|
||||
kept as-is # not a comment
|
||||
c@
|
||||
after everything
|
||||
@
|
||||
11
tst/editor/indent_literal_c_expected.kt
Normal file
11
tst/editor/indent_literal_c_expected.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
@document :text
|
||||
@i before @c @right c@ after @ @x@
|
||||
@note
|
||||
inside the note
|
||||
@
|
||||
@c content with basic@ inside c@ and after
|
||||
@c
|
||||
kept as-is # not a comment
|
||||
c@
|
||||
after everything
|
||||
@
|
||||
@@ -81,6 +81,22 @@ call s:Check('abbreviated @name-', s:Syn(9, 1) ==# 'klammertextAppOpen'
|
||||
\ && s:Syn(9, 6) ==# '' && s:Syn(9, 7) ==# '', s:Syn(9, 6))
|
||||
call s:Check('file marker ##', s:Syn(10, 1) ==# 'klammertextMarker', s:Syn(10, 1))
|
||||
call s:Check('## removes to EOF', s:Syn(11, 3) ==# 'klammertextRemovedFile', s:Syn(11, 3))
|
||||
|
||||
" @c is the single-letter literal klammer (inline @code, 2026-08-16). A
|
||||
" fresh buffer: the ## in the buffer above removes to EOF. The traps a
|
||||
" single-letter name adds: a word ending in c ("basic@") must not close the
|
||||
" region, and "@caption" must not open one.
|
||||
enew!
|
||||
call setline(1, [
|
||||
\ '@c y @ basic@ z c@ tail',
|
||||
\ '@caption arg @'])
|
||||
set filetype=klammertext
|
||||
call s:Check('c verbatim open @c', s:Syn(1, 1) ==# 'klammertextAppOpen', s:Syn(1, 1))
|
||||
call s:Check('c interior @ verbatim', s:Syn(1, 6) ==# 'klammertextVerbatim', s:Syn(1, 6))
|
||||
call s:Check('basic@ does not close @c', s:Syn(1, 13) ==# 'klammertextVerbatim', s:Syn(1, 13))
|
||||
call s:Check('real close c@', s:Syn(1, 17) ==# 'klammertextAppClose', s:Syn(1, 17))
|
||||
call s:Check('text after c@ plain', s:Syn(1, 20) ==# '', s:Syn(1, 20))
|
||||
call s:Check('@caption opens @caption, not @c', s:Syn(2, 1) ==# 'klammertextAppOpen', s:Syn(2, 1))
|
||||
else
|
||||
call add(s:results, 'SKIP syntax checks (this Vim lacks +syntax)')
|
||||
endif
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
# tst/ is the SKS-independent tier, and these suites check that the
|
||||
# implementations of Klammertext's structure agree with the canonical
|
||||
# formatting conventions AND with each other. The klammer names that appear
|
||||
# in the fixtures (@ol, @table, @document, @code) are seeded configuration of
|
||||
# in the fixtures (@ol, @table, @document, @code, @c) are seeded configuration of
|
||||
# the editor tools, not SKS dependencies; nothing here runs ktext or loads a
|
||||
# klammer set.
|
||||
#
|
||||
@@ -60,7 +60,7 @@ OUT="$(mktemp -d)"
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
INDENT_FIXTURES="indent_list indent_document indent_table indent_untouched
|
||||
indent_defs indent_escapes indent_named_close"
|
||||
indent_defs indent_escapes indent_named_close indent_literal_c"
|
||||
ALIGN_FIXTURES="align_mixed align_empty_cells align_boundary align_colspan
|
||||
align_escapes align_too_wide"
|
||||
|
||||
|
||||
@@ -183,6 +183,33 @@ check_eq "25. a real literal klammer still works after a comment naming it" \
|
||||
|
||||
rm -f "$ERR"
|
||||
|
||||
echo
|
||||
echo "-- 26-29: a general body escapes only its OWN literal text, never a --"
|
||||
echo "-- substituted argument value (ordering defect fixed 2026-08-19) --"
|
||||
# An argument value is writer text already escaped at the top level (KTESC
|
||||
# markers, idempotent) PLUS final target markup from klammers the writer
|
||||
# nested in the argument. apply_klammer used to substitute values first and
|
||||
# escape the general body after, so that markup was swept as if it were the
|
||||
# body's literal text: "@sig @b Andy @ @" emitted BSLtextbf{Andy}. The escape
|
||||
# now runs before substitution. BOLD is a target-specific nested klammer
|
||||
# (case 7/9 pattern) whose raw output must survive untouched.
|
||||
BOLD='@@b.k s : d @@ @@b.t :: \textbf{*s*} @@'
|
||||
check_eq "26. substituted klammer output NOT re-escaped" \
|
||||
'Sincerely, \textbf{Andy}' --klammersets none -t t \
|
||||
-s "$T $BOLD @@sig name : Sincerely, *name* @@ @sig @b Andy @ @"
|
||||
check_eq "27. ... while the body's own literal & still IS escaped" \
|
||||
'Fish AMP \textbf{Chips}' --klammersets none -t t \
|
||||
-s "$T $BOLD @@sig2 name : Fish & *name* @@ @sig2 @b Chips @ @"
|
||||
check_eq "28. writer specials in the argument still escaped (markers)" \
|
||||
'Sincerely, a AMP b' --klammersets none -t t \
|
||||
-s "$T @@sig name : Sincerely, *name* @@ @sig a & b @"
|
||||
# The variable shares one text katom with literal specials on both sides:
|
||||
# the katom's literal "_"s must be escaped and the value spliced in raw
|
||||
# afterwards -- the ordering this section exists to pin.
|
||||
check_eq "29. mixed katom: literal specials escaped, spliced value raw" \
|
||||
'AUND\textbf{Q}UNDB' --klammersets none -t t \
|
||||
-s "$T $BOLD @@w s : A_*s*_B @@ @w @b Q @ @"
|
||||
|
||||
echo
|
||||
echo "============================================"
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
|
||||
@@ -219,6 +219,28 @@ main = putStrLn "partial" >> exitWith (ExitFailure 3) @'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the environment can be missing: the errors name what and how --"
|
||||
# Added 2026-08-22 by the error-gallery coverage review: neither message had
|
||||
# ever been printed by a test.
|
||||
run '@eval :cpp nonexistent_gallery_lib @'
|
||||
if [ $STATUS -ne 0 ] && grep -qF "Cannot open library" "$ERRF"; then
|
||||
pass "19. :cpp names the library it could not open"
|
||||
else
|
||||
fail "19. exit $STATUS" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
# :haskell without runghc: strip PATH so the case is deterministic whether or
|
||||
# not GHC is installed; ktext itself is invoked by absolute path (which works
|
||||
# since the 2026-08-21 construct_command_pathname fix).
|
||||
env PATH=/nonexistent "$K/bin/ktext" --klammersets none \
|
||||
-s '@eval :haskell main = putStrLn "x" @' -d >"$OUTF" 2>"$ERRF"
|
||||
hstatus=$?
|
||||
if [ $hstatus -ne 0 ] && tr '\n' ' ' < "$ERRF" | grep -qF "requires runghc"; then
|
||||
pass "20. :haskell without runghc says what to install"
|
||||
else
|
||||
fail "20. exit $hstatus" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "==========="
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
|
||||
@@ -84,7 +84,7 @@ $source" -t fix -d 2>&1)
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
||||
if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -121,7 +121,7 @@ $source" -t fix -d -v 1 2>&1)
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
||||
if printf '%s' "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$needle"; then
|
||||
echo "${green}PASS${reset} $name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -270,7 +270,7 @@ check_fails "22. a set with no parameters is rejected" \
|
||||
'@@bad.o : nothing at all @@'
|
||||
|
||||
check_fails '23. "::" has no meaning for a set' \
|
||||
'An option set IS a declaration' \
|
||||
'An option set is a declaration' \
|
||||
'@@cap.o :: nope @@'
|
||||
|
||||
# --- Collisions -----------------------------------------------------------
|
||||
|
||||
@@ -55,7 +55,7 @@ check_error() {
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
if echo "$output" | grep -qF "$pattern"; then
|
||||
if echo "$output" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $test_name"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
|
||||
121
tst/state_test.sh
Executable file
121
tst/state_test.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# state_test.sh — The @@@state system command and *name* substitution.
|
||||
#
|
||||
# Added 2026-08-22 by the error-gallery coverage review, which found that no
|
||||
# suite covered @@@state at all: its two error messages had never been
|
||||
# printed by any test. What is pinned here, each by outcome:
|
||||
#
|
||||
# * :value sets a variable and *name* substitutes it in a klammer BODY
|
||||
# (bodies are processed at application time). A top-level *name* of a
|
||||
# document-declared variable does NOT substitute — the same read-time
|
||||
# asymmetry @cond had before 2026-08-15; if that is ever changed, case 2
|
||||
# documents today's behaviour and should change with it.
|
||||
# * :replace replaces an existing value.
|
||||
# * Redefining WITHOUT :replace is an error that names the remedy.
|
||||
# * An undefined variable's error lists the frames it searched.
|
||||
# * A shell environment variable substitutes like any state variable.
|
||||
#
|
||||
# NOT to be confused with "state_test", the C++ diagnostic program built from
|
||||
# state_test.cpp in this directory: that one constructs engine objects and
|
||||
# prints what it gets, for a person to read, and asserts nothing (see the
|
||||
# `smoke` target in tst/Makefile). This is the regression suite. The ".sh"
|
||||
# is the only thing distinguishing them — the same collision as
|
||||
# eval_test/eval_test.sh, documented there first.
|
||||
#
|
||||
# Usage: ./state_test.sh (needs ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
export KT_STATE_PROBE="probe-value" # for the environment-variable case
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
# check_eq NAME EXPECTED KTEXT_ARGS... — trimmed stdout equals EXPECTED.
|
||||
check_eq() {
|
||||
local name="$1" expected="$2"
|
||||
shift 2
|
||||
local out status
|
||||
out=$("$KTEXT" "$@" 2>/dev/null)
|
||||
status=$?
|
||||
out=$(printf '%s' "$out" | sed -e 's/[ \t]*$//' | grep -v '^$')
|
||||
if [ $status -eq 0 ] && [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out] (exit $status)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# check_error NAME PATTERN KTEXT_ARGS... — nonzero exit, PATTERN in the
|
||||
# message. Wrap-insensitive: error prose is justified to 80 columns, so a
|
||||
# phrase may wrap anywhere.
|
||||
check_error() {
|
||||
local name="$1" pattern="$2"
|
||||
shift 2
|
||||
local out status
|
||||
out=$("$KTEXT" "$@" 2>&1)
|
||||
status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
echo " got: $(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | tr '\n' ' ' | head -c 200)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}@@@state tests${reset}"
|
||||
echo "=============="
|
||||
echo
|
||||
|
||||
echo "-- setting and substituting --"
|
||||
check_eq " 1. :value sets; *name* substitutes in a body" \
|
||||
"[hello]" \
|
||||
--klammersets none -s '@@@state V :value hello @@@ @@f : [*V*] @@ @f@' -d
|
||||
|
||||
# Documents TODAY'S behaviour: a top-level *name* of a document-declared
|
||||
# variable is left as written (bodies substitute; the top level does not).
|
||||
check_eq " 2. a top-level *name* does not substitute (current behaviour)" \
|
||||
"[*V*]" \
|
||||
--klammersets none -s '@@@state V :value hello @@@ [*V*]' -d
|
||||
|
||||
check_eq " 3. an environment variable substitutes" \
|
||||
"[probe-value]" \
|
||||
--klammersets none -s '@@f : [*KT_STATE_PROBE*] @@ @f@' -d
|
||||
|
||||
echo
|
||||
echo "-- replacing --"
|
||||
check_eq " 4. :replace replaces an existing value" \
|
||||
"[b]" \
|
||||
--klammersets none \
|
||||
-s '@@@state V :value a @@@ @@@state V :replace b @@@ @@f : [*V*] @@ @f@' -d
|
||||
|
||||
check_error " 5. redefining without :replace is an error naming the remedy" \
|
||||
"Use ':replace <new-value>' to replace the current value" \
|
||||
--klammersets none -s '@@@state V :value a @@@ @@@state V :value b @@@' -d
|
||||
|
||||
echo
|
||||
echo "-- the undefined variable --"
|
||||
check_error " 6. an undefined *name* in a body is a located error" \
|
||||
'Variable "Missing" not defined' \
|
||||
--klammersets none -s '@@f : [*Missing*] @@ @f@' -d
|
||||
|
||||
check_error " 7. ... that lists the frames it searched" \
|
||||
"(searched:" \
|
||||
--klammersets none -s '@@f : [*Missing*] @@ @f@' -d
|
||||
|
||||
echo
|
||||
echo "=============="
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
[ $FAIL -eq 0 ]
|
||||
@@ -109,7 +109,7 @@ check_error() {
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if echo "$out" | grep -qF "$pattern"; then
|
||||
if echo "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
|
||||
133
tst/target_test.sh
Executable file
133
tst/target_test.sh
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# target_test.sh — The @@@target system command's :after_apply phase list.
|
||||
#
|
||||
# Added 2026-08-22, when the txt target became the first to declare TWO
|
||||
# phases and found the list separator undocumented and unguarded: two bare
|
||||
# specs written without " ; " were glued into one Python call and failed at
|
||||
# render time with a SyntaxError located at "phase, line 1" rather than at
|
||||
# the declaration. What is pinned here:
|
||||
#
|
||||
# * several phases separated by " ; " run, in order, each seeing its
|
||||
# predecessor's result;
|
||||
# * a bare spec containing whitespace is a DEFINITION-TIME error naming
|
||||
# the " ; " convention (the guard in Target::add_after_apply);
|
||||
# * a mode-tagged spec (":cpp <library> <function>") is exempt from the
|
||||
# whitespace guard -- its library is a filename, and filenames may
|
||||
# contain spaces, which is why the separator is ";" at all.
|
||||
#
|
||||
# SKS-independent: --klammersets none, an inline fixture target, and a
|
||||
# fixture Python module placed next to the input file (the module-resolution
|
||||
# rule adds the input file's directory to sys.path).
|
||||
#
|
||||
# NOT to be confused with "target_test", the C++ diagnostic program built
|
||||
# from target_test.cpp in this directory (asserts nothing; see the `smoke`
|
||||
# target in tst/Makefile). The ".sh" is the only distinguisher -- the third
|
||||
# such collision, after eval_test and state_test.
|
||||
#
|
||||
# Usage: ./target_test.sh (needs ktext on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$DIR"' EXIT
|
||||
|
||||
# Two phases that mark their passage; order is observable in the output.
|
||||
cat > "$DIR/phasefix.py" <<'EOF'
|
||||
def one(text, K=None):
|
||||
return text + "+ONE"
|
||||
|
||||
def two(text, K=None):
|
||||
return text + "+TWO"
|
||||
|
||||
def width(text, K=None):
|
||||
# K is the state's "class K"; a fixture state variable proves the phase
|
||||
# receives it (attributes arrive as strings unless the argtype is typed).
|
||||
return text + "|" + str(K.Fixture_width)
|
||||
EOF
|
||||
|
||||
check_eq() { # check_eq NAME EXPECTED INPUT_FILE_TEXT TARGET_DECL
|
||||
local name="$1" expected="$2" body="$3" decl="$4"
|
||||
printf '%s' "$body" > "$DIR/in.kt"
|
||||
local out status
|
||||
out=$("$KTEXT" "$DIR/in.kt" --klammersets none -s "$decl" -t t -d 2>/dev/null)
|
||||
status=$?
|
||||
out=$(printf '%s' "$out" | tr -d '\n')
|
||||
if [ $status -eq 0 ] && [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
echo " expected: [$expected]"; echo " got: [$out] (exit $status)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_def_error() { # check_def_error NAME PATTERN TARGET_DECL
|
||||
local name="$1" pattern="$2" decl="$3"
|
||||
local out status
|
||||
out=$("$KTEXT" --klammersets none -s "$decl hello" -t t -d 2>&1)
|
||||
status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected a definition error but ktext succeeded"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if printf '%s' "$out" | tr '\n' ' ' | tr -s ' ' | grep -qF "$pattern"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
|
||||
echo " got: $(printf '%s' "$out" | sed 's/\x1b\[[0-9;]*m//g' | tr '\n' ' ' | head -c 200)"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "${bold}@@@target :after_apply tests${reset}"
|
||||
echo "============================"
|
||||
echo
|
||||
|
||||
echo "-- several phases: ';'-separated, run in declared order --"
|
||||
check_eq " 1. one phase runs" \
|
||||
"text+ONE" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.one @@@'
|
||||
check_eq " 2. two phases run in order (ONE then TWO)" \
|
||||
"text+ONE+TWO" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.one ; phasefix.two @@@'
|
||||
check_eq " 3. ... and reversing the list reverses the order" \
|
||||
"text+TWO+ONE" "text" \
|
||||
'@@@target t | test | :after_apply phasefix.two ; phasefix.one @@@'
|
||||
|
||||
echo
|
||||
echo "-- a phase receives the state as K --"
|
||||
check_eq " 4. K.<state variable> is readable inside a phase" \
|
||||
"text|42" "text" \
|
||||
'@@@state Fixture_width :value 42 @@@ @@@target t | test | :after_apply phasefix.width @@@'
|
||||
|
||||
echo
|
||||
echo "-- the missing separator is a DEFINITION-time error --"
|
||||
check_def_error " 5. two bare specs without ';' are rejected at the declaration" \
|
||||
"separated by \" ; \"" \
|
||||
'@@@target t | test | :after_apply phasefix.one phasefix.two @@@'
|
||||
check_def_error " 6. ... and the error names the offending spec" \
|
||||
"phasefix.one phasefix.two" \
|
||||
'@@@target t | test | :after_apply phasefix.one phasefix.two @@@'
|
||||
|
||||
echo
|
||||
echo "-- a mode-tagged spec may contain spaces (its library is a filename) --"
|
||||
# Declaration-time only: the :cpp library is never dlopened unless the
|
||||
# target renders, so declaring it under another name proves the exemption
|
||||
# without needing a real library.
|
||||
check_eq " 7. a ':cpp lib func' spec passes the declaration guard" \
|
||||
"text+ONE" "text" \
|
||||
'@@@target u | unused | :after_apply ^:cpp some lib func @@@ @@@target t | test | :after_apply phasefix.one @@@'
|
||||
|
||||
echo
|
||||
echo "============================"
|
||||
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||
[ $FAIL -eq 0 ]
|
||||
109
tst/transform_test.sh
Executable file
109
tst/transform_test.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# transform_test.sh — Regression tests for the Klammermachine's typographic
|
||||
# TRANSFORM pass (the third positional of @@@target).
|
||||
#
|
||||
# Kept SKS-INDEPENDENT on purpose: a target is a Machine construct, so the
|
||||
# fixture target is declared inline and no klammer set is loaded. The SKS's
|
||||
# own transform tables (html, txt, tex) are exercised by
|
||||
# sks/tst/typography_test.sh.
|
||||
#
|
||||
# What the pass IS: each pair replaces a source character sequence in the
|
||||
# final output with its target spelling (the LaTeX input conventions carried
|
||||
# to other targets: --- to an em dash, quote pairs, ~ to a non-breaking
|
||||
# space). Since 2026-08-23 it runs PER KATOM at final processing, not over
|
||||
# the joined result string, which is what these cases pin:
|
||||
#
|
||||
# - ordinary writer text -> transformed
|
||||
# - ^'...'^ literal span (katom_t::literal) -> NEVER transformed
|
||||
# - a KTESC marker in the text -> never matched; decodes after
|
||||
# the pass (this is the contract hide_typographic() in the SKS's
|
||||
# klammer_base.py relies on to protect @c/@code verbatim text)
|
||||
# - a plain-text @eval result (a renderer) -> transformed like writer text
|
||||
# - a source split across a katom boundary -> NOT transformed (the writer
|
||||
# separated the characters structurally; they are not a dash)
|
||||
# - :includes -> transforms are inherited,
|
||||
# as escapes are
|
||||
# - the pairs run in declared order
|
||||
#
|
||||
# The transform/escape interplay is also pinned: escapes become markers
|
||||
# before klammer application, transforms run at final processing, markers
|
||||
# resolve last — so the two mechanisms cannot corrupt each other's output.
|
||||
#
|
||||
# Usage: ./transform_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/transform_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}Typographic transform tests (Machine mechanism, fixture target 't')${reset}"
|
||||
echo "==================================================================="
|
||||
echo
|
||||
|
||||
# The fixture target: one escape (& -> AMP) and two transforms, whose
|
||||
# replacements are distinct tokens that are easy to assert.
|
||||
T='@@@target t | test target :escape & AMP | --- MDASH | -- NDASH @@@'
|
||||
|
||||
check_eq " 1. writer text: --- transformed" 'a MDASH b' --klammersets none -t t -s "$T a --- b"
|
||||
check_eq " 2. pairs in declared order: -- after ---" 'a NDASH b' --klammersets none -t t -s "$T a -- b"
|
||||
check_eq " 3. literal span: NOT transformed" 'a -- b' --klammersets none -t t -s "$T ^'a -- b'^"
|
||||
check_eq " 4. escape and transform coexist" 'x AMP y NDASH' --klammersets none -t t -s "$T x & y --"
|
||||
# 5-6: the @eval sits in a klammer BODY, applied after the fixture target
|
||||
# is extracted — a top-level @eval runs at read time, before same-input
|
||||
# @@@ extraction (the documented pass-ordering caveat), and would not
|
||||
# find target t.
|
||||
check_eq " 5. renderer @eval result: transformed" 'aNDASHb' --klammersets none -t t -s "$T @@e.t : @eval \"a--b\" @ @@ @e@"
|
||||
check_eq " 6. KTESC marker: skipped, then decoded" '--' --klammersets none -t t -s "$T @@e.t : @eval \"KTESC002dKTESC\" * 2 @ @@ @e@"
|
||||
check_eq " 7. source across a katom boundary: NOT transformed" '- x -' --klammersets none -t t -s "$T @@g : x @@ - @g@ -"
|
||||
|
||||
# 8. :includes — an including target inherits the included target's
|
||||
# transforms (and escapes), so a target built over another renders the
|
||||
# same writer conventions.
|
||||
T2="$T @@@target t2 | over t :includes t @@@"
|
||||
check_eq " 8. :includes inherits transforms" 'a NDASH b AMP c' --klammersets none -t t2 -s "$T2 a -- b & c"
|
||||
|
||||
# 9-12: "^" before any punctuation character quotes it
|
||||
# (hide_quoted_punctuation, 2026-08-23): the pair becomes the KTESC marker
|
||||
# of the character, which the transform pass cannot match. The target
|
||||
# decides the rendering — a :resolve entry, an :escape entry, or (neither)
|
||||
# the raw character. Inside an @eval span the code keeps its carets.
|
||||
check_eq " 9. quoted hyphens: no transform, decode raw" '--' --klammersets none -t t -s "$T ^-^-"
|
||||
check_eq "10. :resolve maps a quoted character" 'LIT-' --klammersets none -t r -s '@@@target r | r :resolve - LIT- @@@ ^-'
|
||||
check_eq "11. quoted char with an :escape entry takes its replacement" 'AMP' --klammersets none -t t -s "$T ^&"
|
||||
check_eq "12. @eval code keeps its carets" '2' --klammersets none -t t -s "$T @@e.t : @eval 3 ^(1) @ @@ @e@"
|
||||
|
||||
echo
|
||||
echo "===================================================================="
|
||||
echo "${bold}Results: $PASS passed, $FAIL failed${reset}"
|
||||
rm -f "$ERR"
|
||||
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user