File size: 4,042 Bytes
843a4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
 * 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;
}