diff --git a/src-tauri/src/dictionary.rs b/src-tauri/src/dictionary.rs new file mode 100644 index 0000000..9a33906 --- /dev/null +++ b/src-tauri/src/dictionary.rs @@ -0,0 +1,142 @@ +use std::{ + fs::File, + io::{Read, Write}, + path::PathBuf, +}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use tauri::State; +use tracing::{debug, error, info, trace}; +use regex::Regex; + +use crate::{ + config::Config, + converter::converter::{get_custom_converter, Converter}, + AppState, +}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum ConversionMethod { + Replace, + None, + Converter(char), +} + +impl Default for ConversionMethod { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct DictionaryEntry { + pub input: String, + pub method: ConversionMethod, + pub output: Option, + pub use_regex: bool, + pub priority: i32, +} + +impl Default for DictionaryEntry { + fn default() -> Self { + Self { + input: String::new(), + method: ConversionMethod::None, + output: None, + use_regex: false, + priority: 0, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Dictionary { + pub entries: Vec, +} + +impl Default for Dictionary { + fn default() -> Self { + Self { + entries: Vec::new(), + } + } +} + +impl Dictionary { + pub fn load() -> Result { + debug!("Loading dictionary"); + std::fs::create_dir_all(Config::get_path())?; + + let dict_path = Self::get_path(); + if !dict_path.exists() { + info!("Dictionary file not found, generating default"); + Self::generate_default_dictionary()?; + } + let mut file = File::open(&dict_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + trace!("Raw dictionary contents: {}", contents); + let dictionary: Dictionary = serde_yaml::from_str(&contents)?; + debug!("Dictionary loaded successfully with {} entries", dictionary.entries.len()); + Ok(dictionary) + } + + pub fn save(&self, state: State) -> Result<(), String> { + debug!("Saving dictionary"); + std::fs::create_dir_all(Config::get_path()).unwrap(); + + let dict_path = Self::get_path(); + let mut file = match File::create(&dict_path) { + Ok(file) => file, + Err(e) => { + error!("Failed to create dictionary file: {}", e); + return Err(format!("Failed to create dictionary file: {}", e)); + } + }; + + match serde_yaml::to_string(&self) { + Ok(yaml) => { + trace!("Dictionary to be saved: {}", yaml); + if let Err(e) = file.write_all(yaml.as_bytes()) { + error!("Failed to write dictionary: {}", e); + return Err(format!("Failed to write dictionary: {}", e)); + } + let mut app_dictionary = state.dictionary.lock().unwrap(); + *app_dictionary = self.clone(); + info!("Dictionary saved successfully"); + Ok(()) + } + Err(e) => { + error!("Failed to serialize dictionary: {}", e); + Err(format!("Failed to serialize dictionary: {}", e)) + } + } + } + + pub fn generate_default_dictionary() -> Result<()> { + debug!("Generating default dictionary"); + let dict_path = Self::get_path(); + let mut file = File::create(&dict_path)?; + let default_dict = Dictionary::default(); + let yaml = serde_yaml::to_string(&default_dict).unwrap(); + file.write_all(yaml.as_bytes())?; + file.flush()?; + info!("Default dictionary generated successfully"); + Ok(()) + } + + pub fn get_path() -> PathBuf { + let path = Config::get_path().join("dictionary.yaml"); + trace!("Dictionary path: {:?}", path); + path + } + + pub fn apply_conversion(&self, text: &str) -> Result { + debug!("Applying dictionary conversions to: {}", text); + + + + todo!() + } +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5035da7..ee7799a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -12,6 +12,7 @@ mod tsf; mod tsf_conversion; mod tauri_emit_subscriber; mod tsf_availability; +mod dictionary; use std::sync::Mutex; @@ -27,6 +28,7 @@ use tauri_emit_subscriber::TauriEmitSubscriber; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tsf_availability::check_tsf_availability; use tracing::debug; +use dictionary::Dictionary; #[derive(Serialize, Deserialize, Debug, Clone)] struct Log { @@ -37,9 +39,11 @@ struct Log { struct AppState { config: Mutex, + dictionary: Mutex, } static STATE: Lazy> = Lazy::new(|| Mutex::new(Config::load().unwrap())); +static DICTIONARY: Lazy> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap())); #[tauri::command] fn load_settings(state: State) -> Result { @@ -71,6 +75,24 @@ fn check_tsf_availability_command() -> Result { } } +#[tauri::command] +fn load_dictionary(state: State) -> Result { + match Dictionary::load() { + Ok(dictionary) => { + let mut app_dictionary = state.dictionary.lock().unwrap(); + *app_dictionary = dictionary.clone(); + Ok(dictionary) + } + Err(e) => Err(format!("Failed to load dictionary: {}", e)), + } +} + +#[tauri::command] +fn save_dictionary(dictionary: Dictionary, state: State) -> Result<(), String> { + *DICTIONARY.lock().unwrap() = dictionary.clone(); + dictionary.save(state).map_err(|e| e.to_string()) +} + #[tauri::command] fn open_ms_settings_regionlanguage_jpnime() -> Result<(), String> { let _ = std::process::Command::new("cmd") @@ -89,11 +111,16 @@ fn main() { Config::generate_default_config().expect("Failed to generate default config"); Config::load().expect("Failed to load default config") })), + dictionary: Mutex::new(Dictionary::load().unwrap_or_else(|_| { + Dictionary::generate_default_dictionary().expect("Failed to generate default dictionary"); + Dictionary::load().expect("Failed to load default dictionary") + })), }) - .invoke_handler(tauri::generate_handler![load_settings, save_settings, check_tsf_availability_command, open_ms_settings_regionlanguage_jpnime]) + .invoke_handler(tauri::generate_handler![load_settings, save_settings, check_tsf_availability_command, open_ms_settings_regionlanguage_jpnime, load_dictionary, save_dictionary]) .setup(|app| { let _span = tracing::span!(tracing::Level::INFO, "main"); app.manage(STATE.lock().unwrap().clone()); + app.manage(DICTIONARY.lock().unwrap().clone()); let app_handle = app.app_handle().clone(); let registry = tracing_subscriber::registry().with(TauriEmitSubscriber { diff --git a/src/App.tsx b/src/App.tsx index 3c52ab9..9d7d36c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { List, Settings, Terminal, Bug, Info } from 'lucide-react'; +import { List, Settings, Terminal, Bug, Info, Check, Book } from 'lucide-react'; import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; import "./App.css"; @@ -11,6 +11,7 @@ import TerminalComponent from "./TerminalComponent"; import AboutComponent from "./AboutComponent"; import TsfSettingsModal from "./TsfSettingsModal"; import { Config } from "./SettingsComponent"; +import DictionaryComponent from "./DictionaryComponent"; interface Log { time: string; @@ -24,6 +25,7 @@ const AppContent = () => { const [showTsfModal, setShowTsfModal] = useState(false); const [currentSettings, setCurrentSettings] = useState(null); const [isTsfAvailable, setIsTsfAvailable] = useState(null); + const [showTsfSuccessMessage, setShowTsfSuccessMessage] = useState(false); useEffect(() => { const unlisten = listen('addLog', (event) => { @@ -119,11 +121,17 @@ const AppContent = () => { ); case 'settings': - return ; + return ; case 'terminal': return ; case 'about': return ; + case 'dictionary': + return ; default: return null; } @@ -138,6 +146,7 @@ const AppContent = () => {
} label="ログ" id="home" /> } label="設定" id="settings" /> + } label="辞書" id="dictionary" />
{/* 下側にデバッグタブを配置 */}
@@ -159,8 +168,25 @@ const AppContent = () => { onClose={() => setShowTsfModal(false)} onSaveSettings={saveSettings} currentSettings={currentSettings} + onTsfEnabled={() => { + setShowTsfSuccessMessage(true); + setTimeout(() => setShowTsfSuccessMessage(false), 5000); // 5秒後に非表示 + }} /> )} + + {/* TSF有効化成功メッセージ */} + {showTsfSuccessMessage && ( +
+
+ +
+
+

TSF再変換が有効化されました

+

文字変換機能が拡張されました

+
+
+ )}
); }; diff --git a/src/DictionaryComponent.tsx b/src/DictionaryComponent.tsx new file mode 100644 index 0000000..e935acb --- /dev/null +++ b/src/DictionaryComponent.tsx @@ -0,0 +1,532 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { Book, Save, Plus, Trash, ChevronUp, ChevronDown, AlertCircle, Check, Edit, AlignLeft, Info, X } from 'lucide-react'; +import { + Dictionary, + DictionaryEntry, + ConversionMethod, + availableConverters, + getDefaultDictionaryEntry, + getConverterInfo, + convertToRustEntry, + convertFromRustEntry +} from './types/dictionary'; + +const DictionaryComponent: React.FC = () => { + const [dictionary, setDictionary] = useState({ entries: [] }); + const [selectedEntry, setSelectedEntry] = useState(null); + const [editIndex, setEditIndex] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [showDialog, setShowDialog] = useState(false); + const [isMethodDropdownOpen, setIsMethodDropdownOpen] = useState(false); + const [isConverterDropdownOpen, setIsConverterDropdownOpen] = useState(false); + const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle'); + + const methodDropdownRef = useRef(null); + const converterDropdownRef = useRef(null); + + // 辞書データの読み込み + useEffect(() => { + loadDictionary(); + }, []); + + // ドロップダウン外のクリックを検知して閉じる + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (methodDropdownRef.current && !methodDropdownRef.current.contains(event.target as Node)) { + setIsMethodDropdownOpen(false); + } + if (converterDropdownRef.current && !converterDropdownRef.current.contains(event.target as Node)) { + setIsConverterDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const loadDictionary = async () => { + try { + const loadedDictionary: any = await invoke('load_dictionary'); + + // Rust形式からTypeScript形式に変換 + const entriesWithPriority = loadedDictionary.entries.map((entry: any, index: number) => { + const convertedEntry = convertFromRustEntry(entry); + // priorityが設定されていない場合はデフォルト値を設定 + if (convertedEntry.priority === undefined) { + convertedEntry.priority = index; + } + return convertedEntry; + }); + + setDictionary({ entries: entriesWithPriority }); + } catch (error) { + console.error('Failed to load dictionary:', error); + } + }; + + const saveDictionary = async (dict: Dictionary) => { + setSaveStatus('saving'); + try { + // TypeScript形式からRust形式に変換 + const rustDict = { + entries: dict.entries.map(entry => convertToRustEntry(entry)) + }; + + await invoke('save_dictionary', { dictionary: rustDict }); + setSaveStatus('success'); + setTimeout(() => setSaveStatus('idle'), 2000); + } catch (error) { + console.error('Failed to save dictionary:', error); + setSaveStatus('error'); + setTimeout(() => setSaveStatus('idle'), 3000); + } + }; + + const handleAddEntry = () => { + setSelectedEntry(getDefaultDictionaryEntry()); + setEditIndex(null); + setIsEditing(true); + setShowDialog(true); + }; + + const handleEditEntry = (entry: DictionaryEntry, index: number) => { + setSelectedEntry({...entry}); + setEditIndex(index); + setIsEditing(true); + setShowDialog(true); + }; + + const handleDeleteEntry = (index: number) => { + const newEntries = [...dictionary.entries]; + newEntries.splice(index, 1); + const newDict = { ...dictionary, entries: newEntries }; + setDictionary(newDict); + saveDictionary(newDict); + }; + + const handleSaveEntry = () => { + if (!selectedEntry) return; + + const newEntries = [...dictionary.entries]; + + // 新規追加の場合 + if (editIndex === null) { + newEntries.push(selectedEntry); + } else { + // 編集の場合 + newEntries[editIndex] = selectedEntry; + } + + const newDict = { ...dictionary, entries: newEntries }; + setDictionary(newDict); + saveDictionary(newDict); + setShowDialog(false); + setIsEditing(false); + setSelectedEntry(null); + }; + + const handleChangePriority = (index: number, direction: 'up' | 'down') => { + if (dictionary.entries.length <= 1) return; + + const newEntries = [...dictionary.entries]; + const entry = newEntries[index]; + + if (direction === 'up' && index > 0) { + const prevEntry = newEntries[index - 1]; + const tempPriority = prevEntry.priority; + prevEntry.priority = entry.priority; + entry.priority = tempPriority; + + // 実際の配列の順序も変更 + newEntries[index] = prevEntry; + newEntries[index - 1] = entry; + } else if (direction === 'down' && index < newEntries.length - 1) { + const nextEntry = newEntries[index + 1]; + const tempPriority = nextEntry.priority; + nextEntry.priority = entry.priority; + entry.priority = tempPriority; + + // 実際の配列の順序も変更 + newEntries[index] = nextEntry; + newEntries[index + 1] = entry; + } + + const newDict = { ...dictionary, entries: newEntries }; + setDictionary(newDict); + saveDictionary(newDict); + }; + + const handleChangeEntryField = (field: keyof DictionaryEntry, value: any) => { + if (!selectedEntry) return; + + const updatedEntry = { ...selectedEntry, [field]: value }; + + // 変換方法がReplace以外の場合はoutputをnullに + if (field === 'method' && value !== ConversionMethod.Replace) { + updatedEntry.output = undefined; + } + + // 変換方法がConverterの場合はconverter_charをデフォルト値に + if (field === 'method' && value === ConversionMethod.Converter && !updatedEntry.converter_char) { + updatedEntry.converter_char = 'r'; + } + + setSelectedEntry(updatedEntry); + }; + + const handleSelectMethod = (method: ConversionMethod) => { + handleChangeEntryField('method', method); + setIsMethodDropdownOpen(false); + }; + + const handleSelectConverter = (converterId: string) => { + handleChangeEntryField('converter_char', converterId); + setIsConverterDropdownOpen(false); + }; + + const getMethodLabel = (method: ConversionMethod, converterChar?: string) => { + switch(method) { + case ConversionMethod.Replace: + return '置き換え'; + case ConversionMethod.None: + return '無変換'; + case ConversionMethod.Converter: + if (converterChar) { + const converter = getConverterInfo(converterChar); + return converter ? `変換: ${converter.name}` : '変換'; + } + return '変換'; + default: + return method; + } + }; + + const SaveStatusIndicator = () => { + switch (saveStatus) { + case 'saving': + return 保存中; + case 'success': + return 保存完了; + case 'error': + return 保存失敗; + default: + return null; + } + }; + + // 辞書エントリダイアログ + const EntryDialog = () => { + if (!showDialog || !selectedEntry) return null; + + return ( +
+
+
+

+ {editIndex !== null ? '辞書エントリの編集' : '新しい辞書エントリ'} +

+ +
+ +
+
+ + handleChangeEntryField('input', e.target.value)} + className="w-full p-1.5 text-sm border rounded bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 focus:border-indigo-400 outline-none" + placeholder="例: こんにちは" + /> +
+ handleChangeEntryField('use_regex', e.target.checked)} + className="h-3.5 w-3.5 text-indigo-500 border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700" + /> + +
+
+ +
+ +
setIsMethodDropdownOpen(!isMethodDropdownOpen)} + > + {getMethodLabel(selectedEntry.method as ConversionMethod, selectedEntry.converter_char)} + +
+ {isMethodDropdownOpen && ( +
+
handleSelectMethod(ConversionMethod.Replace)} + > +
+ {selectedEntry.method === ConversionMethod.Replace && } + + 置き換え + +
+
+
handleSelectMethod(ConversionMethod.None)} + > +
+ {selectedEntry.method === ConversionMethod.None && } + + 無変換 + +
+
+
handleSelectMethod(ConversionMethod.Converter)} + > +
+ {selectedEntry.method === ConversionMethod.Converter && } + + 変換 + +
+
+
+ )} +
+ + {selectedEntry.method === ConversionMethod.Converter && ( +
+ +
setIsConverterDropdownOpen(!isConverterDropdownOpen)} + > + + {selectedEntry.converter_char + ? getConverterInfo(selectedEntry.converter_char)?.name || 'ローマ字→漢字' + : 'ローマ字→漢字'} + + +
+ {isConverterDropdownOpen && ( +
+ {availableConverters.map(converter => ( +
handleSelectConverter(converter.id)} + > +
+ {selectedEntry.converter_char === converter.id && } + + {converter.name} - {converter.description} + +
+
+ ))} +
+ )} +
+ )} + + {selectedEntry.method === ConversionMethod.Replace && ( +
+ + handleChangeEntryField('output', e.target.value)} + className="w-full p-1.5 text-sm border rounded bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 focus:border-indigo-400 outline-none" + placeholder="例: Hello" + /> +
+ )} + +
+ + handleChangeEntryField('priority', parseInt(e.target.value) || 0)} + className="w-full p-1.5 text-sm border rounded bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 focus:border-indigo-400 outline-none" + min="0" + /> +

