mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 11:50:20 -06:00
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>
1475 lines
66 KiB
Rust
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: ¬es,
|
|
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 ¬es {
|
|
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;
|