Files
wordcraft/crates/doc/src/edit.rs
T
NaxvogandClaude Opus 5.5 868fffa6fe Files: a Word chunk that asks to keep its source formatting keeps it (w:matchSrc)
An alternative format chunk holding a Word document with w:matchSrc keeps its
own styles: a style the document already has comes along under a new id, the
chunk's document defaults (and Word's, where they say nothing) go into its
base styles, and its unstyled paragraphs use its own Normal. Without
w:matchSrc the document's styles win, as before. Adopting another document
now also renumbers table styles and the revisions of table, row and cell
property changes.

A document whose chunk shares style names with it (Normal with an exact line
height, an italic Subtitle) ran a page longer than in Word: the chunk's tables
took the document's styles. It now keeps its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 07:04:31 +01:00

1475 lines
66 KiB
Rust

//! Range editing on a [`Document`]: insert text, split paragraphs, delete ranges across
//! paragraphs and tables, apply character/paragraph formatting, copy a range to a fragment and
//! paste a fragment.
use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::para::{InlineObject, Run};
use crate::props::{CharProps, ParaProps};
use crate::{Block, Blocks, DocError, Document, Paragraph, Part, PartKind, Path, Pos, Result, para_block};
use crate::{Table, table::MAX_TABLE_DEPTH};
/// A copied piece of a document: blocks, where the first and last paragraphs may be partial.
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
pub struct Fragment {
pub blocks: Vec<Block>,
/// Copies of the text box stories its shapes show, by the source document's part id. Pasting
/// gives each box a new story made from these.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub parts: BTreeMap<u32, Part>,
}
impl Fragment {
pub fn plain_text(&self) -> String {
self.to_document().plain_text(crate::StoryRef::Body)
}
/// The fragment as the body of a new document (default styles).
pub fn to_document(&self) -> Document {
let mut d = Document::new();
d.body = self.blocks.iter().cloned().map(Arc::new).collect();
d
}
/// Whether the fragment holds an equation (in tables too), which a copy also puts on the
/// system clipboard as MathML (#816).
pub fn has_equation(&self) -> bool {
let mut found = false;
for b in &self.blocks {
each_para(b, 0, &mut |p| found |= p.objects.iter().any(|o| matches!(o, InlineObject::Equation { .. })));
}
found
}
/// The equation (its linear format and whether it is a display equation) when the fragment
/// is that one equation and nothing else but white space and empty paragraphs: a copy of it
/// also goes on the system clipboard in the MathML formats that equation editors read (#1297).
pub fn sole_equation(&self) -> Option<(&str, bool)> {
let mut found = None;
for b in &self.blocks {
let Block::Para(p) = b else { return None };
for c in p.text.chars().filter(|c| !c.is_whitespace()) {
if c != crate::para::OBJ || found.is_some() {
return None;
}
// The paragraph's only visible character, so its first object.
match p.objects.first() {
Some(InlineObject::Equation { linear, display, .. }) => found = Some((linear.as_str(), *display)),
_ => return None,
}
}
}
found
}
/// The text a copy puts on the clipboard: [`Self::plain_text`] with symbol-font characters
/// (Wingdings…) as their Unicode equivalents.
pub fn clipboard_text(&self) -> String {
self.to_document().shown_text(crate::StoryRef::Body)
}
/// Add `piece` at the end, followed by a paragraph mark unless it already ends with one: how
/// Word's Spike collects what it cuts. The piece's text box stories get ids after this
/// fragment's, so pieces copied from different documents (or after an id was freed and reused)
/// never share one.
pub fn append_piece(&mut self, mut piece: Fragment) {
let Some(last) = piece.blocks.last() else { return };
let ends_with_mark = piece.blocks.len() >= 2 && matches!(last, Block::Para(p) if p.is_empty());
if !ends_with_mark {
let mut mark = Paragraph::new();
if let Block::Para(p) = last {
mark.props = p.props.clone();
}
piece.blocks.push(Block::Para(mark));
}
let next = self.parts.keys().next_back().map_or(1, |k| k.saturating_add(1));
let ids: BTreeMap<u32, u32> = piece.parts.keys().zip(next..=u32::MAX).map(|(old, new)| (*old, new)).collect();
let renumber = |b: &mut Block| {
each_para_mut(b, 0, &mut |p| {
let mut changed = false;
for o in p.objects.iter_mut() {
for slot in o.text_box_slots() {
if let Some(new) = slot.and_then(|old| ids.get(&old)) {
*slot = Some(*new);
changed = true;
}
}
}
if changed {
p.touch();
}
})
};
piece.blocks.iter_mut().for_each(renumber);
for (old, mut part) in std::mem::take(&mut piece.parts) {
part.blocks.iter_mut().map(Arc::make_mut).for_each(renumber);
if let Some(new) = ids.get(&old) {
self.parts.insert(*new, part);
}
}
// The empty paragraph after the last mark is where the next piece starts.
if self.blocks.len() >= 2 && matches!(self.blocks.last(), Some(Block::Para(p)) if p.is_empty()) {
self.blocks.pop();
}
self.blocks.append(&mut piece.blocks);
}
pub fn from_text(s: &str) -> Fragment {
let lines: Vec<&str> = s.split('\n').collect();
Fragment {
blocks: lines.iter().map(|l| Block::Para(Paragraph::with_text(l.trim_end_matches('\r'), CharProps::default()))).collect(),
..Default::default()
}
}
}
/// Whether block `b`, in a block list the paragraph walk reaches at `depth`, has cells the walk
/// skips ([`crate::PARA_WALK_DEPTH`]). Bounded by that depth.
fn deeper_than_walk(b: &Block, depth: usize) -> bool {
match b {
Block::Para(_) => false,
Block::Table(t) => t
.rows
.iter()
.flat_map(|r| &r.cells)
.any(|c| depth + 1 > crate::PARA_WALK_DEPTH || c.blocks.iter().any(|x| deeper_than_walk(x, depth + 1))),
}
}
fn order(a: &Pos, b: &Pos) -> (Pos, Pos) {
if a <= b { (a.clone(), b.clone()) } else { (b.clone(), a.clone()) }
}
/// The blocks in `t`'s cells, row by row.
fn cell_blocks(t: &Table) -> impl DoubleEndedIterator<Item = &Block> {
t.rows.iter().flat_map(|r| r.cells.iter()).flat_map(|c| c.blocks.iter()).map(|b| &**b)
}
/// How many tables deep `t` goes, itself included: 1 when its cells hold no table (without
/// recursion: a pasted fragment may be deep).
fn table_height(t: &Table) -> usize {
let mut most = 1;
let mut stack: Vec<(&Block, usize)> = cell_blocks(t).map(|b| (b, 1)).collect();
while let Some((b, d)) = stack.pop() {
if let Block::Table(inner) = b {
let d = d.saturating_add(1);
most = most.max(d);
stack.extend(cell_blocks(inner).map(|cb| (cb, d)));
}
}
most
}
/// `b` as blocks nesting at most `room` tables deep: a table that would go deeper is replaced by
/// its paragraphs (tables in its cells too, document order), as the DOCX reader does.
fn fit_tables(b: Block, room: usize, out: &mut Vec<Block>) {
match b {
Block::Table(t) if room == 0 => {
let mut stack: Vec<&Block> = cell_blocks(&t).rev().collect();
while let Some(cb) = stack.pop() {
match cb {
Block::Para(p) => out.push(Block::Para(p.clone())),
Block::Table(inner) => stack.extend(cell_blocks(inner).rev()),
}
}
}
Block::Table(mut t) if table_height(&t) > room => {
for cell in t.rows.iter_mut().flat_map(|r| r.cells.iter_mut()) {
let mut kept = Vec::new();
for cb in std::mem::take(&mut cell.blocks) {
fit_tables(Arc::unwrap_or_clone(cb), room.saturating_sub(1), &mut kept);
}
cell.blocks = kept.into_iter().map(Arc::new).collect();
}
out.push(Block::Table(t));
}
b => out.push(b),
}
}
/// Nesting limit when following text boxes inside text boxes.
const MAX_BOX_DEPTH: usize = 8;
/// Most text box stories one paste may create (hostile documents can fan out).
const MAX_PASTED_BOXES: usize = 4096;
/// Most text box content (estimated bytes, see [`block_cost`]) one paste may copy: thousands of
/// boxes can show the same story, and each needs a story of its own. Blocks without text boxes
/// are shared between those stories, not copied, so they are not charged per box.
const MAX_PASTED_BOX_BYTES: usize = 32 << 20;
/// What a paste may still spend on text box stories.
struct BoxBudget {
stories: usize,
bytes: usize,
/// The source stories being copied, innermost last: a box inside one of them showing it
/// again is a cycle.
active: Vec<u32>,
/// Each source story's cost ([`block_cost`]), worked out once per paste: a story too big for
/// what is left is not walked again for every box that shows it.
costs: BTreeMap<u32, usize>,
/// Boxes left without a story because the paste's budget couldn't hold theirs.
skipped: usize,
/// A box the fragment has no story for may show this document's story of that id (a
/// fragment copied from this document without its parts). Off for another document's
/// fragment, whose ids mean nothing here.
local: bool,
}
impl BoxBudget {
fn new() -> BoxBudget {
BoxBudget { stories: MAX_PASTED_BOXES, bytes: MAX_PASTED_BOX_BYTES, active: Vec::new(), costs: BTreeMap::new(), skipped: 0, local: true }
}
}
/// Rough size of a block's content, and whether it holds text boxes (and so must be copied).
fn block_cost(b: &Block) -> (usize, bool) {
let (mut cost, mut boxes) = (std::mem::size_of::<Block>(), false);
each_para(b, 0, &mut |p| {
let items = p
.runs
.len()
.saturating_mul(std::mem::size_of::<Run>())
.saturating_add(p.objects.len().saturating_mul(std::mem::size_of::<InlineObject>()));
let objects = p.objects.iter().fold(0usize, |n, o| n.saturating_add(object_cost(o)));
cost = cost.saturating_add(p.text.len()).saturating_add(items).saturating_add(objects);
boxes |= box_ids(p).next().is_some();
});
(cost, boxes)
}
/// Rough heap bytes a clone of `o` copies beyond its own storage: its strings and lists, and its
/// group members however deep. What it shares through `Arc` (pictures' embedded objects, charts,
/// freeforms, wrap polygons, gradients) is not copied, so not counted.
fn object_cost(o: &InlineObject) -> usize {
let float = |f: &crate::para::Float| f.alt.text.len().saturating_add(f.alt.title.len());
let mut cost = 0usize;
let mut todo = vec![o];
while let Some(o) = todo.pop() {
let add = match o {
InlineObject::Image { media, alt, float: f, .. } => media.len().saturating_add(alt.len()).saturating_add(float(f)),
InlineObject::Graphic { alt, float: f, .. } => alt.len().saturating_add(float(f)),
InlineObject::Shape { float: f, extra, .. } => {
let warp = extra.warp.as_ref().map_or(0, |w| {
let adj = w.adj.iter().map(|(name, _)| std::mem::size_of::<(String, i64)>().saturating_add(name.len()));
adj.fold(w.preset.len(), usize::saturating_add)
});
float(f).saturating_add(warp)
}
InlineObject::Group { float: f, children, .. } => {
todo.extend(children.iter().map(|c| &c.obj));
children.len().saturating_mul(std::mem::size_of::<crate::para::GroupChild>()).saturating_add(float(f))
}
InlineObject::Field { instr, result, form, .. } => instr.len().saturating_add(result.len()).saturating_add(form.len()),
InlineObject::FieldStart { instr, .. } => instr.len(),
InlineObject::ControlStart { control } => std::mem::size_of::<crate::control::ContentControl>().saturating_add(serialized_len(control)),
InlineObject::NoteRef { custom, .. } => custom.len(),
InlineObject::BookmarkStart { name } | InlineObject::BookmarkEnd { name } => name.len(),
InlineObject::PermStart { group, editor, .. } => group.len().saturating_add(editor.len()),
InlineObject::Equation { linear, math, .. } => linear.len().saturating_add(math.heap_bytes()),
InlineObject::Ruby { base, ruby, lang, props, .. } => {
[base.len(), ruby.len(), lang.len(), std::mem::size_of::<CharProps>(), serialized_len(props)]
.into_iter()
.fold(0, usize::saturating_add)
}
InlineObject::Opaque { format, xml, text } => format.len().saturating_add(xml.len()).saturating_add(text.len()),
InlineObject::FieldEnd
| InlineObject::ControlEnd
| InlineObject::CommentStart { .. }
| InlineObject::CommentEnd { .. }
| InlineObject::PermEnd { .. }
| InlineObject::PositionalTab { .. } => 0,
};
cost = cost.saturating_add(add);
}
cost
}
/// Length of `v` written as JSON: at least the bytes of every string and list it owns. For the
/// nested formatting and control types, which share nothing through `Arc`.
fn serialized_len<T: Serialize>(v: &T) -> usize {
struct Count(usize);
impl std::io::Write for Count {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0 = self.0.saturating_add(b.len());
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut n = Count(0);
let _ = serde_json::to_writer(&mut n, v);
n.0
}
/// Call `f` on every paragraph of a block, in document order, however deep its tables nest. The
/// walk keeps its own stack instead of recursing, so it is complete: a text box it doesn't reach
/// is shown nowhere, which pruning relies on (#1339). `_depth` is unused (callers keep passing it).
pub(crate) fn each_para<'a>(b: &'a Block, _depth: usize, f: &mut dyn FnMut(&'a Paragraph)) {
// Most blocks are paragraphs, which need no stack.
if let Block::Para(p) = b {
return f(p);
}
let mut todo = vec![b];
while let Some(b) = todo.pop() {
match b {
Block::Para(p) => f(p),
Block::Table(t) => {
// Last first, so they come off the stack in document order.
let cells = t.rows.iter().rev().flat_map(|r| r.cells.iter().rev());
todo.extend(cells.flat_map(|c| c.blocks.iter().rev()).map(|cb| &**cb));
}
}
}
}
/// [`each_para`], changing the paragraphs.
pub(crate) fn each_para_mut(b: &mut Block, _depth: usize, f: &mut dyn FnMut(&mut Paragraph)) {
if let Block::Para(p) = b {
return f(p);
}
let mut todo = vec![b];
while let Some(b) = todo.pop() {
match b {
Block::Para(p) => f(p),
Block::Table(t) => {
let cells = t.rows.iter_mut().rev().flat_map(|r| r.cells.iter_mut().rev());
todo.extend(cells.flat_map(|c| c.blocks.iter_mut().rev()).map(Arc::make_mut));
}
}
}
}
/// Whether a paragraph anywhere in `b` (also inside tables) uses paragraph or character style
/// `id` (tables to 16 levels deep). Table styles are the engine's `table.deleteStyle`'s job.
fn uses_style(b: &Block, id: &str, depth: usize) -> bool {
let is = |s: &Option<String>| s.as_deref() == Some(id);
match b {
Block::Para(p) => is(&p.props.style) || is(&p.mark.style) || p.runs.iter().any(|r| is(&r.props.style)),
Block::Table(t) if depth < 16 => {
t.rows.iter().flat_map(|r| r.cells.iter()).flat_map(|c| c.blocks.iter()).any(|cb| uses_style(cb, id, depth + 1))
}
Block::Table(_) => false,
}
}
/// Point every paragraph and run in `b` that uses style `from` at `to`; returns how many
/// paragraphs changed.
fn restyle_block(b: &mut Block, from: &str, to: &Option<String>, depth: usize) -> usize {
let fix = |s: &mut Option<String>| {
if s.as_deref() == Some(from) {
s.clone_from(to);
true
} else {
false
}
};
match b {
Block::Para(p) => {
let mut changed = fix(&mut p.props.style);
changed |= fix(&mut p.mark.style);
for r in &mut p.runs {
changed |= fix(&mut r.props.style);
}
if changed {
p.normalize();
p.touch();
}
usize::from(changed)
}
Block::Table(t) => {
let mut n = 0;
if depth < 16 {
for cell in t.rows.iter_mut().flat_map(|r| r.cells.iter_mut()) {
for cb in &mut cell.blocks {
if uses_style(cb, from, depth + 1) {
n += restyle_block(Arc::make_mut(cb), from, to, depth + 1);
}
}
}
}
n
}
}
}
/// What [`Document::adopt_with`] renumbers in another document's content: media keys, lists,
/// note and comment stories, comment ids, text boxes (one it doesn't bring is dropped), revisions
/// (after ours, at `rev`; an id past the `revs` it brings is dropped) and styles (those kept under new ids; unstyled paragraphs take
/// `default_style` when it's given).
struct Relink<'a> {
media: &'a BTreeMap<String, String>,
nums: &'a BTreeMap<u32, u32>,
notes: &'a BTreeMap<u32, u32>,
comments: &'a BTreeMap<u32, u32>,
boxes: &'a BTreeMap<u32, Part>,
/// Added to permission range ids: ours come first.
perms: u32,
rev: u32,
revs: u32,
styles: &'a BTreeMap<String, String>,
default_style: Option<&'a str>,
}
impl Relink<'_> {
fn block(&self, b: &mut Block) {
// Its own stack, like `each_para_mut`: tables however deeply nested are relinked (#1339).
let mut todo = vec![b];
while let Some(b) = todo.pop() {
match b {
Block::Para(p) => self.para(p),
Block::Table(t) => {
self.style(&mut t.props.style);
self.shift_change(&mut t.props.fmt_change);
if let Some(c) = t.props.fmt_change.as_mut() {
self.style(&mut c.old.style);
}
for row in t.rows.iter_mut() {
self.shift_change(&mut row.props.fmt_change);
for cell in row.cells.iter_mut() {
self.shift_change(&mut cell.props.fmt_change);
todo.extend(cell.blocks.iter_mut().map(Arc::make_mut));
}
}
}
}
}
}
fn style(&self, id: &mut Option<String>) {
if let Some(new) = id.as_ref().and_then(|i| self.styles.get(i)) {
*id = Some(new.clone());
}
}
/// A paragraph's style: unstyled ones take `default_style` when it's given.
fn para_style(&self, id: &mut Option<String>) {
match (&*id, self.default_style) {
(None, Some(d)) => *id = Some(d.to_string()),
_ => self.style(id),
}
}
fn num(&self, n: &mut Option<crate::props::NumRef>) {
if let Some(n) = n.as_mut()
&& n.num != 0
{
n.num = self.nums.get(&n.num).copied().unwrap_or(0);
}
}
/// Our id for the other document's revision `r`, if it has one.
fn rev_id(&self, r: u32) -> Option<u32> {
if r < self.revs { self.rev.checked_add(r) } else { None }
}
fn shift_change<T>(&self, c: &mut Option<Box<crate::props::PropChange<T>>>) {
match (c.as_ref().and_then(|c| self.rev_id(c.rev)), c.as_mut()) {
(Some(v), Some(x)) => x.rev = v,
_ => *c = None,
}
}
fn para(&self, p: &mut Paragraph) {
self.num(&mut p.props.numbering);
self.para_style(&mut p.props.style);
for r in &mut p.runs {
self.style(&mut r.props.style);
}
self.style(&mut p.mark.style);
for c in p.runs.iter_mut().map(|r| &mut r.props).chain([&mut p.mark]) {
c.ins = c.ins.and_then(|r| self.rev_id(r));
c.del = c.del.and_then(|r| self.rev_id(r));
self.shift_change(&mut c.fmt_change);
// A tracked change's old formatting names styles and lists as the live one does.
if let Some(f) = c.fmt_change.as_mut() {
self.style(&mut f.old.style);
}
}
self.shift_change(&mut p.props.fmt_change);
if let Some(f) = p.props.fmt_change.as_mut() {
self.para_style(&mut f.old.style);
self.num(&mut f.old.numbering);
}
if let Some(s) = p.section.as_deref_mut() {
self.shift_change(&mut s.fmt_change);
}
match (p.props.num_change.as_ref().and_then(|c| self.rev_id(c.rev)), p.props.num_change.as_mut()) {
(Some(v), Some(c)) => c.rev = v,
_ => p.props.num_change = None,
}
for o in &mut p.objects {
self.object(o, 0);
}
p.touch();
}
fn object(&self, o: &mut InlineObject, depth: usize) {
// An id that names nothing here becomes one that names nothing at all.
let map = |m: &BTreeMap<u32, u32>, id: &mut u32| *id = m.get(id).copied().unwrap_or(u32::MAX);
match o {
InlineObject::Image { media, .. } => {
if let Some(k) = self.media.get(media.as_str()) {
*media = k.clone();
}
}
InlineObject::Graphic { graphic, .. } => {
let mapped = |m: &str| self.media.get(m).filter(|k| k.as_str() != m);
if graphic.items.iter().any(|i| matches!(i, crate::graphic::GraphicItem::Image { media, .. } if mapped(media).is_some())) {
for i in &mut Arc::make_mut(graphic).items {
if let crate::graphic::GraphicItem::Image { media, .. } = i
&& let Some(k) = self.media.get(media.as_str())
{
*media = k.clone();
}
}
}
}
InlineObject::Group { children, .. } if depth < MAX_BOX_DEPTH => children.iter_mut().for_each(|c| self.object(&mut c.obj, depth + 1)),
InlineObject::NoteRef { id, .. } => map(self.notes, id),
InlineObject::CommentStart { id } | InlineObject::CommentEnd { id } => map(self.comments, id),
InlineObject::PermStart { id, .. } | InlineObject::PermEnd { id } => *id = id.saturating_add(self.perms),
_ => {}
}
for story in o.text_box_slots() {
if story.is_some_and(|id| !self.boxes.contains_key(&id)) {
*story = None;
}
}
}
}
/// One past the highest permission range id in `blocks`.
fn perm_end<'a>(blocks: impl Iterator<Item = &'a Arc<Block>>) -> u32 {
let mut end = 0u32;
for b in blocks {
each_para(b, 0, &mut |p| {
for o in &p.objects {
if let InlineObject::PermStart { id, .. } | InlineObject::PermEnd { id } = o {
end = end.max(id.saturating_add(1));
}
}
});
}
end
}
/// Text box story ids of a paragraph's shapes.
fn box_ids(p: &Paragraph) -> impl Iterator<Item = u32> + '_ {
p.objects.iter().flat_map(InlineObject::text_boxes)
}
impl Document {
/// Convert notes between footnotes and endnotes: footnotes (`foot`) become endnotes and
/// endnotes (`end`) footnotes, both at once to swap them. Each note's kind changes, as do its
/// reference marks wherever they are (the body, text boxes, the note's own mark) and the
/// note styles (Footnote Text and Footnote Reference to their endnote twins and back).
/// Returns how many notes changed.
pub fn convert_notes(&mut self, foot: bool, end: bool) -> usize {
use crate::para::NoteKind;
let flips: std::collections::HashSet<u32> = self
.parts
.iter()
.filter(|(_, p)| (foot && p.kind == PartKind::Footnote) || (end && p.kind == PartKind::Endnote))
.map(|(id, _)| *id)
.collect();
if flips.is_empty() {
return 0;
}
let swap = |s: &mut Option<String>| {
let to = match s.as_deref() {
Some("FootnoteText") => "EndnoteText",
Some("EndnoteText") => "FootnoteText",
Some("FootnoteReference") => "EndnoteReference",
Some("EndnoteReference") => "FootnoteReference",
_ => return,
};
*s = Some(to.to_string());
};
let refers = |p: &Paragraph| p.objects.iter().any(|o| matches!(o, InlineObject::NoteRef { id, .. } if flips.contains(id)));
// In a paragraph: flip the converted notes' marks and their runs' reference style; in a
// converted note's own story also the paragraph style.
let convert = |p: &mut Paragraph, in_note: bool| {
if !in_note && !refers(p) {
return;
}
let offs = p.object_offsets();
let mut at = Vec::new();
for (k, o) in p.objects.iter_mut().enumerate() {
if let InlineObject::NoteRef { kind, id, .. } = o
&& flips.contains(id)
{
*kind = if *kind == NoteKind::Footnote { NoteKind::Endnote } else { NoteKind::Footnote };
at.extend(offs.get(k).copied());
}
}
// Reference offsets and run boundaries are ordered; do not restart at the first
// reference for every run, including runs after the last reference.
let mut at = at.into_iter().peekable();
let mut start = 0usize;
for r in &mut p.runs {
let end = start.saturating_add(r.len);
while at.peek().is_some_and(|&o| o < start) {
at.next();
}
if at.peek().is_some_and(|&o| o < end) {
swap(&mut r.props.style);
}
start = end;
}
if in_note {
swap(&mut p.props.style);
swap(&mut p.mark.style);
}
p.touch();
};
let touches = |b: &Block| {
let mut any = false;
each_para(b, 0, &mut |p| any |= refers(p));
any
};
for b in self.body.iter_mut() {
if touches(b) {
each_para_mut(Arc::make_mut(b), 0, &mut |p| convert(p, false));
}
}
for (id, part) in self.parts.iter_mut() {
let own = flips.contains(id);
if own {
part.kind = if part.kind == PartKind::Footnote { PartKind::Endnote } else { PartKind::Footnote };
}
for b in part.blocks.iter_mut() {
if own || touches(b) {
each_para_mut(Arc::make_mut(b), 0, &mut |p| convert(p, own));
}
}
}
flips.len()
}
/// Point every paragraph and run that uses paragraph or character style `from` at `to`
/// (`None`: the default style), in every story, tables included. Returns how many paragraphs
/// changed. (Tables' own table styles are retargeted by the engine's table style deletion.)
pub fn restyle(&mut self, from: &str, to: Option<String>) -> usize {
let stories: Vec<crate::StoryRef> =
std::iter::once(crate::StoryRef::Body).chain(self.parts.keys().map(|k| crate::StoryRef::Part(*k))).collect();
let mut n = 0;
for s in stories {
let Ok(bl) = self.story_mut(s) else { continue };
for b in bl.iter_mut() {
if uses_style(b, from, 0) {
n += restyle_block(Arc::make_mut(b), from, &to, 0);
}
}
}
n
}
/// Insert plain text at `pos` (`\n` inside `s` is a manual line break; use
/// [`Document::split_paragraph`] for paragraph breaks). Returns the position after it.
pub fn insert_text(&mut self, pos: &Pos, s: &str, props: &CharProps) -> Result<Pos> {
let p = self.para_mut(pos.story, &pos.path)?;
let n = p.insert_text(pos.off, s, props)?;
Ok(Pos { off: pos.off + n, ..pos.clone() })
}
/// Insert an inline object at `pos`; returns the position after it.
pub fn insert_object(&mut self, pos: &Pos, obj: InlineObject, props: &CharProps) -> Result<Pos> {
let p = self.para_mut(pos.story, &pos.path)?;
p.insert_object(pos.off, obj, props)?;
Ok(Pos { off: pos.off + crate::para::OBJ.len_utf8(), ..pos.clone() })
}
/// Split the paragraph at `pos`; returns the start of the new (second) paragraph.
pub fn split_paragraph(&mut self, pos: &Pos) -> Result<Pos> {
let i = pos.path.last();
let tail = self.para_mut(pos.story, &pos.path)?.split_off(pos.off)?;
let bl = self.container_mut(pos.story, &pos.path)?;
bl.insert((i + 1).min(bl.len()), para_block(tail));
Ok(Pos { story: pos.story, path: pos.path.with_last(i + 1), off: 0 })
}
/// Insert a block before the block at `path` (same container). A table that would nest
/// deeper than [`MAX_TABLE_DEPTH`] is refused.
pub fn insert_block(&mut self, story: crate::StoryRef, path: &Path, block: Block) -> Result<()> {
if let Block::Table(t) = &block
&& path.depth().saturating_add(table_height(t)) > MAX_TABLE_DEPTH
{
return Err(DocError::Invalid(format!("tables can't nest more than {MAX_TABLE_DEPTH} deep")));
}
let i = path.last();
let bl = self.container_mut(story, path)?;
if i > bl.len() {
return Err(DocError::BadPath(path.to_string()));
}
bl.insert(i, Arc::new(block));
Ok(())
}
/// Remove the block at `path` (keeps at least one paragraph in the container).
pub fn remove_block(&mut self, story: crate::StoryRef, path: &Path) -> Result<Block> {
let i = path.last();
let bl = self.container_mut(story, path)?;
if i >= bl.len() {
return Err(DocError::BadPath(path.to_string()));
}
let b = bl.remove(i);
if bl.is_empty() || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
bl.push(para_block(Paragraph::new()));
}
Ok(Arc::try_unwrap(b).unwrap_or_else(|a| (*a).clone()))
}
/// Paragraph paths between `a` and `b` inclusive (same story), document order.
pub fn paths_between(&self, a: &Pos, b: &Pos) -> Vec<Path> {
let (a, b) = order(a, b);
if a.story != b.story {
return Vec::new();
}
// Only the blocks the range reaches are walked (not every paragraph of the story).
let mut out = Vec::new();
if let Some(bl) = self.story(a.story) {
crate::walk_between(bl, &mut Vec::new(), &mut out, 0, (&a.path.0, &b.path.0));
}
out
}
/// Delete `a..b`. Returns the position where the deletion collapsed to. On error the document
/// is unchanged: both ends are checked before anything is deleted.
pub fn delete_range(&mut self, a: &Pos, b: &Pos) -> Result<Pos> {
let (a, b) = order(a, b);
if a == b {
return Ok(a);
}
if a.story != b.story {
return Err(DocError::Invalid("range spans stories".into()));
}
// Check both ends before the first edit, so an error never leaves the document half-deleted.
for p in [&a, &b] {
match self.para(p.story, &p.path) {
Some(para) if para.clamp(p.off) == p.off => {}
Some(_) => return Err(DocError::BadOffset(p.off)),
None => return Err(DocError::BadPath(p.path.to_string())),
}
}
// Content control markers whose partner is outside the range stay (the control is
// emptied, not broken).
let mut kept = self.kept_control_markers(&a, &b);
// The walk below groups `kept` by paragraph (`chunk_by`) and looks offsets up
// (`binary_search`), so it must be sorted; `kept_control_markers` returns it so, and this
// keeps a change there from quietly losing markers.
if !kept.is_sorted() {
kept.sort();
}
let kept_in = |path: &Path| -> Vec<usize> { kept.iter().filter(|(p, _)| p == path).map(|(_, o)| *o).collect() };
if a.path == b.path {
self.para_mut(a.story, &a.path)?.delete_except(a.off, b.off, &kept_in(&a.path))?;
return Ok(a);
}
if a.path.parent() == b.path.parent() {
// Same container: trim the ends, drop the blocks between, join.
let (ia, ib) = (a.path.last(), b.path.last());
// Kept markers of the paragraphs in between move to the join.
let mut moved: Vec<(InlineObject, CharProps)> = Vec::new();
// `kept` is sorted: one walk of each paragraph's objects and runs (#3614).
for group in kept.chunk_by(|x, y| x.0 == y.0) {
let Some((path, _)) = group.first() else { continue };
if *path == a.path || *path == b.path {
continue;
}
let Some(p) = self.para(a.story, path) else { continue };
let mut runs = p.run_ranges().peekable();
for (off, o) in p.objects_with_offsets() {
if group.binary_search_by_key(&off, |(_, k)| *k).is_err() {
continue;
}
while runs.next_if(|(r, _)| r.end <= off).is_some() {}
let props = runs.peek().filter(|(r, _)| r.contains(&off)).map_or(&p.mark, |(_, c)| *c);
moved.push((o.clone(), props.clone()));
}
}
{
let pa = self.para_mut(a.story, &a.path)?;
let end = pa.len();
pa.delete_except(a.off, end, &kept_in(&a.path))?;
for (o, props) in moved {
let at = pa.len();
pa.insert_object(at, o, &props)?;
}
}
let mut tail = {
let pb = self.para_mut(b.story, &b.path)?;
pb.delete_except(0, b.off, &kept_in(&b.path))?;
pb.clone()
};
let bl = self.container_mut(a.story, &a.path)?;
// Section breaks on the marks in between go with their paragraphs.
let between: Vec<_> = bl
.get(ia + 1..ib)
.unwrap_or_default()
.iter()
.filter_map(|b| match &**b {
Block::Para(p) => p.section.clone(),
Block::Table(_) => None,
})
.collect();
if ib < bl.len() && ia < ib {
bl.drain(ia + 1..=ib);
}
tail.props = ParaProps::default();
// The deleted marks include `a`'s, so a section break there goes too: the joined
// paragraph ends where `b`'s mark did, and its text takes the following section's
// setup, as in Word (#2127).
let sect = tail.section.take();
let pa = self.para_mut(a.story, &a.path)?;
let keep_props = pa.props.clone();
pa.append(tail);
pa.props = keep_props;
let before = std::mem::replace(&mut pa.section, sect);
if a.story == crate::StoryRef::Body && a.path.0.len() == 1 {
// Nearest first, as "link to previous" resolved them before the delete.
let following = self.section_mut(a.path.last());
for s in between.iter().rev().chain(before.iter()) {
following.absorb(s);
}
}
return Ok(a);
}
// Different containers (range enters or leaves a table): clear text in every paragraph
// inside, then remove the tables it covers whole (below).
// Whether `b` is at its paragraph's end, before that paragraph is trimmed.
let b_at_end = self.para(b.story, &b.path).is_some_and(|p| p.len() == b.off);
let paths = self.paths_between(&a, &b);
for p in paths.iter().rev() {
let (from, to) = {
let Some(para) = self.para(a.story, p) else { continue };
let from = if *p == a.path { a.off } else { 0 };
let to = if *p == b.path { b.off } else { para.len() };
(from, to)
};
self.para_mut(a.story, p)?.delete_except(from, to, &kept_in(p))?;
}
// Tables the range covers whole, in the innermost container holding both ends, go (#1198,
// #1844): those strictly between the ends, the one `a` is in when it starts at that
// table's very start, and the one `b` is in when it ends at that table's very end. A range
// that is one table and nothing else only empties it (Word's Delete keeps the table).
let common = a.path.0.iter().zip(&b.path.0).take_while(|(x, y)| x == y).count();
let level = common - common % 3;
let (Some(&ia), Some(&ib)) = (a.path.0.get(level), b.path.0.get(level)) else { return Ok(a) };
let (ia, ib) = (ia as usize, ib as usize);
if ia >= ib {
return Ok(a);
}
let prefix = a.path.0.get(..level).unwrap_or_default().to_vec();
let block_path = |i: usize| {
let mut v = prefix.clone();
v.push(i as u32);
Path(v)
};
// Paragraphs inside the block at index `i` of the container, in document order.
let paths = self.para_paths(a.story);
let inside = |i: usize| -> Vec<&Path> {
let pre = block_path(i);
paths.iter().filter(|q| q.0.len() > pre.0.len() && q.0.starts_with(&pre.0)).collect()
};
// A table with cells deeper than the paragraph walk goes has paragraphs `inside` never
// lists, so where it starts or ends isn't known: it is never taken as covered.
let walked = |i: usize| self.container(a.story, &block_path(i)).and_then(|bl| bl.get(i)).is_some_and(|b| !deeper_than_walk(b, level / 3));
let a_covers = a.path.0.len() > level + 1 && a.off == 0 && walked(ia) && inside(ia).first() == Some(&&a.path);
let b_covers = b.path.0.len() > level + 1 && walked(ib) && inside(ib).last() == Some(&&b.path) && b_at_end;
let first = if a_covers { ia } else { ia + 1 };
let last = if b_covers { ib } else { ib - 1 };
let mut removed = 0;
{
let bl = self.container_mut(a.story, &block_path(ia))?;
for i in (first..=last.min(bl.len().saturating_sub(1))).rev() {
if matches!(bl.get(i).map(|b| &**b), Some(Block::Table(_))) {
bl.remove(i);
removed += 1;
}
}
if removed == 0 {
return Ok(a);
}
// A story or cell always ends with a paragraph.
if bl.is_empty() || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
bl.push(para_block(Paragraph::new()));
}
}
// The paragraphs left between the ends join, as in a range within one container.
let at = if a_covers { Pos { story: a.story, path: block_path(ia), off: 0 } } else { a.clone() };
let end = if b_covers {
Pos { story: a.story, path: block_path((ib + 1).saturating_sub(removed)), off: 0 }
} else {
Pos { story: a.story, path: block_path(ib.saturating_sub(removed)), off: 0 }
};
let paras = |p: &Pos| p.path.0.len() == level + 1 && self.para(p.story, &p.path).is_some();
if paras(&at) && paras(&end) && at.path <= end.path {
return self.delete_range(&at, &end);
}
if self.para(at.story, &at.path).is_some() {
return Ok(at);
}
Ok(self.clamp(&at))
}
/// Apply `f` to the character formatting of `a..b`. A collapsed range changes nothing (the
/// engine keeps "pending" formatting for the caret instead).
pub fn format_range(&mut self, a: &Pos, b: &Pos, f: &dyn Fn(&mut CharProps)) -> Result<()> {
let (a, b) = order(a, b);
for p in self.paths_between(&a, &b) {
let para = self.para_mut(a.story, &p)?;
let from = if p == a.path { a.off } else { 0 };
let to = if p == b.path { b.off } else { para.len() };
para.format(from, to, f)?;
// A fully selected paragraph also gets the mark formatted (Word does), but a hyperlink
// ends before the mark: a linked mark would carry the link into every paragraph Enter
// makes from it (issue #413).
if to == para.len() && (from == 0 || p != a.path) {
let style = para.mark.style.clone();
f(&mut para.mark);
if para.mark.link.take().is_some() {
para.mark.style = style.filter(|s| s != "Hyperlink");
}
}
}
Ok(())
}
/// The paragraphs paragraph formatting of `a..b` applies to: [`Self::paths_between`] without
/// the last paragraph when a non-empty range ends at its start (Select Paragraph ends there
/// to include the paragraph mark). A caret applies to its own paragraph (Word, #418).
pub fn para_paths_in(&self, a: &Pos, b: &Pos) -> Vec<Path> {
let (a, b) = order(a, b);
let mut paths = self.paths_between(&a, &b);
if b.off == 0 && a.path != b.path && paths.len() > 1 && paths.last() == Some(&b.path) {
paths.pop();
}
paths
}
/// Apply `f` to the paragraph properties of every paragraph `a..b` covers ([`Self::para_paths_in`]).
pub fn format_paragraphs(&mut self, a: &Pos, b: &Pos, f: &dyn Fn(&mut ParaProps)) -> Result<()> {
for p in self.para_paths_in(a, b) {
let para = self.para_mut(a.story, &p)?;
f(&mut para.props);
para.touch();
}
Ok(())
}
/// Copy `a..b` into a fragment (with copies of the text boxes in it).
pub fn copy_range(&self, a: &Pos, b: &Pos) -> Fragment {
let mut blocks = self.copy_blocks(a, b);
// Half a content control (one marker without the other) isn't copied.
crate::control::balance_blocks(&mut blocks);
let parts = self.text_box_parts(&blocks);
Fragment { blocks, parts }
}
/// A copy of the block at `path` (a whole table, say) as a fragment, with its text boxes.
pub fn copy_block(&self, story: crate::StoryRef, path: &Path) -> Option<Fragment> {
Some(self.fragment_of(vec![self.block(story, path)?.clone()]))
}
/// A copy of rows `rows` of the table at `tp`, with the cells of each that reach into grid
/// columns `cols` (see [`crate::Table::slice`]), as a fragment holding that table.
pub fn copy_table_cells(&self, story: crate::StoryRef, tp: &Path, rows: (usize, usize), cols: (usize, usize)) -> Option<Fragment> {
let t = self.table(story, tp)?.slice(rows, cols);
if t.rows.is_empty() {
return None;
}
Some(self.fragment_of(vec![Block::Table(t)]))
}
/// `blocks` as a fragment: half content controls dropped, text boxes copied along.
fn fragment_of(&self, mut blocks: Vec<Block>) -> Fragment {
crate::control::balance_blocks(&mut blocks);
let parts = self.text_box_parts(&blocks);
Fragment { blocks, parts }
}
/// The table whose whole text `a..b` covers when the range runs across its cells: from the
/// start of its first paragraph to the end of its last. Of the tables holding both ends, the
/// outermost one covered whole (a table nested in a cell when the range covers just it).
/// `None` for a range inside one cell, or one that doesn't cover a whole table.
pub fn whole_table(&self, a: &Pos, b: &Pos) -> Option<Path> {
let (a, b) = order(a, b);
if a.story != b.story || a.off != 0 || a.path.parent() == b.path.parent() {
return None;
}
let mut paths: Option<Vec<Path>> = None;
// Table block paths are the prefixes of a cell paragraph's path one block deep, then
// every three steps (block, row, cell) deeper.
let mut n = 1usize;
while n.saturating_add(3) <= a.path.0.len() {
let pre = a.path.0.get(..n)?;
if !b.path.0.starts_with(pre) {
return None;
}
let tp = Path(pre.to_vec());
if self.table(a.story, &tp).is_some() {
let paths = paths.get_or_insert_with(|| self.para_paths(a.story));
let mut inside = paths.iter().filter(|p| p.0.starts_with(pre));
if let Some(first) = inside.next() {
let last = inside.next_back().unwrap_or(first);
let end = self.para(a.story, last).map_or(0, Paragraph::len);
if *first == a.path && *last == b.path && b.off >= end {
return Some(tp);
}
}
}
n = n.saturating_add(3);
}
None
}
/// The whole body as a fragment (with its text boxes), to insert into another document.
pub fn body_fragment(&self) -> Fragment {
let blocks: Vec<Block> = self.body.iter().map(|b| (**b).clone()).collect();
let parts = self.text_box_parts(&blocks);
Fragment { blocks, parts }
}
/// Another document's body as a fragment of this one, to insert: the media, lists, styles,
/// notes, comments and tracked changes it refers to come along (lists, notes, comments and
/// revisions renumbered so they can't collide with ours; a style we already have by id stays
/// ours), and its text boxes ride in [`Fragment::parts`].
pub fn adopt(&mut self, src: Document) -> Fragment {
self.adopt_with(src, false)
}
/// [`Self::adopt`]; with `keep_styles` its content keeps its own formatting (Word's
/// `w:matchSrc`): a style whose id we already have comes along under a new id, and its
/// document defaults go into its base styles, which its unstyled paragraphs then use.
pub fn adopt_with(&mut self, src: Document, keep_styles: bool) -> Fragment {
self.adopt_in(src, keep_styles, &mut 0)
}
/// [`Self::adopt_with`], its permission range ids from at least `perms` on (and past ours);
/// `perms` moves past them.
fn adopt_in(&mut self, mut src: Document, keep_styles: bool, perms: &mut u32) -> Fragment {
let mut renamed = BTreeMap::new();
let mut default_style = None;
if keep_styles {
if src.styles.get("Normal").is_none() {
src.styles.upsert(crate::styles::Style { id: "Normal".into(), name: "Normal".into(), ..Default::default() });
}
// Ids given out here are taken too: two styles never get the same new one.
let mut given = std::collections::HashSet::new();
let room = src.styles.styles.len().saturating_mul(2).saturating_add(self.styles.styles.len()).saturating_add(1);
for st in &src.styles.styles {
if self.styles.get(&st.id).is_some() {
let fresh = (1..=room)
.map(|n| format!("{}{n}", st.id))
.find(|id| self.styles.get(id).is_none() && src.styles.get(id).is_none() && !given.contains(id));
if let Some(fresh) = fresh {
given.insert(fresh.clone());
renamed.insert(st.id.clone(), fresh);
}
}
}
default_style = Some(renamed.get("Normal").cloned().unwrap_or_else(|| "Normal".into()));
}
let restyle = |id: &mut Option<String>| {
if let Some(new) = id.as_ref().and_then(|i| renamed.get(i)) {
*id = Some(new.clone());
}
};
let mut media = BTreeMap::new();
for (key, data) in &src.media {
let ext = key.rsplit_once('.').map(|(_, e)| e).unwrap_or("png");
media.insert(key.clone(), self.add_media(data.to_vec(), ext));
}
let next_abs = self.numbering.abstracts.iter().map(|a| a.id.saturating_add(1)).max().unwrap_or(0);
let mut abs_map = BTreeMap::new();
for (i, a) in src.numbering.abstracts.iter().enumerate() {
let id = next_abs.saturating_add(i as u32);
abs_map.insert(a.id, id);
let mut a = crate::numbering::AbstractNum { id, ..a.clone() };
a.levels.iter_mut().for_each(|l| restyle(&mut l.style));
self.numbering.abstracts.push(a);
}
let next_num = self.numbering.nums.iter().map(|n| n.id.saturating_add(1)).max().unwrap_or(1).max(1);
let mut nums = BTreeMap::new();
for (i, n) in src.numbering.nums.iter().enumerate() {
let id = next_num.saturating_add(i as u32);
nums.insert(n.id, id);
let abstract_id = abs_map.get(&n.abstract_id).copied().unwrap_or(n.abstract_id);
let mut n = crate::numbering::Num { id, abstract_id, ..n.clone() };
n.level_overrides.iter_mut().for_each(|(_, l)| restyle(&mut l.style));
self.numbering.nums.push(n);
}
let renumber = |n: &mut Option<crate::props::NumRef>| {
if let Some(n) = n.as_mut()
&& n.num != 0
{
n.num = nums.get(&n.num).copied().unwrap_or(0);
}
};
// What a style in the other document doesn't set comes from its defaults there, and
// Word's own (no space, single lines) where those don't say.
let mut para_defaults = src.styles.default_para.clone();
para_defaults.space_before.get_or_insert(0.0);
para_defaults.space_after.get_or_insert(0.0);
para_defaults.line_spacing.get_or_insert(crate::props::LineSpacing::Multiple(1.0));
for st in &src.styles.styles {
let mut st = st.clone();
// Our defaults stay ours.
st.default = false;
if keep_styles {
if let Some(new) = renamed.get(&st.id) {
st.name = format!("{} ({new})", st.name);
st.id = new.clone();
}
restyle(&mut st.based_on);
restyle(&mut st.next);
restyle(&mut st.linked);
// A character style's runs get the defaults through their paragraph's style.
if st.based_on.is_none() && st.kind != crate::styles::StyleKind::Character {
let mut chr = src.styles.default_chr.clone();
chr.overlay(&st.chr);
st.chr = chr;
let mut para = para_defaults.clone();
para.overlay(&st.para);
st.para = para;
}
}
if self.styles.get(&st.id).is_none() {
renumber(&mut st.para.numbering);
self.styles.upsert(st);
}
}
let rev = u32::try_from(self.revisions.len()).unwrap_or(u32::MAX);
self.revisions.extend(src.revisions.iter().cloned());
// Notes and comments get stories (and comments ids) of their own here; their text is
// relinked below, once every new id is known.
let mut notes = BTreeMap::new();
for (id, part) in src.parts.iter().filter(|(_, p)| matches!(p.kind, PartKind::Footnote | PartKind::Endnote)) {
notes.insert(*id, self.add_part(part.kind, Vec::new()));
}
let mut comments = BTreeMap::new();
let mut next_comment = self.comments.keys().next_back().map_or(0, |k| k.saturating_add(1));
for (id, c) in &src.comments {
if src.parts.get(&c.part).is_some_and(|p| p.kind == PartKind::Comment) {
notes.insert(c.part, self.add_part(PartKind::Comment, Vec::new()));
comments.insert(*id, next_comment);
next_comment = next_comment.saturating_add(1);
}
}
let boxes: BTreeMap<u32, Part> = src.parts.iter().filter(|(_, p)| p.kind == PartKind::TextBox).map(|(k, p)| (*k, p.clone())).collect();
// Its permission ranges are numbered after ours, so no end of theirs closes one of ours.
let first_perm = perm_end(self.body.iter().chain(self.parts.values().flat_map(|p| p.blocks.iter()))).max(*perms);
*perms = first_perm.saturating_add(perm_end(src.body.iter().chain(src.parts.values().flat_map(|p| p.blocks.iter()))));
let link = Relink {
media: &media,
nums: &nums,
notes: &notes,
comments: &comments,
boxes: &boxes,
perms: first_perm,
rev,
revs: u32::try_from(src.revisions.len()).unwrap_or(u32::MAX),
styles: &renamed,
default_style: default_style.as_deref(),
};
let mut parts = boxes.clone();
for part in parts.values_mut() {
part.blocks.iter_mut().for_each(|b| link.block(Arc::make_mut(b)));
}
// A note's or comment's text boxes get stories here now; the body's go with the fragment.
let mut budget = BoxBudget::new();
for (old, new) in &notes {
let Some(part) = src.parts.get(old) else { continue };
let mut blocks: Vec<Block> = part.blocks.iter().map(|b| (**b).clone()).collect();
blocks.iter_mut().for_each(|b| link.block(b));
self.adopt_text_boxes(&mut blocks, &parts, 0, &mut budget);
if let Some(p) = self.parts.get_mut(new) {
p.blocks = blocks.into_iter().map(Arc::new).collect();
}
}
for (id, c) in &src.comments {
if let (Some(new), Some(part)) = (comments.get(id), notes.get(&c.part)) {
let parent = c.parent.and_then(|p| comments.get(&p).copied());
// Its durable id is unique in its own document only: the writer gives the copy one.
self.comments.insert(*new, crate::Comment { parent, part: *part, durable_id: None, ..c.clone() });
}
}
let mut blocks: Vec<Block> = src.body.iter().map(|b| (**b).clone()).collect();
blocks.iter_mut().for_each(|b| link.block(b));
Fragment { blocks, parts }
}
/// [`Self::adopt_with`] as blocks to place in a story directly (a reader building one): its
/// text boxes already have stories of their own here. `perms` is the first permission range
/// id the reader hasn't given out (ranges it holds aren't in this document yet); it moves past
/// the adopted ones.
pub fn adopt_blocks(&mut self, src: Document, keep_styles: bool, perms: &mut u32) -> Vec<Block> {
let Fragment { mut blocks, parts } = self.adopt_in(src, keep_styles, perms);
let mut budget = BoxBudget::new();
self.adopt_text_boxes(&mut blocks, &parts, 0, &mut budget);
blocks
}
/// Copies of the text box stories shapes in `blocks` show, nested boxes included.
fn text_box_parts(&self, blocks: &[Block]) -> BTreeMap<u32, Part> {
let mut out = BTreeMap::new();
let mut todo = Vec::new();
for b in blocks {
each_para(b, 0, &mut |p| todo.extend(box_ids(p)));
}
while let Some(id) = todo.pop() {
if out.contains_key(&id) {
continue;
}
let Some(part) = self.parts.get(&id).filter(|p| p.kind == PartKind::TextBox) else { continue };
for b in &part.blocks {
each_para(b, 0, &mut |p| todo.extend(box_ids(p)));
}
out.insert(id, part.clone());
}
out
}
fn copy_blocks(&self, a: &Pos, b: &Pos) -> Vec<Block> {
let (a, b) = order(a, b);
let mut out = Vec::new();
if a.story != b.story {
return out;
}
if a.path.parent() == b.path.parent() {
let Some(bl) = self.container(a.story, &a.path) else { return out };
for i in a.path.last()..=b.path.last() {
let Some(blk) = bl.get(i) else { break };
match &**blk {
Block::Para(p) => {
let mut p = p.clone();
let to = if i == b.path.last() { b.off } else { p.len() };
let from = if i == a.path.last() { a.off } else { 0 };
let _ = p.delete(to.min(p.len()), p.len());
let _ = p.delete(0, from.min(p.len()));
if i == b.path.last() && to < self.para(a.story, &b.path).map(Paragraph::len).unwrap_or(0) {
p.section = None;
}
out.push(Block::Para(p));
}
Block::Table(t) => out.push(Block::Table(t.clone())),
}
}
return out;
}
// Across the cells of a whole table: the table itself, not its cells' paragraphs (#1744).
if let Some(tp) = self.whole_table(&a, &b)
&& let Some(blk) = self.block(a.story, &tp)
{
out.push(blk.clone());
return out;
}
for p in self.paths_between(&a, &b) {
if let Some(para) = self.para(a.story, &p) {
let mut q = para.clone();
let to = if p == b.path { b.off } else { q.len() };
let from = if p == a.path { a.off } else { 0 };
let _ = q.delete(to.min(q.len()), q.len());
let _ = q.delete(0, from.min(q.len()));
q.section = None;
out.push(Block::Para(q));
}
}
out
}
/// Give every text box in `blocks` a new story of its own, copied from `parts` (or, for a
/// fragment without them, from this document's text box of that id). A box whose story can't
/// be found keeps no story rather than showing an unrelated one.
fn adopt_text_boxes(&mut self, blocks: &mut [Block], parts: &BTreeMap<u32, Part>, depth: usize, budget: &mut BoxBudget) {
let mut any = false;
for b in blocks.iter() {
each_para(b, 0, &mut |p| any |= box_ids(p).next().is_some());
}
if !any {
return;
}
for b in blocks.iter_mut() {
each_para_mut(b, 0, &mut |p| {
let mut changed = false;
for o in p.objects.iter_mut() {
for story in o.text_box_slots() {
if let Some(old) = *story {
*story = self.adopt_text_box(old, parts, depth, budget);
changed = true;
}
}
}
if changed {
p.touch();
}
});
}
}
/// A new text box story copied from `old`; its own boxes get new stories too. Blocks without
/// text boxes are shared with the source (copy-on-write), the others copied. A box showing a
/// story it is inside, or whose story the paste's budget can't hold, gets none (CWE-400);
/// only that box: a later box with a smaller story is still copied while the budget lasts.
fn adopt_text_box(&mut self, old: u32, parts: &BTreeMap<u32, Part>, depth: usize, budget: &mut BoxBudget) -> Option<u32> {
if depth >= MAX_BOX_DEPTH || budget.active.contains(&old) {
return None;
}
if budget.stories == 0 {
budget.skipped = budget.skipped.saturating_add(1);
return None;
}
let src = parts.get(&old).or_else(|| self.parts.get(&old).filter(|p| budget.local && p.kind == PartKind::TextBox))?;
// Charge what the copy really allocates before making any of it: the block slots, and the
// blocks holding text boxes (deep-copied below). The rest are shared `Arc`s, however many
// boxes show them, so they cost nothing more. Worked out once per paste.
let cost = *budget.costs.entry(old).or_insert_with(|| {
let slots = src.blocks.len().saturating_mul(std::mem::size_of::<Arc<Block>>());
src.blocks.iter().fold(slots, |a, b| match block_cost(b) {
(c, true) => a.saturating_add(c),
(_, false) => a,
})
});
let Some(left) = budget.bytes.checked_sub(cost) else {
budget.skipped = budget.skipped.saturating_add(1);
return None;
};
budget.bytes = left;
budget.stories -= 1;
let src_blocks = src.blocks.clone();
// The copy keeps its text direction and alignment, not its link (a copy stands alone).
let body = crate::TextBody { next: None, ..src.body };
budget.active.push(old);
let mut blocks: Blocks = Vec::with_capacity(src_blocks.len());
for b in src_blocks {
let mut boxes = false;
each_para(&b, 0, &mut |p| boxes |= box_ids(p).next().is_some());
if boxes {
let mut copy = (*b).clone();
self.adopt_text_boxes(std::slice::from_mut(&mut copy), parts, depth + 1, budget);
blocks.push(Arc::new(copy));
} else {
blocks.push(b);
}
}
budget.active.pop();
let id = self.try_add_part(PartKind::TextBox, blocks).ok()?;
if let Some(p) = self.parts.get_mut(&id) {
p.body = body;
}
Some(id)
}
/// The blocks of `frag`, a fragment of another document, with every text box given a new
/// story of its own here copied from the fragment's parts (as a paste does), to place them
/// in this document; a box the fragment has no story for keeps none.
pub fn adopt_fragment(&mut self, frag: &Fragment) -> Vec<Block> {
let mut blocks = frag.blocks.clone();
let mut budget = BoxBudget { local: false, ..BoxBudget::new() };
self.adopt_text_boxes(&mut blocks, &frag.parts, 0, &mut budget);
if budget.skipped > 0 {
log::warn!(
"{} text boxes are left empty; their stories are past what one copy brings ({MAX_PASTED_BOXES} stories, {MAX_PASTED_BOX_BYTES} bytes)",
budget.skipped
);
}
blocks
}
/// Insert a fragment at `pos`; returns the position after the pasted content.
pub fn insert_fragment(&mut self, pos: &Pos, frag: &Fragment) -> Result<Pos> {
// Tables that would nest deeper than the limit here become their paragraphs (#1596).
let room = MAX_TABLE_DEPTH.saturating_sub(pos.path.depth());
let mut blocks = Vec::with_capacity(frag.blocks.len());
for b in frag.blocks.iter().cloned() {
fit_tables(b, room, &mut blocks);
}
if blocks.is_empty() {
return Ok(pos.clone());
}
if self.para_at(pos).is_none() {
return Err(DocError::BadPath(pos.path.to_string()));
}
// Pasted text boxes get stories of their own (never shared with the copy's source).
let mut budget = BoxBudget::new();
self.adopt_text_boxes(&mut blocks, &frag.parts, 0, &mut budget);
if budget.skipped > 0 {
// Once per paste, not once per box (review of #3874).
log::warn!(
"paste: {} text boxes are left empty; their stories are past what one paste copies ({MAX_PASTED_BOXES} stories, {MAX_PASTED_BOX_BYTES} bytes)",
budget.skipped
);
}
// Single paragraph: inline insert keeping runs.
if blocks.len() == 1
&& let Some(Block::Para(src)) = blocks.first()
{
let src = src.clone();
let target = self.para_mut(pos.story, &pos.path)?;
let tail = target.split_off(pos.off)?;
let added = src.len();
let keep = target.props.clone();
let sect = tail.section.clone();
let mut src = src;
src.section = None;
target.append(src);
target.append(tail);
target.props = keep;
target.section = sect;
return Ok(Pos { off: pos.off + added, ..pos.clone() });
}
// Multi-block: split, merge first into left part, last into right part.
let after = self.split_paragraph(pos)?;
let left = pos.path.clone();
let first = blocks.remove(0);
let last = blocks.pop();
let mut idx = left.last() + 1;
match first {
Block::Para(p) => {
let l = self.para_mut(pos.story, &left)?;
let keep = l.props.clone();
let mut p2 = p;
let pprops = p2.props.clone();
p2.section = None;
l.append(p2);
l.props = if keep.is_empty() { pprops } else { keep };
}
t @ Block::Table(_) => {
self.insert_block(pos.story, &left.with_last(idx), t)?;
idx += 1;
}
}
for b in blocks {
self.insert_block(pos.story, &left.with_last(idx), b)?;
idx += 1;
}
let right = after.path.with_last(idx);
match last {
Some(Block::Para(p)) => {
let r = self.para_mut(pos.story, &right)?;
let tail = std::mem::replace(r, p.clone());
let off = r.len();
let sect = tail.section.clone();
r.append(tail);
r.section = sect;
Ok(Pos { story: pos.story, path: right, off })
}
Some(t @ Block::Table(_)) => {
self.insert_block(pos.story, &right, t)?;
Ok(Pos { story: pos.story, path: right.with_last(idx + 1), off: 0 })
}
None => Ok(Pos { story: pos.story, path: right, off: 0 }),
}
}
/// Replace every block of a story.
pub fn set_story(&mut self, story: crate::StoryRef, blocks: Blocks) -> Result<()> {
let s = self.story_mut(story)?;
*s = blocks;
if s.is_empty() {
s.push(para_block(Paragraph::new()));
}
Ok(())
}
}
#[cfg(test)]
mod tests;