use std::sync::Arc;
use crate::error::{DocError, Result};
use crate::model::{Block, BlockKind, Document, Page};
use crate::plugin::{FormatPlugin, ParseCapability, RawDoc, RawPage, RawTextItem};
use crate::registry::Registry;
#[cfg(feature = "csv")]
use super::csv;
#[cfg(feature = "html")]
use super::html;
#[cfg(feature = "md")]
use super::markdown;
pub fn read_text(path: &str) -> Result<String> {
let raw =
std::fs::read(path).map_err(|e| DocError::corrupt(format!("cannot read {path}: {e}")))?;
Ok(decode(raw))
}
pub fn decode(raw: Vec<u8>) -> String {
if let Some(rest) = raw.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
return String::from_utf8_lossy(rest).into_owned();
}
if let Some(rest) = raw.strip_prefix(&[0xFF, 0xFE]) {
return decode_utf16(rest, true);
}
if let Some(rest) = raw.strip_prefix(&[0xFE, 0xFF]) {
return decode_utf16(rest, false);
}
match String::from_utf8(raw) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
}
}
fn decode_utf16(bytes: &[u8], little_endian: bool) -> String {
let units: Vec<u16> = bytes
.chunks(2)
.filter(|c| c.len() == 2)
.map(|c| {
if little_endian {
u16::from_le_bytes([c[0], c[1]])
} else {
u16::from_be_bytes([c[0], c[1]])
}
})
.collect();
String::from_utf16_lossy(&units)
}
pub fn split_paragraphs(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut buf: Vec<&str> = Vec::new();
for line in text.lines() {
if line.trim().is_empty() {
if !buf.is_empty() {
out.push(buf.join("\n").trim().to_string());
buf.clear();
}
} else {
buf.push(line);
}
}
if !buf.is_empty() {
out.push(buf.join("\n").trim().to_string());
}
out.retain(|s| !s.is_empty());
out
}
pub struct TextParseCapability {
pub id: &'static str,
pub fmt: &'static str,
}
impl ParseCapability for TextParseCapability {
fn id(&self) -> &str {
self.id
}
fn formats(&self) -> &[&str] {
std::slice::from_ref(&self.fmt)
}
fn parse(&self, path: &str) -> Result<RawDoc> {
let content = read_text(path)?;
let items: Vec<RawTextItem> = split_paragraphs(&content)
.into_iter()
.map(|t| RawTextItem {
text: t,
bbox: None,
})
.collect();
Ok(RawDoc {
pages: vec![RawPage {
name: None,
items,
..Default::default()
}],
..Default::default()
})
}
}
pub struct TextPlugin;
impl FormatPlugin for TextPlugin {
fn id(&self) -> &str {
"txt"
}
fn formats(&self) -> &[&str] {
&["txt"]
}
fn default_parse_chain(&self) -> Vec<&'static str> {
vec!["text.read.txt"]
}
fn default_process_chain(&self) -> Vec<&'static str> {
vec![]
}
fn assemble(&self, raw: &RawDoc) -> Document {
let items: Vec<&str> = raw
.pages
.first()
.map(|p| p.items.iter().map(|i| i.text.as_str()).collect())
.unwrap_or_default();
let blocks: Vec<Block> = items
.iter()
.filter(|t| !t.trim().is_empty())
.map(|t| Block {
kind: BlockKind::Paragraph,
content: (*t).to_string(),
..Default::default()
})
.collect();
Document {
pages: vec![Page {
blocks,
text: items.join("\n\n"),
..Default::default()
}],
..Default::default()
}
}
}
pub struct WholeTextParseCapability {
pub id: &'static str,
pub fmt: &'static str,
}
impl ParseCapability for WholeTextParseCapability {
fn id(&self) -> &str {
self.id
}
fn formats(&self) -> &[&str] {
std::slice::from_ref(&self.fmt)
}
fn parse(&self, path: &str) -> Result<RawDoc> {
let content = read_text(path)?;
Ok(RawDoc {
pages: vec![RawPage {
items: vec![RawTextItem {
text: content,
bbox: None,
}],
..Default::default()
}],
..Default::default()
})
}
}
#[cfg(feature = "csv")]
pub struct CsvParseCapability;
#[cfg(feature = "csv")]
impl ParseCapability for CsvParseCapability {
fn id(&self) -> &str {
"text.read.csv"
}
fn formats(&self) -> &[&str] {
&["csv"]
}
fn parse(&self, path: &str) -> Result<RawDoc> {
let content = read_text(path)?;
let rows = csv::parse_rows(&content);
Ok(RawDoc {
pages: vec![RawPage {
tables: if rows.is_empty() { vec![] } else { vec![rows] },
..Default::default()
}],
..Default::default()
})
}
}
pub fn register_defaults(reg: &mut Registry) {
let _ = reg.register_parse(
Arc::new(TextParseCapability {
id: "text.read.txt",
fmt: "txt",
}),
None,
);
#[cfg(feature = "md")]
{
let _ = reg.register_parse(
Arc::new(WholeTextParseCapability {
id: "text.read.md",
fmt: "md",
}),
None,
);
reg.register_plugin(Arc::new(markdown::MarkdownPlugin));
}
#[cfg(feature = "csv")]
{
let _ = reg.register_parse(Arc::new(CsvParseCapability), None);
reg.register_plugin(Arc::new(csv::CsvPlugin));
}
#[cfg(feature = "html")]
{
let _ = reg.register_parse(
Arc::new(WholeTextParseCapability {
id: "html.parse",
fmt: "html",
}),
None,
);
reg.register_plugin(Arc::new(html::HtmlPlugin));
}
reg.register_plugin(Arc::new(TextPlugin));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paragraphs_split_on_blank_lines() {
assert_eq!(
split_paragraphs("a\nb\n\nc\n\n\n d "),
vec!["a\nb", "c", "d"]
);
assert_eq!(split_paragraphs(" \n\n"), Vec::<String>::new());
}
#[test]
fn decode_handles_bom_and_utf16() {
let mut utf8_bom = vec![0xEF, 0xBB, 0xBF];
utf8_bom.extend_from_slice("你好".as_bytes());
assert_eq!(decode(utf8_bom), "你好");
let mut utf16 = vec![0xFF, 0xFE];
for u in "ab".encode_utf16() {
utf16.extend_from_slice(&u.to_le_bytes());
}
assert_eq!(decode(utf16), "ab");
assert!(decode(vec![0x61, 0xFF, 0x62]).contains('a'));
}
#[test]
fn txt_plugin_splits_paragraphs() {
let raw = RawDoc {
pages: vec![RawPage {
items: vec![
RawTextItem {
text: "p1".into(),
bbox: None,
},
RawTextItem {
text: "p2".into(),
bbox: None,
},
],
..Default::default()
}],
..Default::default()
};
let doc = TextPlugin.assemble(&raw);
assert_eq!(doc.pages[0].blocks.len(), 2);
assert_eq!(doc.pages[0].text, "p1\n\np2");
}
}