mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
- 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>
90 lines
3.1 KiB
Rust
90 lines
3.1 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.
|
||
#![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(_))));
|
||
}
|
||
}
|