Files
designcraft/crates/textimport/src/lib.rs
T
Brandon ThomasandClaude Opus 5.5 446c70af04 H: place text files — Word (.docx), RTF, plain text; autoflow
- designcraft-textimport (L3): DOCX (styles by name with spacing/indents/alignment and font,
  size, bold/italic/underline/strike/caps/position; runs, tabs, breaks, footnotes, tables,
  hyperlink/field results; used styles only), RTF (text, paragraphs, bold/italic/underline/
  strike, size, Unicode/cp1252, skipped destinations), plain text (+ tests)
- engine: file.place routes text files into the insertion point, the selected frame or a new
  frame on the page; imported styles added (existing win); removeStyles; autoflow adds pages
  with threaded frames until the story fits (+ test)
- app/web: Place dialogs accept .docx/.rtf/.txt/.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:55:28 -04:00

90 lines
3.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<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")
}
/// 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(".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(_))));
}
}