1use std::collections::HashMap;
9
10use quick_xml::events::{BytesStart, Event};
11use quick_xml::Reader;
12use thiserror::Error;
13
14use crate::expr::{Expr, ExprError};
15use crate::unit::{self, Category, Jamo, Unit};
16
17#[derive(Debug, Error)]
18pub enum ConfigError {
19 #[error("XML 파싱 오류: {0}")]
20 Xml(#[from] quick_xml::Error),
21 #[error("XML 속성 오류: {0}")]
22 Attr(#[from] quick_xml::events::attributes::AttrError),
23 #[error("키 {at:?} 의 값-식 파싱 실패: {source}")]
24 KeyExpr { at: String, source: ExprError },
25 #[error("정수 파싱 실패: {0:?}")]
26 BadInt(String),
27 #[error("알 수 없는 낱자 갈래 {0:?} (CHO/JUNG/JONG 이어야 함)")]
28 BadCategory(String),
29 #[error("UnitMix/VirtualUnit 의 낱자 {0:?} 해석 실패")]
30 BadUnit(String),
31 #[error("InputEntry 인덱스 {0} 없음")]
32 NoEntry(usize),
33 #[error("오토마타 상태 {state} 의 식 파싱 실패: {source}")]
34 AutomataExpr { state: i64, source: ExprError },
35}
36
37#[derive(Debug, Clone)]
41pub struct Config {
42 pub version: String,
43 pub editor: EditorLayer,
44 pub default_entry: usize,
45 pub current_entry: usize,
46 pub entries: Vec<InputEntry>,
47}
48
49#[derive(Debug, Clone, Default)]
50pub struct EditorLayer {
51 pub flags: Vec<String>,
52 pub shortcuts: Vec<Shortcut>,
53 pub final_conv: HashMap<u32, u32>,
55}
56
57#[derive(Debug, Clone)]
58pub struct Shortcut {
59 pub key: String,
60 pub modifier: Vec<String>,
61 pub usage: String,
62 pub value: String,
63}
64
65#[derive(Debug, Clone, Default)]
66pub struct InputEntry {
67 pub scheme_object: String,
68 pub generator_object: String,
69 pub key_table: Option<KeyTable>,
70 pub unit_mix: Vec<UnitMix>,
71 pub virtual_units: Vec<VirtualUnit>,
72 pub automata_default: String,
73 pub automata: Vec<AutomataRow>,
74 pub bksp: Vec<Bksp>,
75}
76
77#[derive(Debug, Clone)]
78pub struct KeyTable {
79 pub name: String,
80 pub from: u32,
81 pub to: u32,
82 pub keys: HashMap<u32, KeyDef>,
84}
85
86#[derive(Debug, Clone)]
87pub struct KeyDef {
88 pub raw: String,
89 pub expr: Expr,
90}
91
92#[derive(Debug, Clone)]
93pub struct UnitMix {
94 pub unit: Category,
95 pub a: String,
96 pub b: String,
97 pub to: String,
98}
99
100#[derive(Debug, Clone)]
101pub struct VirtualUnit {
102 pub unit: Category,
103 pub from: u32,
104 pub to: String,
105}
106
107#[derive(Debug, Clone)]
108pub struct AutomataRow {
109 pub state: i64,
110 pub value: String,
111 pub default: String,
112 pub remark: String,
113}
114
115#[derive(Debug, Clone)]
116pub struct Bksp {
117 pub key: u32,
118 pub value1: String,
119 pub value2: String,
120 pub condition1: String,
121 pub condition2: String,
122}
123
124#[derive(Debug, Clone)]
130pub struct AutomataState {
131 pub value: Expr,
132 pub default: Expr,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum BkspUnit {
139 #[default]
141 LastKey,
142 LowestLastKey,
144 LowestWhole,
146 Syllable,
148}
149
150#[derive(Debug, Clone, Default)]
154pub struct BkspBehavior {
155 pub composing: BkspUnit,
157 pub idle: BkspUnit,
159 pub attach: bool,
161}
162
163fn parse_bksp_value(s: &str) -> (BkspUnit, bool) {
166 let mut unit = BkspUnit::LastKey;
167 let mut attach = false;
168 for tok in s.split('|') {
169 match tok.trim() {
170 "BkspAttach" => attach = true,
171 "ByUnitStep" | "0" => unit = BkspUnit::LastKey,
172 "1" => unit = BkspUnit::LowestLastKey,
173 "2" => unit = BkspUnit::LowestWhole,
174 "BySyllable" | "3" => unit = BkspUnit::Syllable,
175 "ReverseJLTRN" => unit = BkspUnit::LowestLastKey,
178 _ => {}
179 }
180 }
181 (unit, attach)
182}
183
184#[derive(Debug, Clone)]
186pub struct Layout {
187 pub name: String,
188 pub keys: HashMap<u32, Expr>,
190 pub combine: HashMap<(Category, u32, u32), u32>,
192 pub virtual_units: HashMap<u32, Jamo>,
194 pub final_conv: HashMap<u32, u32>,
196 pub shortcuts: Vec<Shortcut>,
198 pub automata: HashMap<i64, AutomataState>,
200 pub automata_start: i64,
202 pub bksp: BkspBehavior,
204}
205
206impl Layout {
207 pub fn combine(&self, cat: Category, a_cp: u32, b_cp: u32) -> Option<u32> {
209 self.combine.get(&(cat, a_cp, b_cp)).copied()
210 }
211
212 pub fn standalone(&self, j: Jamo) -> Option<char> {
214 let compat = self
215 .final_conv
216 .get(&j.cp)
217 .copied()
218 .or_else(|| j.default_compat());
219 compat.and_then(char::from_u32)
220 }
221}
222
223impl Config {
224 pub fn parse(xml: &str) -> Result<Config, ConfigError> {
226 parse_config(xml)
227 }
228
229 pub fn compile(&self, entry_idx: usize) -> Result<Layout, ConfigError> {
231 let entry = self
232 .entries
233 .get(entry_idx)
234 .ok_or(ConfigError::NoEntry(entry_idx))?;
235
236 let keys = entry
237 .key_table
238 .as_ref()
239 .map(|kt| {
240 kt.keys
241 .iter()
242 .map(|(&at, kd)| (at, kd.expr.clone()))
243 .collect()
244 })
245 .unwrap_or_default();
246
247 let mut combine = HashMap::new();
248 for m in &entry.unit_mix {
249 let a = resolve_jamo(&m.a, m.unit)?;
250 let to = resolve_jamo(&m.to, m.unit)?;
251 let b_cp = match unit::resolve_operand(&m.b, Some(m.unit))
252 .ok_or_else(|| ConfigError::BadUnit(m.b.clone()))?
253 {
254 Unit::Toggle => unit::TOGGLE,
255 Unit::Jamo(j) => j.cp,
256 Unit::Virtual(_) => return Err(ConfigError::BadUnit(m.b.clone())),
257 };
258 combine.insert((m.unit, a.cp, b_cp), to.cp);
259 }
260
261 let mut virtual_units = HashMap::new();
262 for v in &entry.virtual_units {
263 let j = resolve_jamo(&v.to, v.unit)?;
264 virtual_units.insert(v.from, j);
265 }
266
267 let mut automata = HashMap::new();
269 for row in &entry.automata {
270 let value = Expr::parse(&row.value).map_err(|source| ConfigError::AutomataExpr {
271 state: row.state,
272 source,
273 })?;
274 let default =
275 Expr::parse(&row.default).map_err(|source| ConfigError::AutomataExpr {
276 state: row.state,
277 source,
278 })?;
279 automata.insert(row.state, AutomataState { value, default });
280 }
281 let automata_start = entry.automata_default.trim().parse().unwrap_or(0);
282
283 let bksp = entry
286 .bksp
287 .iter()
288 .find(|b| b.key == 1)
289 .map(|b| {
290 let (mut composing, a1) = parse_bksp_value(&b.value1);
291 let (idle, a2) = parse_bksp_value(&b.value2);
292 if b.condition1.contains("ReverseJLTRN") {
295 composing = BkspUnit::LowestLastKey;
296 }
297 BkspBehavior {
298 composing,
299 idle,
300 attach: a1 || a2,
301 }
302 })
303 .unwrap_or_default();
304
305 Ok(Layout {
306 name: entry
307 .key_table
308 .as_ref()
309 .map(|k| k.name.clone())
310 .unwrap_or_default(),
311 keys,
312 combine,
313 virtual_units,
314 final_conv: self.editor.final_conv.clone(),
315 shortcuts: self.editor.shortcuts.clone(),
316 automata,
317 automata_start,
318 bksp,
319 })
320 }
321
322 pub fn first_hangul_entry(&self) -> Option<usize> {
324 self.entries
325 .iter()
326 .position(|e| e.generator_object.starts_with("CNgsIme") && e.key_table.is_some())
327 }
328}
329
330fn resolve_jamo(s: &str, cat: Category) -> Result<Jamo, ConfigError> {
331 match unit::resolve_operand(s, Some(cat)) {
332 Some(Unit::Jamo(j)) => Ok(j),
333 _ => Err(ConfigError::BadUnit(s.to_string())),
334 }
335}
336
337fn category_of(s: &str) -> Result<Category, ConfigError> {
338 match s {
339 "CHO" => Ok(Category::Cho),
340 "JUNG" => Ok(Category::Jung),
341 "JONG" => Ok(Category::Jong),
342 other => Err(ConfigError::BadCategory(other.to_string())),
343 }
344}
345
346fn parse_int(s: &str) -> Result<u32, ConfigError> {
347 unit::parse_int(s).ok_or_else(|| ConfigError::BadInt(s.to_string()))
348}
349
350fn attrs(e: &BytesStart) -> Result<HashMap<String, String>, ConfigError> {
352 let mut m = HashMap::new();
353 for a in e.attributes() {
354 let a = a?;
355 let key = String::from_utf8_lossy(a.key.as_ref()).into_owned();
356 let val = a.unescape_value()?.into_owned();
357 m.insert(key, val);
358 }
359 Ok(m)
360}
361
362fn get<'a>(m: &'a HashMap<String, String>, k: &str) -> &'a str {
363 m.get(k).map(String::as_str).unwrap_or("")
364}
365
366fn parse_config(xml: &str) -> Result<Config, ConfigError> {
367 let mut reader = Reader::from_str(xml);
368 reader.config_mut().trim_text(true);
369
370 let mut cfg = Config {
371 version: String::new(),
372 editor: EditorLayer::default(),
373 default_entry: 0,
374 current_entry: 0,
375 entries: Vec::new(),
376 };
377
378 #[derive(PartialEq)]
380 enum Section {
381 None,
382 Scheme,
383 Generator,
384 }
385 let mut section = Section::None;
386 let mut in_input_layer = false;
387
388 loop {
389 match reader.read_event()? {
390 Event::Start(e) | Event::Empty(e) => {
391 let name = e.name();
392 let tag = name.as_ref();
393 let a = attrs(&e)?;
394 match tag {
395 b"EditContextSetting" => cfg.version = get(&a, "version").to_string(),
396 b"EditorLayer" => {
397 cfg.editor.flags = split_flags(get(&a, "flag"));
398 }
399 b"Shortcut" => cfg.editor.shortcuts.push(Shortcut {
400 key: get(&a, "key").to_string(),
401 modifier: split_flags(get(&a, "modifier")),
402 usage: get(&a, "usage").to_string(),
403 value: get(&a, "value").to_string(),
404 }),
405 b"FinalConv" => {
406 let from = parse_int(get(&a, "from"))?;
407 let to = parse_int(get(&a, "to"))?;
408 cfg.editor.final_conv.insert(from, to);
409 }
410 b"InputLayer" => {
411 in_input_layer = true;
412 cfg.default_entry = parse_int(get(&a, "default")).unwrap_or(0) as usize;
413 cfg.current_entry = parse_int(get(&a, "current")).unwrap_or(0) as usize;
414 }
415 b"InputEntry" => {
416 cfg.entries.push(InputEntry::default());
417 }
418 b"InputSchemeSetting" => {
419 section = Section::Scheme;
420 if let Some(en) = cfg.entries.last_mut() {
421 en.scheme_object = get(&a, "object").to_string();
422 }
423 }
424 b"GeneratorSetting" => {
425 section = Section::Generator;
426 if let Some(en) = cfg.entries.last_mut() {
427 en.generator_object = get(&a, "object").to_string();
428 }
429 }
430 b"KeyTable" => {
431 if let Some(en) = cfg.entries.last_mut() {
432 en.key_table = Some(KeyTable {
433 name: get(&a, "name").to_string(),
434 from: parse_int(get(&a, "from")).unwrap_or(33),
435 to: parse_int(get(&a, "to")).unwrap_or(126),
436 keys: HashMap::new(),
437 });
438 }
439 }
440 b"Key" if section == Section::Scheme => {
441 let at_s = get(&a, "at");
442 let val = get(&a, "value");
443 let at = parse_int(at_s)?;
444 let expr = Expr::parse(val).map_err(|source| ConfigError::KeyExpr {
445 at: at_s.to_string(),
446 source,
447 })?;
448 if let Some(en) = cfg.entries.last_mut() {
449 if let Some(kt) = en.key_table.as_mut() {
450 kt.keys.insert(
451 at,
452 KeyDef {
453 raw: val.to_string(),
454 expr,
455 },
456 );
457 }
458 }
459 }
460 b"UnitMix" => {
461 let unit = category_of(get(&a, "unit"))?;
462 if let Some(en) = cfg.entries.last_mut() {
463 en.unit_mix.push(UnitMix {
464 unit,
465 a: get(&a, "a").to_string(),
466 b: get(&a, "b").to_string(),
467 to: get(&a, "to").to_string(),
468 });
469 }
470 }
471 b"VirtualUnit" => {
472 let unit = category_of(get(&a, "unit"))?;
473 if let Some(en) = cfg.entries.last_mut() {
474 en.virtual_units.push(VirtualUnit {
475 unit,
476 from: parse_int(get(&a, "from"))?,
477 to: get(&a, "to").to_string(),
478 });
479 }
480 }
481 b"AutomataTable" => {
482 if let Some(en) = cfg.entries.last_mut() {
483 en.automata_default = get(&a, "default").to_string();
484 }
485 }
486 b"Automata" => {
487 if let Some(en) = cfg.entries.last_mut() {
488 en.automata.push(AutomataRow {
489 state: parse_int(get(&a, "state")).unwrap_or(0) as i64,
490 value: get(&a, "value").to_string(),
491 default: get(&a, "default").to_string(),
492 remark: get(&a, "remark").to_string(),
493 });
494 }
495 }
496 b"Bksp" => {
497 if let Some(en) = cfg.entries.last_mut() {
498 en.bksp.push(Bksp {
499 key: parse_int(get(&a, "key")).unwrap_or(0),
500 value1: get(&a, "value1").to_string(),
501 value2: get(&a, "value2").to_string(),
502 condition1: get(&a, "condition1").to_string(),
503 condition2: get(&a, "condition2").to_string(),
504 });
505 }
506 }
507 _ => {}
508 }
509 }
510 Event::End(e) => {
511 let name = e.name();
512 match name.as_ref() {
513 b"InputSchemeSetting" | b"GeneratorSetting" => section = Section::None,
514 b"InputLayer" => in_input_layer = false,
515 _ => {}
516 }
517 let _ = in_input_layer; }
519 Event::Eof => break,
520 _ => {}
521 }
522 }
523
524 Ok(cfg)
525}
526
527fn split_flags(s: &str) -> Vec<String> {
528 if s.is_empty() {
529 Vec::new()
530 } else {
531 s.split('|').map(|p| p.trim().to_string()).collect()
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 const MINI: &str = r#"<?xml version="1.0" encoding="utf-8"?>
541<EditContextSetting version="0x500">
542 <EditorLayer flag="DEL_MOVE|ARROW_MOVE">
543 <ShortcutTable>
544 <Shortcut key="VK_HANGUL" usage="IME_SWITCH" value="!A"/>
545 <Shortcut key="VK_HANJA" usage="KEYCHAR" value="C0|0x82"/>
546 </ShortcutTable>
547 <FinalConvTable>
548 <FinalConv from="0x1100" to="0x3131"/>
549 <FinalConv from="0x1161" to="0x314F"/>
550 <FinalConv from="0x11A8" to="0x3131"/>
551 </FinalConvTable>
552 </EditorLayer>
553 <InputLayer default="0" current="2">
554 <InputEntry>
555 <InputSchemeSetting flag="0" object="CBasicInputScheme">
556 <KeyTable name="mini" flag="0" from="33" to="126">
557 <Key at="0x6B" value="H3|G_"/>
558 <Key at="0x66" value="H3|A_"/>
559 <Key at="0x78" value="H3|_G"/>
560 <Key at="0x24" value="T ? H3|0x1F4 : 0x24"/>
561 </KeyTable>
562 </InputSchemeSetting>
563 <GeneratorSetting flag="0" object="CNgsImeEx" flagex="1">
564 <UnitMixTable>
565 <UnitMix unit="CHO" a="G_" b="G_" to="GG"/>
566 <UnitMix unit="CHO" a="G_" b="500" to="GG"/>
567 <UnitMix unit="JUNG" a="O_" b="A_" to="WA"/>
568 <UnitMix unit="JONG" a="R_" b="S_" to="RS"/>
569 </UnitMixTable>
570 <VirtualUnitTable>
571 <VirtualUnit unit="JUNG" from="128" to="O_"/>
572 <VirtualUnit unit="JUNG" from="130" to="EU"/>
573 </VirtualUnitTable>
574 <AutomataTable default="0">
575 <Automata state="0" value="1" default="0" remark="초기 상태"/>
576 <Automata state="2" value="A&&A!=500 ? 0 : B||C||A==500 ? 2 : -2" default="0" remark="완성"/>
577 </AutomataTable>
578 </GeneratorSetting>
579 </InputEntry>
580 <InputEntry>
581 <InputSchemeSetting object="CInputScheme"/>
582 <GeneratorSetting flag="0" object="CIme"/>
583 </InputEntry>
584 </InputLayer>
585</EditContextSetting>"#;
586
587 #[test]
588 fn parse_mini() {
589 let cfg = Config::parse(MINI).unwrap();
590 assert_eq!(cfg.version, "0x500");
591 assert_eq!(cfg.default_entry, 0);
592 assert_eq!(cfg.current_entry, 2);
593 assert_eq!(cfg.entries.len(), 2);
594 assert_eq!(cfg.editor.shortcuts.len(), 2);
595 assert_eq!(cfg.editor.final_conv.get(&0x1100), Some(&0x3131));
596
597 let e0 = &cfg.entries[0];
598 assert_eq!(e0.scheme_object, "CBasicInputScheme");
599 assert_eq!(e0.generator_object, "CNgsImeEx");
600 let kt = e0.key_table.as_ref().unwrap();
601 assert_eq!(kt.name, "mini");
602 assert_eq!(kt.keys.len(), 4);
603 assert_eq!(e0.unit_mix.len(), 4);
604 assert_eq!(e0.virtual_units.len(), 2);
605 assert_eq!(e0.automata.len(), 2);
606 assert_eq!(e0.automata_default, "0");
607
608 assert_eq!(cfg.entries[1].scheme_object, "CInputScheme");
610 assert!(cfg.entries[1].key_table.is_none());
611
612 assert_eq!(cfg.first_hangul_entry(), Some(0));
613 }
614
615 #[test]
616 fn compile_mini() {
617 let cfg = Config::parse(MINI).unwrap();
618 let layout = cfg.compile(0).unwrap();
619 assert_eq!(layout.name, "mini");
620 assert_eq!(layout.keys.len(), 4);
621
622 assert_eq!(layout.combine(Category::Cho, 0x1100, 0x1100), Some(0x1101));
624 assert_eq!(
625 layout.combine(Category::Cho, 0x1100, unit::TOGGLE),
626 Some(0x1101)
627 );
628 assert_eq!(layout.combine(Category::Jung, 0x1169, 0x1161), Some(0x116A));
630 assert_eq!(layout.combine(Category::Jong, 0x11AF, 0x11BA), Some(0x11B3));
632
633 assert_eq!(
635 layout.virtual_units.get(&128),
636 Some(&Jamo::new(Category::Jung, 0x1169))
637 );
638 assert_eq!(
639 layout.virtual_units.get(&130),
640 Some(&Jamo::new(Category::Jung, 0x1173))
641 );
642
643 assert_eq!(
645 layout.standalone(Jamo::new(Category::Cho, 0x1100)),
646 Some('ㄱ')
647 );
648 assert_eq!(
649 layout.standalone(Jamo::new(Category::Jong, 0x11A8)),
650 Some('ㄱ')
651 );
652
653 assert_eq!(layout.automata.len(), 2);
655 assert_eq!(layout.automata_start, 0);
656 use crate::expr::{Ctx, Value};
658 let st2 = layout.automata.get(&2).expect("state 2");
659 assert_eq!(
660 st2.value.eval(&Ctx {
661 a: 1,
662 ..Default::default()
663 }),
664 Ok(Value::Int(0))
665 );
666 }
667
668 #[test]
669 fn compile_bksp_behavior() {
670 let xml = r#"<?xml version="1.0" encoding="utf-8"?>
672<EditContextSetting version="0x500">
673 <EditorLayer flag="0"><FinalConvTable/></EditorLayer>
674 <InputLayer default="0" current="0">
675 <InputEntry>
676 <InputSchemeSetting object="CBasicInputScheme">
677 <KeyTable name="t" flag="0" from="33" to="126"><Key at="0x6B" value="H3|G_"/></KeyTable>
678 </InputSchemeSetting>
679 <GeneratorSetting object="CNgsImeEx">
680 <UnitMixTable/><VirtualUnitTable/><AutomataTable default="0"/>
681 <Extra>
682 <Bksp key="1" value1="ByUnitStep|BkspAttach" value2="BySyllable" condition1="0" condition2="0"/>
683 <Bksp key="2" value1="0" value2="BySyllable" condition1="0" condition2="0"/>
684 </Extra>
685 </GeneratorSetting>
686 </InputEntry>
687 </InputLayer>
688</EditContextSetting>"#;
689 let layout = Config::parse(xml).unwrap().compile(0).unwrap();
690 assert_eq!(layout.bksp.composing, BkspUnit::LastKey); assert_eq!(layout.bksp.idle, BkspUnit::Syllable); assert!(layout.bksp.attach); }
694
695 #[test]
696 fn compile_bksp_default_when_absent() {
697 let layout = Config::parse(MINI).unwrap().compile(0).unwrap();
699 assert_eq!(layout.bksp.composing, BkspUnit::LastKey);
700 assert!(!layout.bksp.attach);
701 }
702}