mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 21:56:50 -06:00
An .xlsx cell reference far from the others no longer allocates the whole range before the cell cap (billions of cells, out of memory), and long column names can't overflow. IDML import doesn't trust zip part sizes for up-front allocation and adds header/footer counts saturating. Tagged Text and IDML export use let-else; the font fallback has a compiled-in last resort. These crates deny the panicking lints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.4 KiB
Rust
100 lines
3.4 KiB
Rust
//! 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.
|
||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||
#![forbid(unsafe_code)]
|
||
|
||
mod docx;
|
||
pub mod export;
|
||
mod rtf;
|
||
pub mod tagged;
|
||
mod xlsx;
|
||
|
||
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<String>,
|
||
pub para: ParaAttrs,
|
||
pub chars: CharAttrs,
|
||
}
|
||
|
||
/// The imported text.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Imported {
|
||
pub story: Story,
|
||
pub para_styles: Vec<ImportedStyle>,
|
||
pub char_styles: Vec<ImportedStyle>,
|
||
/// Anything the import couldn't keep (images, fields, …).
|
||
pub warnings: Vec<String>,
|
||
}
|
||
|
||
#[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") || l.ends_with(".xlsx")
|
||
}
|
||
|
||
/// Import `bytes` (format from the file name, else sniffed).
|
||
pub fn import(name: &str, bytes: &[u8]) -> Result<Imported, ImportError> {
|
||
let l = name.to_ascii_lowercase();
|
||
if l.ends_with(".xlsx") {
|
||
return xlsx::import(bytes);
|
||
}
|
||
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 tagged::is_tagged(bytes) {
|
||
return tagged::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(_))));
|
||
}
|
||
}
|