Files
kokkoja-rel/client/js/character_codes.js
T
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

148 lines
5.5 KiB
JavaScript
Raw 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.
// @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;
}