// @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; }