Skip to main content

inputx_nihongo_wasm/
lib.rs

1//! WASM bindings for `inputx-nihongo`. Exposes a JS-friendly
2//! `JapaneseEngine` so browser / Node consumers can drive the
3//! Japanese IME engine via `wasm-pack` output.
4//!
5//! Build:
6//!
7//!     wasm-pack build crates/inputx-nihongo-wasm --target web --release
8//!
9//! Usage (JS):
10//!
11//!     import init, { JapaneseEngine, KanaKind } from "@goliapkg/nihongo";
12//!     await init();
13//!     const eng = new JapaneseEngine();
14//!     for (const c of "shinjuku") eng.handleLetter(c.charCodeAt(0));
15//!     console.log(eng.candidates());
16//!     // [
17//!     //   { word: "新宿", kind: KanaKind.Kanji, freq: N,
18//!     //     composed: false, proximityMilli: 1000 },
19//!     //   { word: "しんじゅく", kind: KanaKind.Hiragana, freq: 0, ... },
20//!     //   { word: "シンジュク", kind: KanaKind.Katakana, freq: 0, ... },
21//!     // ]
22//!     const committed = eng.commitIndex(0); // "新宿"
23
24use wasm_bindgen::prelude::*;
25
26use inputx_nihongo::{
27    JapaneseEngine as CoreEngine, KanaKind as CoreKanaKind,
28};
29
30/// Kana kind mirrored for JS. Use as `KanaKind.Kanji` etc. Numeric
31/// values match the facade enum's discriminants.
32#[wasm_bindgen]
33#[derive(Copy, Clone)]
34pub enum KanaKind {
35    Hiragana = 0,
36    Katakana = 1,
37    Kanji = 2,
38}
39
40impl From<CoreKanaKind> for KanaKind {
41    fn from(k: CoreKanaKind) -> Self {
42        match k {
43            CoreKanaKind::Hiragana => KanaKind::Hiragana,
44            CoreKanaKind::Katakana => KanaKind::Katakana,
45            CoreKanaKind::Kanji => KanaKind::Kanji,
46        }
47    }
48}
49
50/// JP engine wrapping the embedded romaji / jukugo / kanji tables.
51/// Cheap to construct (all data is in const tables).
52#[wasm_bindgen]
53pub struct JapaneseEngine {
54    engine: CoreEngine,
55}
56
57#[wasm_bindgen]
58impl JapaneseEngine {
59    #[wasm_bindgen(constructor)]
60    pub fn new() -> JapaneseEngine {
61        Self { engine: CoreEngine::new() }
62    }
63
64    /// Push one ASCII letter byte (`'a'..='z'`, `'A'..='Z'`) into the
65    /// buffer and re-render candidates. The `-` byte is also accepted
66    /// (chōonpu `ー`) when the buffer is non-empty.
67    ///
68    /// Returns `true` if the byte was accepted; rejected bytes leave
69    /// state unchanged so the host controller can route the byte
70    /// elsewhere (locale punct mapping, raw passthrough).
71    #[wasm_bindgen(js_name = handleLetter)]
72    pub fn handle_letter(&mut self, byte: u8) -> bool {
73        self.engine.handle_letter(byte)
74    }
75
76    /// Pop one byte off the buffer. Returns `true` if state changed.
77    pub fn backspace(&mut self) -> bool {
78        self.engine.backspace()
79    }
80
81    /// Drop the buffer entirely. Returns `true` if state changed.
82    pub fn escape(&mut self) -> bool {
83        self.engine.escape()
84    }
85
86    /// Current buffer rendered as a string (for the preedit display).
87    pub fn preedit(&self) -> String {
88        self.engine.preedit().to_string()
89    }
90
91    #[wasm_bindgen(js_name = isComposing, getter)]
92    pub fn is_composing(&self) -> bool {
93        self.engine.is_composing()
94    }
95
96    /// Current candidates as a `js_sys::Array` of `Object`s. Each
97    /// object carries:
98    ///
99    ///   - `word: string`
100    ///   - `kind: KanaKind` (enum numeric value)
101    ///   - `freq: number` (0–100, higher = more common)
102    ///   - `composed: boolean` (`true` for mechanical
103    ///     `compose_sentence` products — particle/copula glue)
104    ///   - `proximityMilli: number` (1000 = exact match, < 1000 =
105    ///     prefix-prediction candidate whose reading the buffer is
106    ///     only a prefix of, e.g. `shinjuk → 新宿` at 875)
107    pub fn candidates(&self) -> js_sys::Array {
108        let arr = js_sys::Array::new();
109        for c in self.engine.candidates() {
110            let obj = js_sys::Object::new();
111            let _ = js_sys::Reflect::set(
112                &obj,
113                &"word".into(),
114                &JsValue::from_str(&c.word),
115            );
116            let kind: KanaKind = c.kind.into();
117            let _ = js_sys::Reflect::set(
118                &obj,
119                &"kind".into(),
120                &JsValue::from_f64(kind as u32 as f64),
121            );
122            let _ = js_sys::Reflect::set(
123                &obj,
124                &"freq".into(),
125                &JsValue::from_f64(c.freq as f64),
126            );
127            let _ = js_sys::Reflect::set(
128                &obj,
129                &"composed".into(),
130                &JsValue::from_bool(c.composed),
131            );
132            let _ = js_sys::Reflect::set(
133                &obj,
134                &"proximityMilli".into(),
135                &JsValue::from_f64(c.proximity_milli as f64),
136            );
137            arr.push(&obj);
138        }
139        arr
140    }
141
142    /// Commit the candidate at `index`. Returns the committed text and
143    /// clears the buffer. Returns `null` (via empty `JsValue`) if
144    /// `index` is out of range; state is unchanged on out-of-range.
145    #[wasm_bindgen(js_name = commitIndex)]
146    pub fn commit_index(&mut self, index: usize) -> Option<String> {
147        self.engine.commit_index(index)
148    }
149}
150
151impl Default for JapaneseEngine {
152    fn default() -> Self {
153        Self::new()
154    }
155}