morse-code / lib /morse.js
RemiFabre
feat: initial Reachy Mini Morse Code app
843a4b2
Raw
History Blame Contribute Delete
4.04 kB
/**
* Morse code tables + text <-> morse-string conversion.
*
* A "morse string" here uses the conventional ASCII rendering:
* - '.' dot
* - '-' dash
* - ' ' (single space) gap between letters
* - '/' gap between words (we accept ' / ' or '/' on input)
*
* Example: "SOS" <-> "... --- ..."
*
* This module is pure (no DOM, no audio, no SDK) so it is unit-tested
* directly in Node. The physical *timing* of dots/dashes lives in
* wire.js + timing.js; this layer is purely symbolic.
*/
// International Morse (ITU): letters, digits, and the common punctuation
// that survives an impulse channel. Prosigns are intentionally omitted.
export const CHAR_TO_MORSE = Object.freeze({
A: ".-", B: "-...", C: "-.-.", D: "-..", E: ".", F: "..-.",
G: "--.", H: "....", I: "..", J: ".---", K: "-.-", L: ".-..",
M: "--", N: "-.", O: "---", P: ".--.", Q: "--.-", R: ".-.",
S: "...", T: "-", U: "..-", V: "...-", W: ".--", X: "-..-",
Y: "-.--", Z: "--..",
0: "-----", 1: ".----", 2: "..---", 3: "...--", 4: "....-",
5: ".....", 6: "-....", 7: "--...", 8: "---..", 9: "----.",
".": ".-.-.-", ",": "--..--", "?": "..--..", "'": ".----.",
"!": "-.-.--", "/": "-..-.", "(": "-.--.", ")": "-.--.-",
"&": ".-...", ":": "---...", ";": "-.-.-.", "=": "-...-",
"+": ".-.-.", "-": "-....-", "_": "..--.-", '"': ".-..-.",
"$": "...-..-", "@": ".--.-.",
});
// Reverse lookup: morse pattern -> character.
export const MORSE_TO_CHAR = Object.freeze(
Object.fromEntries(Object.entries(CHAR_TO_MORSE).map(([c, m]) => [m, c])),
);
/** True if `ch` (single char) can be transmitted. Case-insensitive. */
export function isEncodable(ch) {
return Object.prototype.hasOwnProperty.call(CHAR_TO_MORSE, ch.toUpperCase());
}
/**
* Encode arbitrary text to a morse string.
*
* - Case-insensitive.
* - Runs of whitespace collapse to a single word break ('/').
* - Unknown characters are dropped, but reported in `.skipped` so the UI
* can warn ("é, 😀 can't be sent in Morse").
*
* Returns { morse, skipped }.
*/
export function textToMorse(text) {
const words = String(text).trim().toUpperCase().split(/\s+/).filter(Boolean);
const skipped = [];
const encodedWords = words.map((word) => {
const letters = [];
for (const ch of word) {
const code = CHAR_TO_MORSE[ch];
if (code) letters.push(code);
else skipped.push(ch);
}
return letters.join(" ");
}).filter(Boolean);
return { morse: encodedWords.join(" / "), skipped };
}
/**
* Decode a morse string back to text. Tolerant of input shape:
* "... --- ...", "...|---|...", "-.-. -.-- / -.-." etc.
* Letter separators: one or more spaces. Word separators: '/' (optionally
* space-padded). Unknown patterns become '?'.
*/
export function morseToText(morse) {
return String(morse)
.trim()
.split(/\s*\/\s*|\s{2,}\/?\s*/) // word breaks
.map((word) =>
word
.trim()
.split(/\s+/)
.filter(Boolean)
.map((pat) => MORSE_TO_CHAR[pat] ?? (pat ? "?" : ""))
.join(""),
)
.filter((w) => w.length > 0)
.join(" ");
}
/**
* Flatten a morse string into the element/gap token stream the wire layer
* consumes. Tokens:
* 'dot' | 'dah' | 'elemGap' | 'letterGap' | 'wordGap'
*
* No leading/trailing gaps are emitted.
*/
export function morseToTokens(morse) {
const tokens = [];
const words = String(morse).trim().split(/\s*\/\s*/).filter(Boolean);
words.forEach((word, wi) => {
if (wi > 0) tokens.push("wordGap");
const letters = word.trim().split(/\s+/).filter(Boolean);
letters.forEach((letter, li) => {
if (li > 0) tokens.push("letterGap");
[...letter].forEach((sym, si) => {
if (si > 0) tokens.push("elemGap");
tokens.push(sym === "." ? "dot" : "dah");
});
});
});
return tokens;
}