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>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-13 20:40:09 +02:00
1 parent 34ca33d365
commit 89b4a41b2a
15 files changed
+364 -1273

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
// @ts-check
/**
* Klammertext's character codes, for typing letters a keyboard lacks:
* `^o"` is ö, `^s^` is ß, `^a1` is ā, `^00E9^` is é, `^130ED^` an
* Egyptian hieroglyph, `^9053 53EF^` two characters from one pair of
* carets, and `^^` is a caret. The text field replaces a code as soon
* as the keystroke that completes it is typed (text_editor.js), like a
* compose key.
*
* The tables and the rules are those of Klammertext's K/mac/character.h,
* character.cpp and katom_process.cpp (the live tree; K/src is
* deprecated), copied 2026-10-13; `kdesc -c` lists the tables. Left
* out, since a text block is not a document: the `^'...'^` literal span
* (a code is replaced only at the caret here, so nothing needs
* protecting), and the one-hex-digit form `^a^`, which makes a control
* character. A single character standing for itself inside a sequence
* (`^9053 a^` is 道a) applies only when at least one member is a code
* point (Andy, 2026-10-13), so that a typed `x^2 + y^2` stays. An
* invalid code point, which is an error in a document, stays as typed.
* The result is normalized to its precomposed form, as Klammertext's
* `compose` does, so that ö is one character in the page file and to
* Backspace.
*/
/** The diacritic each mark letter stands for, as a combining character. */
export const DIACRITICS = {
'`': '̀', // grave accent
"'": '́', // acute accent
'h': '̂', // circumflex (hat)
'~': '̃', // tilde
'-': '̄', // macron
'"': '̈', // dieresis
'b': '̆', // breve
'd': '̇', // dot above
'r': '̊', // ring above
'w': '̌', // wedge (caron)
'c': '̧', // cedilla
'a': '̋', // double acute accent
};
/** Letters outside the Roman alphabet and ligatures, `^code^`. */
export const EXTENDED_LATIN = {
's': 'ß', // Eszett
'i': 'ı', // dotless i
'I': 'İ', // capital dotted i
't': 'þ', // thorn
'T': 'Þ', // thorn capital
'e': 'ð', // eth
'E': 'Ð', // eth capital
'o': 'ø', // o stroke
'O': 'Ø', // o stroke capital
'd': 'đ', // d stroke
'D': 'Đ', // d stroke capital
'ae': 'æ', // ae ligature
'AE': 'Æ', // ae ligature capital
'oe': 'œ', // oe ligature
'OE': 'Œ', // oe ligature capital
};
/** Mandarin pinyin tones on a vowel, `^a1` to `^a4`. */
export const PINYIN_TONES = {
'1': '̄', // high
'2': '́', // rising
'3': '̌', // falling-rising
'4': '̀', // falling
};
/** A code point in hex: 4 to 6 digits (2 and 3 would collide with the mnemonics). */
const HEX = '[0-9A-Fa-f]{4,6}';
/**
* Whether the number is a code point a character can be made of
* (Klammertext's `valid_codepoint`): in range, not a surrogate, not a
* noncharacter.
* @param {number} cp
* @returns {boolean}
*/
export function validCodePoint(cp) {
if (cp < 0 || cp > 0x10FFFF) return false;
if (0xD800 <= cp && cp <= 0xDFFF) return false;
if (0xFDD0 <= cp && cp <= 0xFDEF) return false;
if ((cp & 0xFFFE) === 0xFFFE) return false;
return true;
}
/**
* What a sequence's members stand for: a hex member its code point, any
* other, a single character, itself; or null when no member is a code
* point (the run is not a sequence) or one is not a valid code point.
* @param {string[]} members
* @returns {string | null}
*/
function charactersOf(members) {
const hexRe = new RegExp(`^${HEX}$`, 'u');
if (!members.some((m) => hexRe.test(m))) return null;
let result = '';
for (const member of members) {
if (hexRe.test(member)) {
const cp = parseInt(member, 16);
if (!validCodePoint(cp)) return null;
result += String.fromCodePoint(cp);
} else {
result += member;
}
}
return result;
}
/**
* The code that the text ends with, if it ends with a complete one.
* Checked in Klammertext's order: an extended Latin letter, a code point
* or a sequence, the caret itself, a diacritic, a pinyin tone.
* @param {string} text
* @returns {{length: number, character: string} | null}
* How many characters of the text the code takes, and what it means.
*/
export function codeAtEnd(text) {
const extended = /\^(ae|AE|oe|OE|[siItTeEoOdD])\^$/u.exec(text);
if (extended) {
const name = /** @type {keyof typeof EXTENDED_LATIN} */ (extended[1]);
return { length: name.length + 2, character: EXTENDED_LATIN[name] };
}
// One pair of carets around members separated by spaces, on one line,
// no space inside the carets: code points of 4 to 6 hex digits and,
// once one of those is present, single characters standing for
// themselves (a caret cannot be one: it would close the sequence).
const member = `(?:${HEX}|[^\\s^])`;
const sequence = new RegExp(`\\^(${member}(?: +${member})*)\\^$`, 'u').exec(text);
if (sequence) {
const character = charactersOf(sequence[1].split(/ +/));
if (character !== null) return { length: sequence[0].length, character };
}
if (text.endsWith('^^')) return { length: 2, character: '^' };
// The base is a letter of any alphabet: not a space, a digit or ASCII
// punctuation (Klammertext's rule).
const diacritic = /\^([^\s0-9!-/:-@[-`{-~])([-'`h~"cbrdwa])$/u.exec(text);
if (diacritic) {
const mark = /** @type {keyof typeof DIACRITICS} */ (diacritic[2]);
return { length: diacritic[0].length, character: (diacritic[1] + DIACRITICS[mark]).normalize('NFC') };
}
const tone = /\^([aeiou])([1-4])$/u.exec(text);
if (tone) {
const number = /** @type {keyof typeof PINYIN_TONES} */ (tone[2]);
return { length: 3, character: (tone[1] + PINYIN_TONES[number]).normalize('NFC') };
}
return null;
}