import { CHAR_TO_MORSE, MORSE_CHARACTERS, MORSE_TO_CHAR, type MorseCharacterSet } from "./characters.js"; export interface EncodeOptions { letterSeparator?: string; wordSeparator?: string; unknownFallback?: string; } export type Separator = string | RegExp | readonly string[]; export interface DecodeOptions { letterSeparator?: Separator; wordSeparator?: Separator; unknownFallback?: string; } const DEFAULT_ENCODE_LETTER_SEPARATOR = " "; const DEFAULT_ENCODE_WORD_SEPARATOR = " / "; const DEFAULT_DECODE_LETTER_SEPARATOR = /(?:\s+|\|)/u; const DEFAULT_DECODE_WORD_SEPARATOR = /(?:\/|\s{3,})/u; const VALID_MORSE_PATTERN = /^[.\-/\s|]+$/u; function escapeRegex(source: string): string { return source.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); } function toSeparatorRegex(separator: Separator | undefined, fallback: RegExp): RegExp { if (!separator) { return fallback; } if (separator instanceof RegExp) { return separator; } if (Array.isArray(separator)) { const pattern = separator.map((value) => escapeRegex(value)).join("|"); return new RegExp(`(?:${pattern})`, "u"); } if (typeof separator === "string") { return new RegExp(`(?:${escapeRegex(separator)})`, "u"); } throw new TypeError("Invalid separator configuration"); } function normalizeTextInput(text: string): string[] { return text .replace(/\r?\n/gu, " ") .split(/\s+/u) .map((word) => word.trim()) .filter((word) => word.length > 0); } function normalizeMorseInput(morse: string, options: DecodeOptions): string[][] { const wordSeparatorRegex = toSeparatorRegex(options.wordSeparator, DEFAULT_DECODE_WORD_SEPARATOR); const letterSeparatorRegex = toSeparatorRegex(options.letterSeparator, DEFAULT_DECODE_LETTER_SEPARATOR); return morse .trim() .split(wordSeparatorRegex) .map((word) => word .trim() .split(letterSeparatorRegex) .map((token) => token.trim()) .filter((token) => token.length > 0) ) .filter((word) => word.length > 0); } export function characters(): MorseCharacterSet { return { letters: { ...MORSE_CHARACTERS.letters }, digits: { ...MORSE_CHARACTERS.digits }, punctuation: { ...MORSE_CHARACTERS.punctuation } }; } function translateTextWord(word: string, options: EncodeOptions): string { const letterSeparator = options.letterSeparator ?? DEFAULT_ENCODE_LETTER_SEPARATOR; const fallback = options.unknownFallback; const codes = Array.from(word.toUpperCase()).map((char) => { const code = CHAR_TO_MORSE[char]; if (code) { return code; } if (fallback !== undefined) { return fallback; } throw new Error(`Unsupported character: ${char}`); }); return codes.join(letterSeparator); } export function encode(text: string, options: EncodeOptions = {}): string { if (text.trim().length === 0) { return ""; } const words = normalizeTextInput(text); const wordSeparator = options.wordSeparator ?? DEFAULT_ENCODE_WORD_SEPARATOR; return words.map((word) => translateTextWord(word, options)).join(wordSeparator); } export function decode(morse: string, options: DecodeOptions = {}): string { if (morse.trim().length === 0) { return ""; } const tokens = tokenizeMorse(morse, options); const fallback = options.unknownFallback; const decodedWords = tokens.map((letters) => letters .map((code) => { const normalizedCode = code.replace(/\s+/gu, ""); const matched = MORSE_TO_CHAR[normalizedCode]; if (matched) { return matched; } if (fallback !== undefined) { return fallback; } throw new Error(`Unsupported morse token: ${code}`); }) .join("") ); return decodedWords.join(" "); } export function isMorseCandidate(input: string): boolean { return VALID_MORSE_PATTERN.test(input.trim()); } export function textToMorseTokens(text: string, options: EncodeOptions = {}): string[][] { const words = normalizeTextInput(text); return words .map((word) => { const codes = Array.from(word.toUpperCase()).map((char) => { const code = CHAR_TO_MORSE[char]; if (code) { return code; } throw new Error(`Unsupported character: ${char}`); }); return codes.filter((code) => code.length > 0); }) .filter((codes) => codes.length > 0); } export function tokenizeMorse(morse: string, options: DecodeOptions = {}): string[][] { return normalizeMorseInput(morse, options); }