use encoding_rs::{
Encoding, BIG5, EUC_KR, GBK, SHIFT_JIS, WINDOWS_1251, WINDOWS_1252, WINDOWS_1253, WINDOWS_1254,
WINDOWS_1255, WINDOWS_1256, WINDOWS_1258, WINDOWS_874,
};
use crate::clx::Piece;
use crate::list::Numberer;
use crate::papx::PapxTable;
pub(crate) fn encoding_for_codepage(cp: u16) -> &'static Encoding {
match cp {
949 => EUC_KR, 932 => SHIFT_JIS,
936 => GBK,
950 => BIG5,
1251 => WINDOWS_1251,
1253 => WINDOWS_1253,
1254 => WINDOWS_1254,
1255 => WINDOWS_1255,
1256 => WINDOWS_1256,
874 => WINDOWS_874,
1258 => WINDOWS_1258,
_ => WINDOWS_1252,
}
}
pub(crate) struct Decoded {
pub raw: String,
pub labeled: String,
}
pub(crate) fn decode_pieces(
word: &[u8],
pieces: &[Piece],
enc: &'static Encoding,
papx: &PapxTable,
numberer: &mut Numberer<'_>,
) -> Decoded {
let mut d = Decoded {
raw: String::new(),
labeled: String::new(),
};
let mut para_start = 0usize;
let budget = word.len().saturating_add(16);
let mut consumed = 0usize;
for p in pieces {
if p.cch == 0 {
continue;
}
if consumed >= budget {
break;
}
if p.compressed {
let end = p.fc.saturating_add(p.cch).min(word.len());
let Some(slice) = word.get(p.fc..end) else {
continue;
};
consumed = consumed.saturating_add(slice.len());
let mut buf: Vec<u8> = Vec::new();
for (j, &b) in slice.iter().enumerate() {
if b == 0x07 || b == 0x0D {
flush_bytes(&mut d, &mut buf, enc);
emit_mark(
&mut d,
&mut para_start,
papx,
numberer,
(p.fc + j) as u32,
b,
);
} else {
buf.push(b);
}
}
flush_bytes(&mut d, &mut buf, enc);
} else {
let byte_len = p.cch.saturating_mul(2);
let end = p.fc.saturating_add(byte_len).min(word.len());
let Some(slice) = word.get(p.fc..end) else {
continue;
};
consumed = consumed.saturating_add(slice.len());
let mut buf: Vec<u16> = Vec::new();
for (i, c) in slice.chunks_exact(2).enumerate() {
let u = u16::from_le_bytes([c[0], c[1]]);
if u == 0x0007 || u == 0x000D {
flush_units(&mut d, &mut buf);
emit_mark(
&mut d,
&mut para_start,
papx,
numberer,
(p.fc + i * 2) as u32,
u as u8,
);
} else {
buf.push(u);
}
}
flush_units(&mut d, &mut buf);
}
}
d
}
fn flush_bytes(d: &mut Decoded, buf: &mut Vec<u8>, enc: &'static Encoding) {
if !buf.is_empty() {
let s = enc.decode(buf).0;
d.raw.push_str(&s);
d.labeled.push_str(&s);
buf.clear();
}
}
fn flush_units(d: &mut Decoded, buf: &mut Vec<u16>) {
if !buf.is_empty() {
let s = String::from_utf16_lossy(buf);
d.raw.push_str(&s);
d.labeled.push_str(&s);
buf.clear();
}
}
fn emit_mark(
d: &mut Decoded,
para_start: &mut usize,
papx: &PapxTable,
numberer: &mut Numberer<'_>,
fc: u32,
mark: u8,
) {
if !numberer.is_empty() {
let (ilfo, ilvl) = papx.list_at(fc);
if ilfo > 0 {
if let Some(label) = numberer.label(ilfo, ilvl) {
d.labeled.insert_str(*para_start, &label);
}
}
}
let ch = if mark == 0x07 && !papx.is_empty() {
let (in_table, row_end) = papx.at(fc);
if in_table && !row_end {
'\t'
} else {
'\n'
}
} else {
'\n'
};
d.raw.push(ch);
d.labeled.push(ch);
*para_start = d.labeled.len();
}
pub(crate) fn strip_controls(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for ch in raw.chars() {
match ch {
'\u{13}' | '\u{14}' | '\u{15}' => {}
'\t' => out.push('\t'),
'\r' | '\u{0B}' | '\u{0C}' | '\u{0E}' | '\n' => out.push('\n'),
'\u{1E}' => out.push('-'),
'\u{A0}' => out.push(' '),
'\u{00}' => {}
c if (c as u32) < 0x20 => {}
c => out.push(c),
}
}
out
}
pub(crate) fn normalize_lines(text: &str) -> String {
let unified = text.replace("\r\n", "\n").replace('\r', "\n");
let mut lines: Vec<&str> = Vec::new();
for line in unified.split('\n') {
let trimmed_end = if line.chars().all(|ch| ch == '\t') {
line
} else {
line.trim_end()
};
let trimmed = trimmed_end.trim_start_matches(' ');
if trimmed.is_empty() {
continue;
}
if !trimmed.contains('\t')
&& !is_numeric_field_line(trimmed)
&& lines.last() == Some(&trimmed)
{
continue;
}
lines.push(trimmed);
}
lines.join("\n")
}
fn is_numeric_field_line(line: &str) -> bool {
let mut saw_digit = false;
for ch in line.chars() {
if ch.is_ascii_digit() {
saw_digit = true;
} else if !matches!(ch, ' ' | '\t' | '-' | '\u{2010}' | '\u{2011}' | '/' | '\\') {
return false;
}
}
saw_digit
}
pub(crate) fn finalize(raw: &str) -> String {
normalize_lines(&strip_controls(raw))
}
pub(crate) fn has_indexable(text: &str) -> bool {
text.chars()
.any(|c| c.is_alphanumeric() || ('가'..='힣').contains(&c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_duplicate_table_rows_but_collapses_prose() {
assert_eq!(normalize_lines("X\tY\nX\tY\n"), "X\tY\nX\tY");
assert_eq!(normalize_lines("2\n2\n"), "2\n2");
assert_eq!(normalize_lines("hi\nhi\n"), "hi");
}
#[test]
fn preserves_leading_empty_cell_drops_trailing_tab() {
assert_eq!(normalize_lines("\tB\t\n"), "\tB");
assert_eq!(normalize_lines("A\tB\t\n"), "A\tB");
assert_eq!(normalize_lines("\t\n-\n"), "\t\n-");
assert_eq!(normalize_lines(" hello \n"), "hello");
}
#[test]
fn overlapping_pieces_decode_is_bounded() {
let word = b"A\x00".repeat(50); let pieces: Vec<Piece> = (0..1000)
.map(|_| Piece {
cch: 50,
fc: 0,
compressed: false,
})
.collect();
let lists = crate::list::parse(&[], 0, 0, 0, 0);
let papx = crate::papx::parse(&[], &[], 0, 0);
let mut numberer = Numberer::new(&lists);
let d = decode_pieces(&word, &pieces, WINDOWS_1252, &papx, &mut numberer);
assert!(
d.raw.chars().count() < 1000,
"overlapping-piece decode was not bounded: {} chars",
d.raw.chars().count()
);
}
}