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