use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;
use std::str::FromStr;
use lindera::LinderaResult;
use lindera::dictionary::Lattice;
use lindera::error::{LinderaError, LinderaErrorKind};
use lindera::mode::Mode;
use lindera::token::Token;
use lindera_analysis::character_filter::CharacterFilterLoader;
use lindera_analysis::token_filter::TokenFilterLoader;
use lindera_analysis::tokenizer::TokenizerBuilder;
use lindera_cli::get_version;
use super::io_err;
#[derive(Debug, clap::Args)]
#[clap(
author,
about = "Tokenize text using a morphological analysis dictionary",
version = get_version(),
)]
pub struct TokenizeArgs {
#[clap(
short = 'd',
long = "dict",
required = true,
help = "Dictionary directory path, URI, or downloaded dictionary name (e.g., embedded://ipadic, /path/to/dictionary, ipadic)"
)]
dict: String,
#[clap(
short = 'o',
long = "output",
default_value = "mecab",
help = "Output format (mecab|wakati|json)"
)]
output: String,
#[clap(
short = 'u',
long = "user-dict",
help = "User dictionary path or URI (optional)"
)]
user_dict: Option<String>,
#[clap(
short = 'm',
long = "mode",
default_value = "normal",
help = "Tokenization mode (normal|decompose)"
)]
mode: Mode,
#[clap(
short = 'c',
long = "char-filter",
help = "Character filter config (JSON)"
)]
character_filters: Option<Vec<String>>,
#[clap(
short = 't',
long = "token-filter",
help = "Token filter config (JSON)"
)]
token_filters: Option<Vec<String>>,
#[clap(
long = "keep-whitespace",
help = "Keep whitespace tokens in output (default: whitespace is ignored for MeCab compatibility)"
)]
keep_whitespace: bool,
#[clap(
long = "mmap",
help = "Use memory-mapped file loading for the dictionary directory's word list. Ignored for embedded:// dictionaries and when the mmap feature is disabled. Rebuilding or truncating the dictionary directory while a process holds it mapped can cause a SIGBUS on the next lookup."
)]
use_mmap: bool,
#[clap(
short = 'N',
long = "nbest",
default_value = "1",
help = "Number of N-best results (default: 1)"
)]
nbest: usize,
#[clap(
long = "nbest-unique",
help = "Deduplicate N-best results with the same word boundaries (keeps only the lowest-cost POS variant)"
)]
nbest_unique: bool,
#[clap(
long = "nbest-cost-threshold",
help = "Maximum cost difference from best path for N-best results (e.g. 10000)"
)]
nbest_cost_threshold: Option<i64>,
#[clap(help = "Input text file (default: stdin)")]
input_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy)]
pub enum Format {
Mecab,
Wakati,
Json,
}
impl FromStr for Format {
type Err = LinderaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"mecab" => Ok(Format::Mecab),
"wakati" => Ok(Format::Wakati),
"json" => Ok(Format::Json),
_ => Err(LinderaErrorKind::Args.with_error(anyhow::anyhow!("Invalid format: {s}"))),
}
}
}
fn write_output<W: Write>(
writer: &mut W,
format: Format,
tokens: Vec<Token>,
details_buf: &mut String,
) -> LinderaResult<()> {
match format {
Format::Mecab => mecab_output(writer, tokens, details_buf),
Format::Json => json_output(writer, tokens),
Format::Wakati => wakati_output(writer, tokens),
}
}
fn mecab_output<W: Write>(
writer: &mut W,
mut tokens: Vec<Token>,
details_buf: &mut String,
) -> LinderaResult<()> {
for token in tokens.iter_mut() {
details_buf.clear();
for (i, detail) in token.details().iter().enumerate() {
if i > 0 {
details_buf.push(',');
}
details_buf.push_str(detail);
}
writeln!(writer, "{}\t{}", token.surface.as_ref(), details_buf).map_err(io_err)?;
}
writeln!(writer, "EOS").map_err(io_err)?;
Ok(())
}
fn json_output<W: Write>(writer: &mut W, mut tokens: Vec<Token>) -> LinderaResult<()> {
let mut json_tokens = Vec::new();
for token in tokens.iter_mut() {
let token_value = token.as_value();
json_tokens.push(token_value);
}
serde_json::to_writer_pretty(&mut *writer, &json_tokens)
.map_err(|err| LinderaErrorKind::Serialize.with_error(anyhow::anyhow!(err)))?;
writeln!(writer).map_err(io_err)?;
Ok(())
}
fn wakati_output<W: Write>(writer: &mut W, tokens: Vec<Token>) -> LinderaResult<()> {
let mut it = tokens.iter().peekable();
while let Some(token) = it.next() {
if it.peek().is_some() {
write!(writer, "{} ", token.surface.as_ref()).map_err(io_err)?;
} else {
writeln!(writer, "{}", token.surface.as_ref()).map_err(io_err)?;
}
}
Ok(())
}
pub fn tokenize(args: TokenizeArgs) -> LinderaResult<()> {
let mut builder = TokenizerBuilder::new()?;
let dict_uri = crate::dictionary_registry::resolve_dictionary_arg(
args.dict.as_str(),
std::env::var_os(crate::dictionary_registry::DATA_DIR_ENV).map(PathBuf::from),
get_version(),
)?;
builder.set_segmenter_dictionary(dict_uri.as_str());
if let Some(user_dic_uri) = args.user_dict {
builder.set_segmenter_user_dictionary(user_dic_uri.as_str());
}
builder.set_segmenter_mode(&args.mode);
if args.keep_whitespace {
builder.set_segmenter_keep_whitespace(true);
}
if args.use_mmap {
builder.set_segmenter_use_mmap(true);
}
let mut tokenizer = builder
.build()
.map_err(|err| LinderaErrorKind::Args.with_error(err))?;
let output_format = Format::from_str(args.output.as_str())?;
for filter in args.character_filters.iter().flatten() {
let character_filter = CharacterFilterLoader::load_from_cli_flag(filter)?;
tokenizer.append_character_filter(character_filter);
}
for filter in args.token_filters.iter().flatten() {
let token_filter = TokenFilterLoader::load_from_cli_flag(filter)?;
tokenizer.append_token_filter(token_filter);
}
let mut reader: Box<dyn BufRead> = if let Some(input_file) = args.input_file {
Box::new(BufReader::new(File::open(input_file).map_err(io_err)?))
} else {
Box::new(BufReader::new(io::stdin()))
};
let nbest = args.nbest;
let nbest_unique = args.nbest_unique;
let nbest_cost_threshold = args.nbest_cost_threshold;
let mut lattice = Lattice::default();
let stdout = io::stdout();
let mut writer = BufWriter::new(stdout.lock());
let mut text = String::new();
let mut details_buf = String::new();
loop {
text.clear();
let size = reader.read_line(&mut text).map_err(io_err)?;
if size == 0 {
break;
}
if nbest >= 2 {
let results = tokenizer.tokenize_nbest_with_lattice(
text.trim(),
&mut lattice,
nbest,
nbest_unique,
nbest_cost_threshold,
)?;
for (rank, (tokens, cost)) in results.into_iter().enumerate() {
writeln!(writer, "NBEST {} (cost={})", rank + 1, cost).map_err(io_err)?;
write_output(&mut writer, output_format, tokens, &mut details_buf)?;
}
} else {
let tokens = tokenizer.tokenize_with_lattice(text.trim(), &mut lattice)?;
write_output(&mut writer, output_format, tokens, &mut details_buf)?;
}
}
writer.flush().map_err(io_err)?;
Ok(())
}