Files
craft-libs/crates/wordmodel/src/block.rs
T
Dylan M. Taylor 08882273d1 Add craft-wordmodel: WordCraft's document model without its editing code (#963)
* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.
2026-11-11 19:43:08 -05:00

81 lines
2.4 KiB
Rust

use super::{Arc, Deserialize, Paragraph, Serialize, Table};
/// A block in a story. (Blocks are always behind an `Arc`, so the size difference is fine.)
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum Block {
Para(Paragraph),
Table(Table),
}
impl Block {
pub fn as_para(&self) -> Option<&Paragraph> {
match self {
Block::Para(p) => Some(p),
Block::Table(_) => None,
}
}
pub fn as_table(&self) -> Option<&Table> {
match self {
Block::Table(t) => Some(t),
Block::Para(_) => None,
}
}
}
pub type Blocks = Vec<Arc<Block>>;
/// Text boxes inside text boxes count (and show) this many levels deep.
pub const MAX_TEXT_BOX_DEPTH: usize = 4;
/// Bounds the work of expanding text boxes inside text boxes (layout, walks, saving), whatever a
/// document says: a box isn't expanded inside itself, nesting stops at [`MAX_TEXT_BOX_DEPTH`], and
/// an outermost box expands at most [`BoxBudget::MAX_NESTED`] boxes inside it. (A box whose
/// shapes all show that same box, or chains of boxes each showing the next many times, would
/// otherwise grow exponentially.) Outermost boxes aren't capped: their number is the document's
/// (or its pages', for headers), so the work stays linear and no box is left blank for budget.
#[derive(Debug, Default)]
pub struct BoxBudget {
open: Vec<u32>,
nested: usize,
total: usize,
}
impl BoxBudget {
/// Boxes expanded inside one outermost box.
pub const MAX_NESTED: usize = 64;
/// Start expanding text box story `id`; false if it mustn't be (pair a true with `leave`).
pub fn enter(&mut self, id: u32) -> bool {
if self.open.contains(&id) || self.open.len() >= MAX_TEXT_BOX_DEPTH {
return false;
}
if self.open.is_empty() {
self.nested = 0;
} else if self.nested >= Self::MAX_NESTED {
return false;
} else {
self.nested += 1;
}
self.total += 1;
self.open.push(id);
true
}
/// Done expanding the box last entered.
pub fn leave(&mut self) {
self.open.pop();
}
/// Whether a text box is being expanded now.
pub fn inside(&self) -> bool {
!self.open.is_empty()
}
/// Boxes expanded so far.
pub fn total(&self) -> usize {
self.total
}
}