//! Text import (File › Place of text files): plain text, Word documents (`.docx`) and RTF become //! a frameless [`Story`] with paragraph and character styles, local bold/italic/underline/size, //! footnotes (Word) and tables (Word), ready to flow into text frames. //! //! Word styles are imported by name (their font, size, bold/italic and space before/after become //! the style definition, like InDesign's "Use Word styles" import option). RTF keeps bold, //! italic, underline, size and paragraph breaks. #![forbid(unsafe_code)] mod docx; mod rtf; use designcraft_doc::{CharAttrs, ParaAttrs, Story}; /// A paragraph or character style found in the source. #[derive(Clone, Debug, Default, PartialEq)] pub struct ImportedStyle { pub name: String, pub based_on: Option, pub para: ParaAttrs, pub chars: CharAttrs, } /// The imported text. #[derive(Clone, Debug)] pub struct Imported { pub story: Story, pub para_styles: Vec, pub char_styles: Vec, /// Anything the import couldn't keep (images, fields, …). pub warnings: Vec, } #[derive(Debug, PartialEq)] pub enum ImportError { Unsupported(String), Corrupt(String), } impl std::fmt::Display for ImportError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ImportError::Unsupported(s) => write!(f, "unsupported text file: {s}"), ImportError::Corrupt(s) => write!(f, "can't read the file: {s}"), } } } /// Text files this crate reads, by extension. pub fn is_text_file(name: &str) -> bool { let l = name.to_ascii_lowercase(); l.ends_with(".txt") || l.ends_with(".text") || l.ends_with(".md") || l.ends_with(".docx") || l.ends_with(".rtf") } /// Import `bytes` (format from the file name, else sniffed). pub fn import(name: &str, bytes: &[u8]) -> Result { let l = name.to_ascii_lowercase(); if l.ends_with(".docx") || bytes.starts_with(b"PK\x03\x04") { return docx::import(bytes); } if l.ends_with(".rtf") || bytes.starts_with(b"{\\rtf") { return rtf::import(bytes); } if l.ends_with(".doc") { return Err(ImportError::Unsupported("legacy Word .doc (save it as .docx)".into())); } Ok(plain(&String::from_utf8_lossy(bytes))) } /// Plain text: line breaks become paragraphs (CRLF / CR normalised). pub fn plain(text: &str) -> Imported { let t = text.trim_start_matches('\u{FEFF}').replace("\r\n", "\n").replace('\r', "\n"); let story = Story::with_text(designcraft_doc::StoryId(0), &t, Default::default()); Imported { story, para_styles: vec![], char_styles: vec![], warnings: vec![] } } #[cfg(test)] mod tests { use super::*; #[test] fn plain_text_paragraphs() { let i = import("a.txt", b"\xEF\xBB\xBFOne\r\nTwo\rThree").unwrap(); assert_eq!(i.story.text, "One\nTwo\nThree"); assert_eq!(i.story.paras.len(), 3); assert!(is_text_file("Report.DOCX") && !is_text_file("a.png")); assert!(matches!(import("x.doc", b"\xD0\xCF"), Err(ImportError::Unsupported(_)))); } }