use anyhow::{Context, Result};
use madi_core::Token;
use std::path::Path;
pub const BUILTIN_SKIP: &str = include_str!("../boilerplate.txt");
#[derive(Default, Clone)]
pub struct SkipRules {
rules: Vec<Vec<Cond>>,
remove: Vec<String>,
split: Vec<String>,
}
#[derive(Clone)]
enum Cond {
Prefix(String),
Suffix(String),
Contains(String),
Exact(String),
LenLt(usize),
}
impl SkipRules {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path).with_context(|| path.display().to_string())?;
Self::parse(&text, &path.display().to_string())
}
pub fn builtin() -> Self {
Self::parse(BUILTIN_SKIP, "builtin").expect("builtin boilerplate rules")
}
pub fn parse(text: &str, origin: &str) -> Result<Self> {
let mut r = SkipRules::default();
for (ln, raw) in text.lines().enumerate() {
let line = raw.trim_start_matches('\u{FEFF}').trim_end();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(rest) = line.strip_prefix('-') {
if !rest.is_empty() {
r.remove.push(rest.to_string());
}
continue;
}
if let Some(rest) = line.strip_prefix('|') {
if !rest.is_empty() {
r.split.push(rest.to_string());
}
continue;
}
let mut conds = Vec::new();
for part in line.split(" && ") {
let part = part.trim();
if part.is_empty() {
continue;
}
let c = if let Some(n) = part.strip_prefix("len<") {
Cond::LenLt(n.parse().with_context(|| format!("{}:{}: len<N", origin, ln + 1))?)
} else if let Some(x) = part.strip_prefix('^') {
Cond::Prefix(x.to_string())
} else if let Some(x) = part.strip_prefix('~') {
Cond::Contains(x.to_string())
} else if let Some(x) = part.strip_prefix('=') {
Cond::Exact(x.to_string())
} else if let Some(x) = part.strip_suffix('$') {
Cond::Suffix(x.to_string())
} else {
anyhow::bail!("{}:{}: 규칙 형식 오류: {}", origin, ln + 1, part);
};
conds.push(c);
}
if !conds.is_empty() {
r.rules.push(conds);
}
}
Ok(r)
}
pub fn len(&self) -> usize {
self.rules.len() + self.remove.len() + self.split.len()
}
pub fn clean(&self, text: &str) -> String {
let mut t: String = text.chars().filter(|c| !matches!(c, '\u{FEFF}' | '\u{200B}' | '\u{200C}' | '\u{200D}')).collect();
for r in &self.remove {
if t.contains(r.as_str()) {
t = t.replace(r.as_str(), "");
}
}
for r in &self.split {
if t.contains(r.as_str()) {
t = t.replace(r.as_str(), "\n");
}
}
t
}
pub fn skip(&self, sent: &str) -> bool {
let s = sent.trim();
let mut n_chars: Option<usize> = None;
'rule: for conds in &self.rules {
for c in conds {
let ok = match c {
Cond::Prefix(x) => s.starts_with(x.as_str()),
Cond::Suffix(x) => s.ends_with(x.as_str()),
Cond::Contains(x) => s.contains(x.as_str()),
Cond::Exact(x) => s == x,
Cond::LenLt(n) => *n_chars.get_or_insert_with(|| s.chars().count()) < *n,
};
if !ok {
continue 'rule;
}
}
return true;
}
false
}
}
pub fn split_sentences(text: &str) -> Vec<String> {
let mut out = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let chars: Vec<char> = line.chars().collect();
let mut start = 0;
let n = chars.len();
let mut i = 0;
while i < n {
let c = chars[i];
if matches!(c, '.' | '?' | '!' | '…') {
let mut j = i + 1;
while j < n && matches!(chars[j], '.' | '?' | '!' | '…') {
j += 1;
}
let quoted = j < n && matches!(chars[j], '"' | '”' | '’' | '\'' | ')' | ']' | '」' | '』' | '》' | '〉');
let prev_digit = i > 0 && chars[i - 1].is_ascii_digit();
let next_digit = j < n && chars[j].is_ascii_digit();
let at_end = j >= n;
let next_space = j < n && chars[j].is_whitespace();
if (at_end || next_space) && !quoted && !(c == '.' && prev_digit && next_digit) {
let s: String = chars[start..j].iter().collect();
let s = s.trim();
if !s.is_empty() {
out.push(s.to_string());
}
start = j;
}
i = j.max(i + 1);
continue;
}
i += 1;
}
if start < n {
let s: String = chars[start..].iter().collect();
let s = s.trim();
if !s.is_empty() {
out.push(s.to_string());
}
}
}
out
}
pub fn format_tokens(toks: &[Token]) -> String {
let mut s = String::new();
for (i, t) in toks.iter().enumerate() {
if i > 0 {
if t.space_before {
s.push_str(" | ");
} else {
s.push(' ');
}
}
s.push_str(&t.form.replace(' ', "_"));
s.push('/');
s.push_str(t.tag.code());
}
s
}