+ 数値が大きいほど優先度が高くなります。 +

+
+
+ +
+ + +
+
+
+ ); + }; + + return ( +
+
+

+ + 辞書 +

+
+ + +
+
+ +
+ {dictionary.entries.length > 0 ? ( +
+ + + + + + + + + + + + {dictionary.entries.map((entry, index) => ( + + + + + + + + ))} + +
優先度変換対象変換方法変換後操作
+
+ {entry.priority} +
+ + +
+
+
+
+ {entry.use_regex && ( + 正規表現 + )} + {entry.input} +
+
+ {getMethodLabel(entry.method as ConversionMethod, entry.converter_char)} + + {entry.method === ConversionMethod.Replace ? entry.output : '-'} + +
+ + +
+
+
+ ) : ( +
+ +

辞書エントリがありません

+

「新規追加」ボタンから辞書エントリを追加してください

+ +
+ )} +
+ + {/* エントリ編集ダイアログ */} + +
+ ); +}; + +export default DictionaryComponent; \ No newline at end of file diff --git a/src/SettingsComponent.tsx b/src/SettingsComponent.tsx index feba8da..c6504a8 100644 --- a/src/SettingsComponent.tsx +++ b/src/SettingsComponent.tsx @@ -90,9 +90,15 @@ const CheckboxField: React.FC = ({ id, name, label, checked, interface SettingsComponentProps { setShowTsfModal: (show: boolean) => void; + currentSettings: Config | null; + onSaveSettings: (config: Config) => Promise; } -const SettingsComponent: React.FC = ({ setShowTsfModal }) => { +const SettingsComponent: React.FC = ({ + setShowTsfModal, + currentSettings, + onSaveSettings +}) => { const [settings, setSettings] = useState({ prefix: ';', split: '/', @@ -115,6 +121,13 @@ const SettingsComponent: React.FC = ({ setShowTsfModal } }; }, []); + // 親コンポーネントから新しい設定が渡されたら更新する + useEffect(() => { + if (currentSettings) { + setSettings(currentSettings); + } + }, [currentSettings]); + const loadSettings = async () => { try { const loadedSettings: Config = await invoke('load_settings'); @@ -127,7 +140,12 @@ const SettingsComponent: React.FC = ({ setShowTsfModal } const saveSettings = async (newSettings: Config) => { setSaveStatus('saving'); try { - await invoke('save_settings', { config: newSettings }); + if (onSaveSettings) { + await onSaveSettings(newSettings); + } else { + // 後方互換性のために残す + await invoke('save_settings', { config: newSettings }); + } setSaveStatus('success'); setTimeout(() => setSaveStatus('idle'), 2000); } catch (error) { diff --git a/src/TsfSettingsModal.tsx b/src/TsfSettingsModal.tsx index d2569ee..6f4cbc8 100644 --- a/src/TsfSettingsModal.tsx +++ b/src/TsfSettingsModal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { open } from '@tauri-apps/plugin-shell'; import { AlertCircle, Settings, Check, X, ExternalLink } from 'lucide-react'; import { Config } from './SettingsComponent'; @@ -9,14 +9,65 @@ interface TsfSettingsModalProps { onClose: () => void; onSaveSettings: (config: Config) => Promise; currentSettings: Config; + onTsfEnabled?: () => void; } const TsfSettingsModal: React.FC = ({ isOpen, onClose, onSaveSettings, - currentSettings + currentSettings, + onTsfEnabled }) => { + const [checkingStatus, setCheckingStatus] = useState<'idle' | 'checking'>('idle'); + + // TSFが利用可能かどうかを定期的にチェックする + useEffect(() => { + if (!isOpen) return; // モーダルが閉じている場合は処理しない + + let intervalId: number | null = null; + + const checkTsfAvailability = async () => { + try { + const available: boolean = await invoke('check_tsf_availability_command'); + + if (available) { + // TSFが利用可能になったら + const newSettings = { ...currentSettings, use_tsf_reconvert: true }; + await onSaveSettings(newSettings); + + // 成功コールバックを呼び出し + if (onTsfEnabled) { + onTsfEnabled(); + } + + // モーダルを閉じる + onClose(); + + // インターバルをクリア + if (intervalId !== null) { + clearInterval(intervalId); + } + } + } catch (error) { + console.error('TSF利用可能性チェックに失敗しました:', error); + } + }; + + // 初回チェック + checkTsfAvailability(); + + // 1秒ごとにチェック(標準的なsetIntervalを使用) + intervalId = window.setInterval(checkTsfAvailability, 1000); + + // クリーンアップ関数 + return () => { + if (intervalId !== null) { + clearInterval(intervalId); + } + }; + }, [isOpen, currentSettings, onSaveSettings, onClose, onTsfEnabled]); + if (!isOpen) return null; const openWindowsSettings = async () => { @@ -89,6 +140,10 @@ const TsfSettingsModal: React.FC = ({ (機能制限あり) + +
+ 設定が完了すると、このウィンドウは自動的に閉じます +
); diff --git a/src/types/dictionary.ts b/src/types/dictionary.ts new file mode 100644 index 0000000..d70e272 --- /dev/null +++ b/src/types/dictionary.ts @@ -0,0 +1,97 @@ +export enum ConversionMethod { + Replace = 'Replace', + None = 'None', + Converter = 'Converter' +} + +export interface DictionaryEntry { + input: string; + method: ConversionMethod; + output?: string; + use_regex: boolean; + priority: number; + converter_char?: string; +} + +export interface RustDictionaryEntry { + input: string; + method: string | { Converter: string }; + output?: string; + use_regex: boolean; + priority: number; +} + +export interface RustDictionary { + entries: RustDictionaryEntry[]; +} + +export interface Dictionary { + entries: DictionaryEntry[]; + } + +export interface ConverterInfo { + id: string; + name: string; + description: string; +} + +export const availableConverters: ConverterInfo[] = [ + { id: 'r', name: 'ローマ字→漢字', description: 'ローマ字を漢字に変換します' }, + { id: 'h', name: 'ひらがな変換', description: '入力をひらがなに変換します' }, + { id: 'k', name: 'カタカナ変換', description: '入力をカタカナに変換します' }, + { id: 'c', name: '計算', description: '数式を計算します' }, + { id: 'n', name: '無変換', description: '入力をそのまま出力します' }, +]; + +export function getConverterInfo(id: string): ConverterInfo | undefined { + return availableConverters.find(converter => converter.id === id); +} + +export function getDefaultDictionaryEntry(): DictionaryEntry { + return { + input: '', + method: ConversionMethod.Replace, + output: '', + use_regex: false, + priority: 0 + }; +} + +export function convertToRustEntry(entry: DictionaryEntry): RustDictionaryEntry { + let method: string | { Converter: string }; + + if (entry.method === ConversionMethod.Converter && entry.converter_char) { + method = { Converter: entry.converter_char }; + } else { + method = entry.method; + } + + return { + input: entry.input, + method: method, + output: entry.method === ConversionMethod.Replace ? entry.output : undefined, + use_regex: entry.use_regex, + priority: entry.priority + }; +} + +export function convertFromRustEntry(entry: any): DictionaryEntry { + let method: ConversionMethod; + let converter_char: string | undefined; + + if (typeof entry.method === 'object' && entry.method.Converter) { + method = ConversionMethod.Converter; + converter_char = entry.method.Converter; + } else { + method = entry.method as ConversionMethod; + } + + return { + input: entry.input, + method: method, + output: entry.output, + use_regex: entry.use_regex, + priority: entry.priority, + converter_char: converter_char + }; +} \ No newline at end of file