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:
1 parent
34ca33d365
commit
89b4a41b2a
15 files changed
+364
-1273
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user