use aho_corasick::{AhoCorasick, AhoCorasickBuilder, MatchKind};
use std::collections::HashMap;
use std::iter::IntoIterator;
use std::str::FromStr;
use once_cell::unsync::Lazy;
use regex::Regex;
use crate::{
pagerules::PageRules,
rule::{Conv, ConvAction, ConvRule},
variant::Variant,
};
const NESTED_RULE_MAX_DEPTH: usize = 10;
pub struct ZhConverter {
variant: Variant,
automaton: AhoCorasick,
mapping: HashMap<String, String>,
}
impl ZhConverter {
pub fn new(automaton: AhoCorasick, mapping: HashMap<String, String>) -> ZhConverter {
ZhConverter {
variant: Variant::Zh,
automaton,
mapping,
}
}
#[inline(always)]
pub fn from_pairs(pairs: &[(impl AsRef<str>, impl AsRef<str>)]) -> ZhConverter {
let mut builder = ZhConverterBuilder::new();
for (from, to) in pairs {
builder = builder.add_conv_pair(from.to_owned(), to.to_owned());
}
builder.build()
}
pub fn convert(&self, text: &str) -> String {
let mut output = String::with_capacity(text.len());
self.converted(text, &mut output);
output
}
pub fn converted(&self, text: &str, output: &mut String) {
let mut last = 0;
for (s, e) in self.automaton.find_iter(text).map(|m| (m.start(), m.end())) {
if s > last {
output.push_str(&text[last..s]);
}
output.push_str(self.mapping.get(&text[s..e]).unwrap());
last = e;
}
output.push_str(&text[last..]);
}
pub fn convert_allowing_inline_rules(&self, text: &str) -> String {
let p1 = Lazy::new(|| Regex::new(r#"-\{"#).unwrap()); let p2 = Lazy::new(|| Regex::new(r#"-\{|\}-"#).unwrap());
let mut pos = 0;
let mut converted = String::with_capacity(text.len());
let mut pieces = vec![];
while let Some(m1) = p1.find_at(text, pos) {
self.converted(&text[pos..m1.start()], &mut converted);
if m1.as_str() != "-{" {
converted.push_str(&text[m1.start()..m1.end()]); pos = m1.end();
continue;
}
pos = m1.start() + 2;
pieces.push(String::new());
while let Some(m2) = p2.find_at(text, pos) {
if m2.as_str() == "-{" {
if pieces.len() >= NESTED_RULE_MAX_DEPTH {
pos += 2;
continue;
}
pieces.last_mut().unwrap().push_str(&text[pos..m2.start()]);
pieces.push(String::new()); pos = m2.end();
} else {
let mut piece = pieces.pop().unwrap();
piece.push_str(&text[pos..m2.start()]);
let upper = if let Some(upper) = pieces.last_mut() {
upper
} else {
&mut converted
};
if let Ok(rule) = ConvRule::from_str(&piece) {
rule.write_output(upper, self.variant).unwrap();
} else {
upper.push_str(&piece);
}
pos = m2.end();
if pieces.is_empty() {
break;
}
}
}
while let Some(piece) = pieces.pop() {
converted.push_str("-{");
converted.push_str(&piece);
}
}
if pos < text.len() {
converted.push_str(&self.convert(&text[pos..]));
}
converted
}
}
#[derive(Debug, Clone, Default)]
pub struct ZhConverterBuilder<'t> {
target: Variant,
tables: Vec<(&'t str, &'t str)>,
adds: HashMap<String, String>,
removes: HashMap<String, String>, dfa: bool,
}
impl<'t> ZhConverterBuilder<'t> {
pub fn new() -> Self {
Default::default()
}
pub fn target(mut self, variant: Variant) -> Self {
self.target = variant;
self
}
pub fn table(mut self, table: (&'t str, &'t str)) -> Self {
self.tables.push(table);
self
}
#[inline(always)]
pub fn rules_from_page(self, text: &str) -> Self {
self.page_rules(
&PageRules::from_str(text).expect("Page rules parsing in infallible for now"),
)
}
#[inline(always)]
pub fn page_rules(self, page_rules: &PageRules) -> Self {
self.conv_actions(page_rules.as_conv_actions())
}
fn conv_actions<'i>(mut self, conv_actions: impl IntoIterator<Item = &'i ConvAction>) -> Self {
for conv_action in conv_actions {
let pairs = conv_action.as_conv().get_convs_by_target(self.target);
if conv_action.adds() {
self.adds
.extend(pairs.iter().map(|&(f, t)| (f.to_owned(), t.to_owned())));
} else {
self.removes
.extend(pairs.iter().map(|&(f, t)| (f.to_owned(), t.to_owned())));
}
}
self
}
pub fn add_conv(mut self, conv: Conv) -> Self {
let pairs = conv.get_convs_by_target(self.target);
self.adds
.extend(pairs.iter().map(|&(f, t)| (f.to_owned(), t.to_owned())));
self
}
pub fn remove_conv(mut self, conv: Conv) -> Self {
let pairs = conv.get_convs_by_target(self.target);
self.removes
.extend(pairs.iter().map(|&(f, t)| (f.to_owned(), t.to_owned())));
self
}
pub fn add_conv_pair(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
let (from, to): (&str, &str) = (from.as_ref(), to.as_ref());
if from.is_empty() {
panic!("Conv pair should have non-empty from.")
}
self.adds.insert(from.to_owned(), to.to_owned());
self
}
pub fn remove_conv_pair(mut self, from: impl AsRef<str>, to: impl AsRef<str>) -> Self {
self.removes
.insert(from.as_ref().to_owned(), to.as_ref().to_owned());
self
}
pub fn conv_lines(mut self, lines: &str) -> Self {
for line in lines.lines().map(str::trim).filter(|s| !s.is_empty()) {
if let Ok(conv) = Conv::from_str(line.trim()) {
dbg!(&conv);
self.adds
.extend(conv.get_convs_by_target(self.target).iter().map(|&(f, t)| {
if f.is_empty() {
panic!("Conv pair should have non-empty from.")
}
(f.to_owned(), t.to_owned())
}));
}
}
self
}
pub fn dfa(mut self, enabled: bool) -> Self {
self.dfa = enabled;
self
}
pub fn build(&self) -> ZhConverter {
let Self {
target,
tables,
dfa,
adds,
removes,
} = self;
let mut mapping = HashMap::with_capacity(
(tables.iter().map(|(fs, _ts)| fs.len()).sum::<usize>() + adds.len())
.saturating_sub(removes.len()),
);
mapping.extend(
tables
.iter()
.map(|(froms, tos)| itertools::zip(froms.trim().split('|'), tos.trim().split('|')))
.flatten()
.filter(|&(from, to)| !(from.is_empty() && to.is_empty())) .filter(|&(from, _to)| !removes.contains_key(from))
.map(|(from, to)| (from.to_owned(), to.to_owned())),
);
mapping.extend(
adds.iter()
.filter(|(from, _to)| !removes.contains_key(from.as_str()))
.map(|(from, to)| (from.to_owned(), to.to_owned())),
);
let sequence = mapping.keys();
let automaton = AhoCorasickBuilder::new()
.match_kind(MatchKind::LeftmostLongest)
.dfa(*dfa)
.build(sequence);
ZhConverter {
variant: *target,
mapping,
automaton,
}
}
}