Skip to main content

lindera_nodejs/
tokenizer.rs

1//! Tokenizer implementation for morphological analysis.
2//!
3//! This module provides a builder pattern for creating tokenizers and the tokenizer itself.
4//! The build-flow orchestration is delegated to
5//! [`lindera_binding_core::CoreTokenizerBuilder`] / [`lindera_binding_core::CoreTokenizer`];
6//! this module only adds the napi wrappers and the JS-value conversion.
7
8use std::path::Path;
9
10use lindera_binding_core::{CoreTokenizer, CoreTokenizerBuilder};
11
12use crate::dictionary::{JsDictionary, JsUserDictionary};
13use crate::error::to_napi_error;
14use crate::token::{JsNbestResult, JsToken};
15use crate::util::js_value_to_serde_value;
16
17/// Builder for creating a Tokenizer with custom configuration.
18///
19/// The builder pattern allows for fluent configuration of tokenizer parameters including
20/// dictionaries, modes, and filter pipelines.
21#[napi(js_name = "TokenizerBuilder")]
22pub struct JsTokenizerBuilder {
23    inner: CoreTokenizerBuilder,
24}
25
26#[napi]
27impl JsTokenizerBuilder {
28    /// Creates a new TokenizerBuilder with default configuration.
29    #[napi(constructor)]
30    pub fn new() -> napi::Result<Self> {
31        let inner = CoreTokenizerBuilder::new().map_err(to_napi_error)?;
32        Ok(Self { inner })
33    }
34
35    /// Loads configuration from a JSON file.
36    ///
37    /// # Arguments
38    ///
39    /// * `file_path` - Path to the configuration file.
40    ///
41    /// # Returns
42    ///
43    /// A new TokenizerBuilder with the loaded configuration.
44    #[napi]
45    pub fn from_file(&self, file_path: String) -> napi::Result<JsTokenizerBuilder> {
46        let inner =
47            CoreTokenizerBuilder::from_file(Path::new(&file_path)).map_err(to_napi_error)?;
48        Ok(JsTokenizerBuilder { inner })
49    }
50
51    /// Sets the tokenization mode.
52    ///
53    /// # Arguments
54    ///
55    /// * `mode` - Mode string ("normal" or "decompose").
56    #[napi]
57    pub fn set_mode(&mut self, mode: String) -> napi::Result<()> {
58        self.inner.set_mode(&mode).map_err(to_napi_error)?;
59        Ok(())
60    }
61
62    /// Sets the dictionary path or URI.
63    ///
64    /// # Arguments
65    ///
66    /// * `path` - Path to the dictionary directory or embedded URI (e.g. "embedded://ipadic").
67    #[napi]
68    pub fn set_dictionary(&mut self, path: String) {
69        self.inner.set_dictionary(&path);
70    }
71
72    /// Sets the user dictionary URI.
73    ///
74    /// # Arguments
75    ///
76    /// * `uri` - URI to the user dictionary.
77    #[napi]
78    pub fn set_user_dictionary(&mut self, uri: String) {
79        self.inner.set_user_dictionary(&uri);
80    }
81
82    /// Sets whether to keep whitespace in tokenization results.
83    ///
84    /// # Arguments
85    ///
86    /// * `keep_whitespace` - If true, whitespace tokens will be included in results.
87    #[napi]
88    pub fn set_keep_whitespace(&mut self, keep_whitespace: bool) {
89        self.inner.set_keep_whitespace(keep_whitespace);
90    }
91
92    /// Appends a character filter to the preprocessing pipeline.
93    ///
94    /// # Arguments
95    ///
96    /// * `kind` - Type of character filter to add (e.g. "unicode_normalize", "mapping").
97    /// * `args` - Optional filter arguments as a JSON-compatible object.
98    #[napi]
99    pub fn append_character_filter(
100        &mut self,
101        kind: String,
102        args: Option<serde_json::Value>,
103    ) -> napi::Result<()> {
104        let filter_args = js_value_to_serde_value(args);
105        self.inner.append_character_filter(&kind, &filter_args);
106        Ok(())
107    }
108
109    /// Appends a token filter to the postprocessing pipeline.
110    ///
111    /// # Arguments
112    ///
113    /// * `kind` - Type of token filter to add (e.g. "lowercase", "japanese_stop_tags").
114    /// * `args` - Optional filter arguments as a JSON-compatible object.
115    #[napi]
116    pub fn append_token_filter(
117        &mut self,
118        kind: String,
119        args: Option<serde_json::Value>,
120    ) -> napi::Result<()> {
121        let filter_args = js_value_to_serde_value(args);
122        self.inner.append_token_filter(&kind, &filter_args);
123        Ok(())
124    }
125
126    /// Builds the tokenizer with the configured settings.
127    ///
128    /// # Returns
129    ///
130    /// A configured Tokenizer instance ready for use.
131    #[napi]
132    pub fn build(&self) -> napi::Result<JsTokenizer> {
133        let inner = self.inner.build().map_err(to_napi_error)?;
134        Ok(JsTokenizer { inner })
135    }
136}
137
138/// Tokenizer for performing morphological analysis.
139///
140/// The tokenizer processes text and returns tokens with their morphological features.
141#[napi(js_name = "Tokenizer")]
142pub struct JsTokenizer {
143    inner: CoreTokenizer,
144}
145
146#[napi]
147impl JsTokenizer {
148    /// Creates a new tokenizer with the given dictionary and mode.
149    ///
150    /// # Arguments
151    ///
152    /// * `dictionary` - Dictionary to use for tokenization.
153    /// * `mode` - Tokenization mode ("normal" or "decompose"). Default: "normal".
154    /// * `user_dictionary` - Optional user dictionary for custom words.
155    #[napi(constructor)]
156    pub fn new(
157        dictionary: &JsDictionary,
158        mode: Option<String>,
159        user_dictionary: Option<&JsUserDictionary>,
160    ) -> napi::Result<Self> {
161        let mode_str = mode.unwrap_or_else(|| "normal".to_string());
162        let dict = dictionary.inner.clone();
163        let user_dict = user_dictionary.map(|d| d.inner.clone());
164
165        let inner =
166            CoreTokenizer::from_segmenter(&mode_str, dict, user_dict).map_err(to_napi_error)?;
167
168        Ok(Self { inner })
169    }
170
171    /// Tokenizes the given text.
172    ///
173    /// # Arguments
174    ///
175    /// * `text` - Text to tokenize.
176    ///
177    /// # Returns
178    ///
179    /// An array of Token objects containing morphological features.
180    #[napi]
181    pub fn tokenize(&self, text: String) -> napi::Result<Vec<JsToken>> {
182        let views = self.inner.tokenize(&text).map_err(to_napi_error)?;
183        Ok(views.into_iter().map(JsToken::from_view).collect())
184    }
185
186    /// Tokenizes the given text and returns N-best results.
187    ///
188    /// # Arguments
189    ///
190    /// * `text` - Text to tokenize.
191    /// * `n` - Number of N-best results to return.
192    /// * `unique` - If true, deduplicate results (default: false).
193    /// * `cost_threshold` - Maximum cost difference from the best path (default: undefined).
194    ///
195    /// # Returns
196    ///
197    /// An array of NbestResult objects, each containing tokens and their cost.
198    #[napi]
199    pub fn tokenize_nbest(
200        &self,
201        text: String,
202        n: u32,
203        unique: Option<bool>,
204        cost_threshold: Option<i64>,
205    ) -> napi::Result<Vec<JsNbestResult>> {
206        let results = self
207            .inner
208            .tokenize_nbest(&text, n as usize, unique.unwrap_or(false), cost_threshold)
209            .map_err(to_napi_error)?;
210
211        let js_results: Vec<JsNbestResult> = results
212            .into_iter()
213            .map(|(views, cost)| {
214                JsNbestResult::new(views.into_iter().map(JsToken::from_view).collect(), cost)
215            })
216            .collect();
217
218        Ok(js_results)
219    }
220}