mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-15 06:52:23 +00:00
* 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.
81 lines
2.4 KiB
Rust
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
|
|
}
|
|
}
|