A character written in the ^...^ mnemonic form -- ^oe^, ^zz^, ^hello^ --
used to kill ktext outright: no message, no output, and an exit status
that looked like a crash because it was one. Only the forms that happen
to spell hexadecimal digits escaped, and they were right by luck rather
than by reading. Any caret form that names no character is now a located
error that quotes what was written.
A klammer may now sit inside square brackets and braces as it already
could inside parentheses, and may be followed by the punctuation that
ends a sentence: [@i word@], {@b word@}, @i word@'s. The rule is a fixed
list of characters, because none of them can occur inside a klammer name
and so none of them is ambiguous next to a delimiter. A letter still
cannot: "x@" is the named closing delimiter, so a letter before "@" has
two readings and no rule can separate them.
The abbreviations an author writes -- the hyphen form @i-word, a klammer
glued to its neighbouring punctuation -- are now recognised as author
conveniences only. They are not applied to @@ definitions or @@@ system
commands, which are written by someone who needs no such shorthand and
whose forms are already whole.
Also fixed, each of them silent: an exported shell function in the
environment broke every @eval in the document; a line break written with
@nl was destroyed when it passed through a klammer that generates a
table, printing as \textbackslash{}newline in the finished PDF; and
"kdesc --coverage" shortened its file column by comparing the spelling of
a path rather than the path, so a klammer set reached by a symbolic link
showed a full path where its neighbours showed a short one.
New in the distribution: ref/, a small klammerset that describes the
Klammermachine's own tables in Klammertext. It currently provides the
character tables -- the diacritics and the extended Latin letters -- as
klammers that render to any target, so the documentation of what can be
written cannot drift from what the machine accepts. It is early, and
will grow.
The Klammertext taxonomy now has a section on targets, describes the
@@@klammerset system command alongside the other three, and states the
precedence of "^" -- what a caret means before a letter, a digit, a
punctuation character, and a quote, in one ordered list.
(from dev 6594c7ff8c1f)
294 lines
12 KiB
Bash
Executable File
294 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# editor_test.sh — Regression tests for the Klammertext editor support
|
|
# (doc/edit/: the shared Python core, the language server, and the Emacs,
|
|
# Sublime Text, Vim, and VS Code integrations built on them).
|
|
#
|
|
# Why this lives in tst/: the editor code implements the LANGUAGE's structural
|
|
# layer — @-run length, bar-run dimension, ^-escapes, # removal, literal
|
|
# spans, nesting depth — independently of both the SKS and the Klammermachine.
|
|
# 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, @c) are seeded configuration of
|
|
# the editor tools, not SKS dependencies; nothing here runs ktext or loads a
|
|
# klammer set.
|
|
#
|
|
# What is checked, for every fixture pair <name>.kt / <name>_expected.kt in
|
|
# tst/editor/:
|
|
# * the shared core's output equals the expected file — through the Python
|
|
# API (what Sublime uses) AND through the core's CLI (what Vim uses)
|
|
# * idempotence: the tool applied to the expected file leaves it unchanged
|
|
# * the three Sublime adapter plugins import outside Sublime and resolve
|
|
# to the shared core
|
|
# * the language server passes its protocol test (tst/editor/ls_test.py:
|
|
# handshake, diagnostics, formatting, matching, alignTable — the VS Code
|
|
# extension's whole surface)
|
|
# * when Emacs is installed: the Emacs units' output equals the expected
|
|
# file, and equals the shared core's output byte for byte (the sync
|
|
# check across the independent elisp implementation) — skipped otherwise
|
|
# * when a Vim with +eval is installed: the Vim plugin's commands produce
|
|
# the same bytes (exercising the CLI shell-out glue) — skipped otherwise
|
|
#
|
|
# The editor code lives in doc/edit/ (same layout in the development tree and
|
|
# the distribution).
|
|
#
|
|
# Usage: ./editor_test.sh Requires python3; Emacs and Vim optional.
|
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
red=$'\033[31m'
|
|
green=$'\033[32m'
|
|
bold=$'\033[1m'
|
|
reset=$'\033[0m'
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT="$(cd "$HERE/.." && pwd)"
|
|
EDIT_DIR="$ROOT/doc/edit"
|
|
if [ ! -d "$EDIT_DIR/emacs" ]; then
|
|
echo "editor_test.sh: cannot locate the editor support (doc/edit)" >&2
|
|
exit 1
|
|
fi
|
|
EMACS_DIR="$EDIT_DIR/emacs"
|
|
SUBLIME_DIR="$EDIT_DIR/sublime"
|
|
SHARED_DIR="$EDIT_DIR/shared"
|
|
VIM_DIR="$EDIT_DIR/vim"
|
|
FIX="$HERE/editor"
|
|
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_literal_c"
|
|
ALIGN_FIXTURES="align_mixed align_empty_cells align_boundary align_colspan
|
|
align_escapes align_too_wide"
|
|
|
|
# Build the (MODE INFILE OUTFILE) triples: each fixture is run from its input
|
|
# and from its expected file (the idempotence check).
|
|
py_args=()
|
|
el_args=()
|
|
add_fixture() { # add_fixture MODE NAME
|
|
local mode="$1" name="$2"
|
|
py_args+=("$mode" "$FIX/$name.kt" "$OUT/$name.py.out")
|
|
py_args+=("$mode" "$FIX/${name}_expected.kt" "$OUT/$name.py.idem")
|
|
py_args+=("$mode-cli" "$FIX/$name.kt" "$OUT/$name.cli.out")
|
|
el_args+=("$mode" "$FIX/$name.kt" "$OUT/$name.el.out")
|
|
el_args+=("$mode" "$FIX/${name}_expected.kt" "$OUT/$name.el.idem")
|
|
}
|
|
for f in $INDENT_FIXTURES; do add_fixture indent "$f"; done
|
|
for f in $ALIGN_FIXTURES; do add_fixture align "$f"; done
|
|
|
|
check() { # check NAME FILE_A FILE_B
|
|
if diff -q "$2" "$3" >/dev/null 2>&1; then
|
|
echo "${green}PASS${reset} $1"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} $1"
|
|
diff "$2" "$3" | head -10
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# --- the shared core: API and CLI (required) -------------------------------
|
|
if ! command -v python3 >/dev/null 2>&1; then
|
|
echo "editor_test.sh: python3 not found" >&2
|
|
exit 1
|
|
fi
|
|
if ! PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/editor_driver.py" "$SHARED_DIR" "${py_args[@]}"; then
|
|
echo "editor_test.sh: the Python driver failed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- the Sublime adapters resolve to the shared core -----------------------
|
|
if PYTHONDONTWRITEBYTECODE=1 python3 -c "
|
|
import sys
|
|
sys.path.insert(0, '$SUBLIME_DIR')
|
|
import Klammertext, Klammertext_indent, Klammertext_align
|
|
assert Klammertext.KE.__file__.startswith('$SHARED_DIR')
|
|
" 2>"$OUT/sublime.log"; then
|
|
echo "${green}PASS${reset} sublime adapters import the shared core"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} sublime adapters import the shared core"
|
|
cat "$OUT/sublime.log"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# --- the language server protocol test -------------------------------------
|
|
if PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/ls_test.py" "$SHARED_DIR" "$FIX" >"$OUT/ls.log" 2>&1; then
|
|
echo "${green}PASS${reset} language server protocol ($(grep -c '^PASS' "$OUT/ls.log") checks)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} language server protocol:"
|
|
cat "$OUT/ls.log"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# run_bounded SECONDS CMD... -- run CMD with a wall-clock limit, returning
|
|
# 137 if it had to be killed. An external editor that is INSTALLED BUT
|
|
# NON-FUNCTIONAL is not hypothetical: Olion's Homebrew emacs blocks forever
|
|
# on "emacs --batch" with no CPU used, and because this suite had no bound it
|
|
# sat for 2h43m producing nothing (2026-09-05). A hang is worse than a
|
|
# failure -- it is indistinguishable from slowness, so nobody investigates.
|
|
#
|
|
# No "timeout(1)": macOS does not have it (nor gtimeout without coreutils),
|
|
# and this suite ships. Background the command, race a killer against it.
|
|
# "set -m" makes the job its own PROCESS GROUP so the whole group can be
|
|
# killed. Killing only the pid leaves the children: a hung editor's orphaned
|
|
# child still holds the inherited stdout, and anything reading this suite
|
|
# through a pipe then blocks anyway -- the timeout fires and the hang
|
|
# continues, which is the worst of both. Measured while writing this.
|
|
run_bounded() {
|
|
local secs="$1"; shift
|
|
set -m
|
|
"$@" & local pid=$!
|
|
set +m
|
|
( sleep "$secs"; kill -9 -"$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null ) \
|
|
>/dev/null 2>&1 & local killer=$!
|
|
wait "$pid" 2>/dev/null; local st=$?
|
|
kill "$killer" 2>/dev/null; wait "$killer" 2>/dev/null
|
|
return $st
|
|
}
|
|
|
|
EDITOR_TIMEOUT=${EDITOR_TIMEOUT:-120}
|
|
|
|
# --- the Emacs units (optional) --------------------------------------------
|
|
HAVE_EMACS=0
|
|
if command -v emacs >/dev/null 2>&1; then
|
|
run_bounded "$EDITOR_TIMEOUT" \
|
|
emacs --batch -L "$EMACS_DIR" \
|
|
-l klammertext-mode -l klammertext-indent -l klammertext-align \
|
|
-l "$FIX/editor_driver.el" "${el_args[@]}" 2>"$OUT/emacs.log"
|
|
emacs_status=$?
|
|
if [ "$emacs_status" = 0 ]; then
|
|
HAVE_EMACS=1
|
|
elif [ "$emacs_status" = 137 ]; then
|
|
# Distinguished from a failure on purpose: "it hung" and "it said no"
|
|
# need different investigations, and only one of them is about
|
|
# Klammertext.
|
|
echo "${red}FAIL${reset} the Emacs driver did not finish within" \
|
|
"${EDITOR_TIMEOUT}s and was killed (emacs is installed but not" \
|
|
"working on this machine)" >&2
|
|
FAIL=$((FAIL + 1))
|
|
else
|
|
echo "${red}FAIL${reset} the Emacs driver failed:" >&2
|
|
cat "$OUT/emacs.log" >&2
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo "(Emacs not installed — the Emacs half is skipped; the shared core still runs)"
|
|
fi
|
|
|
|
# --- the Vim plugin (optional) ---------------------------------------------
|
|
HAVE_VIM=0
|
|
VIM_BIN="$(command -v vim || true)"
|
|
if [ -n "$VIM_BIN" ] && "$VIM_BIN" --version 2>/dev/null | grep -q '+eval'; then
|
|
HAVE_VIM=1
|
|
run_vim() { # run_vim MODE INFILE OUTFILE
|
|
local cmd
|
|
if [ "$1" = indent ]; then
|
|
cmd='KlammertextReindent'
|
|
else
|
|
cmd='call search("|") | KlammertextAlign'
|
|
fi
|
|
"$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
|
|
--cmd "set rtp^=$VIM_DIR" \
|
|
-c 'filetype plugin on' \
|
|
-c "edit! $2" -c 'set ft=klammertext' \
|
|
-c "$cmd" -c "saveas! $3" -c 'qa!' </dev/null >/dev/null 2>&1
|
|
}
|
|
for f in $INDENT_FIXTURES; do
|
|
run_vim indent "$FIX/$f.kt" "$OUT/$f.vim.out"
|
|
run_vim indent "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
|
|
done
|
|
for f in $ALIGN_FIXTURES; do
|
|
run_vim align "$FIX/$f.kt" "$OUT/$f.vim.out"
|
|
run_vim align "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
|
|
done
|
|
# The comprehensive plugin checks: ftdetect (Kotlin override), syntax
|
|
# token classes, jump-to-match (incl. multibyte columns), the location-
|
|
# list check, and — with +python3 — indentexpr (gg=G) and the live
|
|
# match highlighter. Sections skip inside the driver per Vim feature.
|
|
if KT_FIX="$FIX" KT_OUT="$OUT/vim_feature.txt" \
|
|
"$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
|
|
--cmd "set rtp^=$VIM_DIR" \
|
|
-c "source $FIX/vim_feature_test.vim" </dev/null >/dev/null 2>&1; then
|
|
echo "${green}PASS${reset} vim plugin features ($(grep -c '^PASS' "$OUT/vim_feature.txt") checks$(
|
|
grep -q '^SKIP' "$OUT/vim_feature.txt" && printf '; %s' "$(grep -c '^SKIP' "$OUT/vim_feature.txt") section(s) skipped"))"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} vim plugin features:"
|
|
grep -v '^PASS' "$OUT/vim_feature.txt" 2>/dev/null || echo " (the Vim driver itself failed)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo "(no Vim with +eval installed — the Vim half is skipped; the shared core still runs)"
|
|
fi
|
|
|
|
# --- the VS Code extension (optional: needs a Node runtime) ----------------
|
|
# node itself, or VS Code's Electron binary run as Node.
|
|
run_node() {
|
|
if command -v node >/dev/null 2>&1; then
|
|
node "$@"
|
|
elif [ -x /usr/share/code/code ]; then
|
|
ELECTRON_RUN_AS_NODE=1 /usr/share/code/code "$@"
|
|
else
|
|
return 127
|
|
fi
|
|
}
|
|
if command -v node >/dev/null 2>&1 || [ -x /usr/share/code/code ]; then
|
|
if run_node "$FIX/vscode_ext_test.js" "$EDIT_DIR/vscode" "$FIX" >"$OUT/vscode.log" 2>&1; then
|
|
echo "${green}PASS${reset} vscode extension ($(grep -c '^PASS' "$OUT/vscode.log") checks)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} vscode extension:"
|
|
cat "$OUT/vscode.log"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
else
|
|
echo "(no Node runtime — the VS Code extension test is skipped; the language server is still tested)"
|
|
fi
|
|
|
|
# --- the VS Code .vsix package builds with the full payload ----------------
|
|
# (the no-marketplace install path; needs only bash + python3)
|
|
mkdir -p "$OUT/vsix"
|
|
cp -R "$EDIT_DIR/vscode" "$EDIT_DIR/shared" "$OUT/vsix/"
|
|
if ( cd "$OUT/vsix/vscode" && ./make_vsix.sh >/dev/null 2>&1 ) && \
|
|
PYTHONDONTWRITEBYTECODE=1 python3 -c "
|
|
import sys, zipfile
|
|
names = set(zipfile.ZipFile('$OUT/vsix/vscode/klammertext.vsix').namelist())
|
|
need = {'extension.vsixmanifest', '[Content_Types].xml',
|
|
'extension/package.json', 'extension/extension.js',
|
|
'extension/language-configuration.json', 'extension/README.md',
|
|
'extension/klammertext_edit.py', 'extension/klammertext_ls.py',
|
|
'extension/syntaxes/klammertext.tmLanguage.json'}
|
|
sys.exit(1 if need - names else 0)
|
|
"; then
|
|
echo "${green}PASS${reset} vscode .vsix package builds (make_vsix.sh)"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} vscode .vsix package builds (make_vsix.sh)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# --- compare ---------------------------------------------------------------
|
|
for f in $INDENT_FIXTURES $ALIGN_FIXTURES; do
|
|
check "$f (python)" "$OUT/$f.py.out" "$FIX/${f}_expected.kt"
|
|
check "$f (python idempotent)" "$OUT/$f.py.idem" "$FIX/${f}_expected.kt"
|
|
check "$f (cli)" "$OUT/$f.cli.out" "$FIX/${f}_expected.kt"
|
|
if [ "$HAVE_EMACS" = 1 ]; then
|
|
check "$f (emacs)" "$OUT/$f.el.out" "$FIX/${f}_expected.kt"
|
|
check "$f (emacs idempotent)" "$OUT/$f.el.idem" "$FIX/${f}_expected.kt"
|
|
check "$f (emacs == python)" "$OUT/$f.el.out" "$OUT/$f.py.out"
|
|
fi
|
|
if [ "$HAVE_VIM" = 1 ]; then
|
|
check "$f (vim)" "$OUT/$f.vim.out" "$FIX/${f}_expected.kt"
|
|
check "$f (vim idempotent)" "$OUT/$f.vim.idem" "$FIX/${f}_expected.kt"
|
|
fi
|
|
done
|
|
|
|
echo
|
|
echo "${bold}editor_test: $PASS passed, $FAIL failed${reset}"
|
|
[ "$FAIL" -eq 0 ]
|