Files
kokkoja-rel/tests/test_character_codes.py
ackandClaude Fable 5.1 89b4a41b2a Character codes in typed text; the site and relay at app.kokkoja.com
With the Text tool, Klammertext's codes give letters a keyboard lacks:
^o" is ö, ^s^ ß, ^a1 ā, ^130ED^ a code point of 4 to 6 digits,
^9053 53EF^ a sequence, ^^ a caret, replaced as each code is completed.
The public site and relay moved to app.kokkoja.com; the old name
redirects.  The Markdown documentation is withdrawn from the release
while it is converted to Klammertext; the README's links to it are
stale until then.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 20:40:09 +02:00

101 lines
5.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The client's reading of Klammertext's character codes (character_codes.js)
against the tables `kdesc -c` prints and the rules of the live Klammertext
tree, K/mac (character.cpp, katom_process.cpp; K/src is deprecated), as
`ktext -s` showed them on 2026-10-13. Needs node (the type checker's).
"""
import json
import os
import subprocess
import unittest
CLIENT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "client", "js")
HARNESS = """
import { codeAtEnd } from %s;
const texts = JSON.parse(await new Promise((resolve) => {
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => resolve(data));
}));
console.log(JSON.stringify(texts.map((text) => codeAtEnd(text))));
"""
# `kdesc -c`, 2026-10-13: Displayed, Written.
DIACRITICS = [("é", "^e'"), ("à", "^a`"), ("ô", "^oh"), ("ñ", "^n~"), ("ü", '^u"'), ("ç", "^cc"),
("ō", "^o-"), ("ğ", "^gb"), ("å", "^ar"), ("ė", "^ed"), ("š", "^sw"), ("ő", "^oa")]
EXTENDED_LATIN = [("ß", "^s^"), ("ı", "^i^"), ("İ", "^I^"), ("þ", "^t^"), ("Þ", "^T^"), ("ð", "^e^"),
("Ð", "^E^"), ("ø", "^o^"), ("Ø", "^O^"), ("đ", "^d^"), ("Đ", "^D^"), ("æ", "^ae^"),
("Æ", "^AE^"), ("œ", "^oe^"), ("Œ", "^OE^")]
PINYIN = [("ā", "^a1"), ("á", "^a2"), ("ǎ", "^a3"), ("à", "^a4")]
def codes_at_end(texts):
harness = HARNESS % json.dumps(os.path.join(CLIENT, "character_codes.js"))
result = subprocess.run(["node", "--input-type=module", "-e", harness],
input=json.dumps(texts), capture_output=True, text=True, check=True)
return json.loads(result.stdout)
class CharacterCodesTest(unittest.TestCase):
def check(self, cases):
"""[(text, expected)]: expected is the character, or None."""
results = codes_at_end([text for text, _ in cases])
for (text, expected), result in zip(cases, results):
with self.subTest(text=text):
if expected is None:
self.assertIsNone(result)
else:
self.assertEqual(result, {"length": len(text) - len("Vor "), "character": expected})
def test_the_tables(self):
cases = [("Vor " + written, shown) for shown, written in DIACRITICS + EXTENDED_LATIN + PINYIN]
self.check(cases)
# Precomposed, one code point each, as the tables display them.
for shown, _ in DIACRITICS + EXTENDED_LATIN + PINYIN:
self.assertEqual(len(shown), 1)
def test_other_bases_and_capitals(self):
"""The base of a diacritic is a letter of any alphabet, not a
digit, a space or ASCII punctuation."""
self.check([("Vor ^A'", "Á"), ("Vor ^U\"", "Ü"), ("Vor ^Ss", None), ("Vor ^Sw", "Š"),
("Vor ^c'", "ć"), ("Vor ^Ccedilla", None), ("Vor ^e1", "ē"), ("Vor ^u3", "ǔ"),
("Vor ^x1", None), ("Vor ^q~", "q̃"), ("Vor ^1~", None), ("Vor ^.~", None),
("Vor ^ö-", "\u022B"), ("Vor ^α'", "ά")])
def test_code_points_and_the_caret(self):
self.check([("Vor ^00E9^", "é"), ("Vor ^00e9^", "é"), ("Vor ^20AC^", "€"), ("Vor ^^", "^"),
("Vor ^00AE^", "®"), # not ^E^ at its end
("Vor ^130ED^", "𓃭"), ("Vor ^1F600^", "😀"), ("Vor ^10FFFD^", "\U0010FFFD"),
# 1 to 3 digits are not a code point: a control
# character, or a collision with the mnemonics.
("Vor ^a^", None), ("Vor ^7^", None), ("Vor ^ae^", "æ"), ("Vor ^0E9^", None),
# Not a character: beyond the range, a surrogate, a
# noncharacter. Stays as typed (an error in a document).
("Vor ^110000^", None), ("Vor ^D800^", None), ("Vor ^FFFE^", None), ("Vor ^FDD0^", None)])
def test_sequences(self):
"""Code points separated by spaces between one pair of carets."""
self.check([("Vor ^9053 53EF 9053 975E 5A38 9053^", "道可道非娸道"), ("Vor ^9053 53EF^", "道可"),
("Vor ^0041 1F600^", "A😀"),
("Vor ^ 9053^", None), ("Vor ^9053 ^", None), # no space inside the carets
("Vor ^9053 D800^", None), # one bad member spoils it
# A single character stands for itself, as in
# Klammertext, once a code point is among the members
# (Andy, 2026-10-13); without one, x^2 + y^2 stays.
("Vor ^9053 a 53EF^", "道a可"), ("Vor ^a 1F600^", "a😀"), ("Vor ^9053 @^", "道@"),
("Vor ^2 + y^", None), ("Vor ^a b^", None), ("Vor ^ab 9053^", None),
("Vor ^a^", None)])
def test_an_unfinished_or_no_code(self):
"""A keystroke at a time: nothing until the code is complete, and
a caret with something else after it stays."""
self.check([("Vor ^", None), ("Vor ^o", None), ("Vor ^ae", None), ("Vor ^00E", None),
("Vor ^00E9", None), ("Vor ^ a", None), ("Vor ^o\"a", None), ("Vor 2^3", None),
("Vor ^@'", None), ("Vor ", None), ("", None)])
if __name__ == "__main__":
unittest.main()