Spaces:
Running
Running
| /** | |
| * 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; | |
| } | |