147 lines
5.5 KiB
JavaScript
147 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;
|
|||
|
|
}
|