Merge branch 'main' of https://github.com/storytold/wordcraft into pr-5559

# Conflicts:
#	crates/formats/src/odt/import.rs
This commit is contained in:
FredApps committed 2026-11-04 15:24:27 +01:00
commit 3c7aa5df90
53 files changed
+440 -289

No files matched your search

+3 -3
View File
@@ -7,7 +7,7 @@ use super::settings::{CoreProps, CustomProp, Settings};
use super::text_walk::{TextMode, notes_and_boxes, story_word_count_of, text_of, walk};
use std::sync::Arc;
use super::{BTreeMap, CharProps, Document, InlineObject, Numbering, Paragraph, SectionProps, StyleSheet, Table, edit, numbering, para, section};
use super::{BTreeMap, CharProps, Document, InlineObject, Paragraph, SectionProps, StyleSheet, Table, edit, numbering, para, section};
/// [`Document::media`] by content, so adding media is linear in the bytes added rather than a
/// scan of every earlier image (CWE-407). Rebuilt whenever `media` changed size behind its back;
@@ -62,8 +62,8 @@ impl Document {
body: vec![para_block(Paragraph::new())],
last_section: SectionProps::default(),
parts: BTreeMap::new(),
styles: StyleSheet::builtin(),
numbering: Numbering::default(),
styles: Arc::new(StyleSheet::builtin()),
numbering: Arc::default(),
comments: BTreeMap::new(),
revisions: Vec::new(),
settings: Settings::default(),
+4 -4
View File
@@ -1054,7 +1054,7 @@ impl Document {
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() });
Arc::make_mut(&mut 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();
@@ -1089,7 +1089,7 @@ impl Document {
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);
Arc::make_mut(&mut 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();
@@ -1099,7 +1099,7 @@ impl Document {
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);
Arc::make_mut(&mut self.numbering).nums.push(n);
}
let renumber = |n: &mut Option<crate::props::NumRef>| {
if let Some(n) = n.as_mut()
@@ -1138,7 +1138,7 @@ impl Document {
}
if self.styles.get(&st.id).is_none() {
renumber(&mut st.para.numbering);
self.styles.upsert(st);
Arc::make_mut(&mut self.styles).upsert(st);
}
}
let rev = u32::try_from(self.revisions.len()).unwrap_or(u32::MAX);
+16 -16
View File
@@ -692,19 +692,19 @@ fn adopting_a_document_renumbers_what_it_refers_to() {
let mut ours = doc(&["ours"]);
let our_note = ours.add_part(PartKind::Footnote, vec![para_block(Paragraph::with_text("our note", CharProps::default()))]);
ours.comments.insert(0, Comment { part: our_note, ..Comment::default() });
let our_list = ours.numbering.add_list(crate::numbering::ListKind::Bullet);
let our_list = Arc::make_mut(&mut ours.numbering).add_list(crate::numbering::ListKind::Bullet);
ours.revisions.push(Revision { kind: RevisionKind::Insert, ..Revision::default() });
let mut theirs = doc(&["theirs"]);
let note = theirs.add_part(PartKind::Footnote, vec![para_block(Paragraph::with_text("their note", CharProps::default()))]);
let said = theirs.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text("their comment", CharProps::default()))]);
theirs.comments.insert(0, Comment { author: "Them".into(), part: said, durable_id: Some("0A0B0C0D".into()), ..Comment::default() });
let list = theirs.numbering.add_list(crate::numbering::ListKind::Bullet);
let list = Arc::make_mut(&mut theirs.numbering).add_list(crate::numbering::ListKind::Bullet);
assert_eq!(list, our_list);
theirs.revisions.push(Revision { kind: RevisionKind::Delete, ..Revision::default() });
let mut style = crate::styles::Style { id: "TheirList".into(), name: "Their List".into(), ..Default::default() };
style.para.numbering = Some(crate::props::NumRef { num: list, level: 0 });
theirs.styles.upsert(style);
Arc::make_mut(&mut theirs.styles).upsert(style);
let p = theirs.para_mut(StoryRef::Body, &Path::top(0)).unwrap();
p.props.numbering = Some(crate::props::NumRef { num: list, level: 0 });
p.runs[0].props.del = Some(0);
@@ -748,11 +748,11 @@ fn a_kept_character_style_leaves_what_it_doesnt_set_to_the_paragraph_style() {
use crate::styles::{Style, StyleKind, StyleSheet};
let mut ours = doc(&["ours"]);
let mut theirs = doc(&["theirs"]);
theirs.styles = StyleSheet::empty();
theirs.styles.default_chr = CharProps { size: Some(9.0), ..Default::default() };
theirs.styles = Arc::new(StyleSheet::empty());
Arc::make_mut(&mut theirs.styles).default_chr = CharProps { size: Some(9.0), ..Default::default() };
let big = CharProps { size: Some(20.0), ..Default::default() };
theirs.styles.upsert(Style { id: "Big".into(), name: "Big".into(), chr: big, ..Default::default() });
theirs.styles.upsert(Style { id: "Link".into(), name: "Link".into(), kind: StyleKind::Character, ..Default::default() });
Arc::make_mut(&mut theirs.styles).upsert(Style { id: "Big".into(), name: "Big".into(), chr: big, ..Default::default() });
Arc::make_mut(&mut theirs.styles).upsert(Style { id: "Link".into(), name: "Link".into(), kind: StyleKind::Character, ..Default::default() });
let p = theirs.para_mut(StoryRef::Body, &Path::top(0)).unwrap();
p.props.style = Some("Big".into());
p.runs[0].props.style = Some("Link".into());
@@ -766,15 +766,15 @@ fn a_kept_character_style_leaves_what_it_doesnt_set_to_the_paragraph_style() {
fn kept_styles_renamed_on_adoption_are_renamed_in_list_levels_too() {
use crate::styles::Style;
let mut ours = doc(&["ours"]);
ours.styles.upsert(Style { id: "Heading1".into(), name: "heading 1".into(), ..Default::default() });
Arc::make_mut(&mut ours.styles).upsert(Style { id: "Heading1".into(), name: "heading 1".into(), ..Default::default() });
let mut theirs = doc(&["theirs"]);
theirs.styles.upsert(Style { id: "Heading1".into(), name: "heading 1".into(), ..Default::default() });
let list = theirs.numbering.add_list(crate::numbering::ListKind::Numbered);
Arc::make_mut(&mut theirs.styles).upsert(Style { id: "Heading1".into(), name: "heading 1".into(), ..Default::default() });
let list = Arc::make_mut(&mut theirs.numbering).add_list(crate::numbering::ListKind::Numbered);
let abs = theirs.numbering.nums.iter().find(|n| n.id == list).map(|n| n.abstract_id).unwrap();
let a = theirs.numbering.abstracts.iter_mut().find(|a| a.id == abs).unwrap();
let a = Arc::make_mut(&mut theirs.numbering).abstracts.iter_mut().find(|a| a.id == abs).unwrap();
a.levels[0].style = Some("Heading1".into());
let level = a.levels[0].clone();
theirs.numbering.nums.iter_mut().find(|n| n.id == list).unwrap().level_overrides.push((1, level));
Arc::make_mut(&mut theirs.numbering).nums.iter_mut().find(|n| n.id == list).unwrap().level_overrides.push((1, level));
let before = ours.numbering.abstracts.len();
ours.adopt_with(theirs, true);
@@ -814,10 +814,10 @@ fn an_adopted_tracked_format_change_keeps_its_old_styles_and_list() {
use crate::{Revision, RevisionKind};
for keep in [false, true] {
let mut ours = doc(&["ours"]);
let our_list = ours.numbering.add_list(crate::numbering::ListKind::Bullet);
let our_list = Arc::make_mut(&mut ours.numbering).add_list(crate::numbering::ListKind::Bullet);
let mut theirs = doc(&["theirs"]);
theirs.revisions.push(Revision { kind: RevisionKind::Format, ..Revision::default() });
let list = theirs.numbering.add_list(crate::numbering::ListKind::Bullet);
let list = Arc::make_mut(&mut theirs.numbering).add_list(crate::numbering::ListKind::Bullet);
assert_eq!(list, our_list);
let p = theirs.para_mut(StoryRef::Body, &Path::top(0)).unwrap();
// Live and old props name the same styles (ours has them too) and their list.
@@ -845,8 +845,8 @@ fn adopted_styles_never_become_our_defaults() {
let mut ours = doc(&["ours"]);
let mut theirs = doc(&["theirs"]);
// A localized default character style, and one whose id we have too.
theirs.styles.upsert(Style { id: "Absatz".into(), kind: StyleKind::Character, default: true, ..Default::default() });
theirs.styles.upsert(Style { id: "Strong".into(), kind: StyleKind::Character, default: true, ..Default::default() });
Arc::make_mut(&mut theirs.styles).upsert(Style { id: "Absatz".into(), kind: StyleKind::Character, default: true, ..Default::default() });
Arc::make_mut(&mut theirs.styles).upsert(Style { id: "Strong".into(), kind: StyleKind::Character, default: true, ..Default::default() });
ours.adopt_with(theirs, keep);
assert!(ours.styles.styles.iter().all(|s| !s.default), "keep_styles {keep}");
}
+9 -5
View File
@@ -2,9 +2,9 @@
//!
//! A [`Document`] holds the main story (`body`) and secondary stories (`parts`: headers,
//! footers, notes, comments, text boxes). A story is a list of [`Block`]s: paragraphs and tables;
//! table cells hold block lists of their own. Blocks are `Arc`-shared, so cloning a document is
//! cheap (undo snapshots, handing a frozen copy to the renderer) and edits copy only what they
//! touch (`Arc::make_mut`).
//! table cells hold block lists of their own. Blocks, the style sheet and the list definitions
//! are `Arc`-shared, so cloning a document is cheap (undo snapshots, handing a frozen copy to the
//! renderer) and edits copy only what they touch (`Arc::make_mut`).
//!
//! Positions ([`Pos`]) name a story, a [`Path`] to a paragraph and a byte offset in it. Paths
//! compare in document order.
@@ -100,8 +100,12 @@ pub struct Document {
/// Properties of the last section (OOXML's body-level `w:sectPr`).
pub last_section: SectionProps,
pub parts: BTreeMap<u32, Part>,
pub styles: StyleSheet,
pub numbering: Numbering,
/// Shared, like the blocks: an undo step copies it only when a command changes it (a full
/// copy is kilobytes a style, so typing into a document with hundreds of styles cost megabytes
/// a step).
pub styles: Arc<StyleSheet>,
/// Shared like [`Document::styles`].
pub numbering: Arc<Numbering>,
pub comments: BTreeMap<u32, Comment>,
pub revisions: Vec<Revision>,
pub settings: Settings,
+12 -11
View File
@@ -1,4 +1,5 @@
use super::*;
use std::sync::Arc;
mod note_conversion;
@@ -35,8 +36,8 @@ fn word_count_counts_each_chinese_and_japanese_character() {
#[test]
fn word_count_includes_numbered_list_labels_but_not_bullets() {
let mut d = Document::new();
let num_id = d.numbering.add_list(ListKind::Numbered);
let bul_id = d.numbering.add_list(ListKind::Bullet);
let num_id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let bul_id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Bullet);
let mut p1 = Paragraph::with_text("Apple", CharProps::default());
p1.props.numbering = Some(props::NumRef { num: num_id, level: 0 });
@@ -60,9 +61,9 @@ fn word_count_includes_numbered_list_labels_but_not_bullets() {
#[test]
fn word_count_includes_numbered_labels_with_nothing_suffix() {
let mut d = Document::new();
let num_id = d.numbering.add_list(ListKind::Numbered);
let num_id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let aid = d.numbering.num(num_id).expect("list num exists").abstract_id;
let a = d.numbering.abstracts.iter_mut().find(|a| a.id == aid).expect("abstract num exists");
let a = Arc::make_mut(&mut d.numbering).abstracts.iter_mut().find(|a| a.id == aid).expect("abstract num exists");
a.levels[0].suffix = numbering::LevelSuffix::Nothing;
let mut p1 = Paragraph::with_text("Apple", CharProps::default());
@@ -81,9 +82,9 @@ fn word_count_includes_numbered_labels_with_nothing_suffix() {
#[test]
fn word_count_keeps_a_cjk_list_label_one_word() {
let mut d = Document::new();
let num_id = d.numbering.add_list(ListKind::Numbered);
let num_id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let aid = d.numbering.num(num_id).expect("list num exists").abstract_id;
let a = d.numbering.abstracts.iter_mut().find(|a| a.id == aid).expect("abstract num exists");
let a = Arc::make_mut(&mut d.numbering).abstracts.iter_mut().find(|a| a.id == aid).expect("abstract num exists");
a.levels[0].format = section::NumFormat::ChineseCounting;
a.levels[0].text = "第%1章".into();
a.levels[0].start = 12;
@@ -187,7 +188,7 @@ fn parts_and_lists_after_the_largest_ids_take_free_ones() {
assert_eq!(d.add_part(PartKind::Footer, Vec::new()), 3);
assert!(parts.iter().all(|(k, p)| d.parts.get(k) == Some(p)));
let n = &mut d.numbering;
let n = Arc::make_mut(&mut d.numbering);
n.add_list(ListKind::Bullet);
n.add_list(ListKind::Bullet);
n.abstracts[1].id = u32::MAX;
@@ -229,11 +230,11 @@ fn same_content_sees_every_change() {
assert!(!d.same_content(&e), "the formatting changed");
// The styles changed.
let mut e = d.clone();
e.styles.get_mut("Normal").unwrap().chr.bold = Some(true);
Arc::make_mut(&mut e.styles).get_mut("Normal").unwrap().chr.bold = Some(true);
assert!(!d.same_content(&e), "a style changed");
// The lists changed.
let mut e = d.clone();
e.numbering.add_list(ListKind::Numbered);
Arc::make_mut(&mut e.numbering).add_list(ListKind::Numbered);
assert!(!d.same_content(&e), "a list was added");
// The settings changed.
let mut e = d.clone();
@@ -452,9 +453,9 @@ fn box_para(story: u32, id: u32) -> Blocks {
fn adopting_relinks_lists_in_deeply_nested_tables() {
// As #1339: a list paragraph 20 tables down takes the list it is brought in with.
let mut ours = Document::from_text("ours");
let our_list = ours.numbering.add_list(numbering::ListKind::Bullet);
let our_list = Arc::make_mut(&mut ours.numbering).add_list(numbering::ListKind::Bullet);
let mut theirs = Document::from_text("theirs");
let list = theirs.numbering.add_list(numbering::ListKind::Bullet);
let list = Arc::make_mut(&mut theirs.numbering).add_list(numbering::ListKind::Bullet);
assert_eq!(list, our_list);
let mut p = Paragraph::with_text("item", CharProps::default());
p.props.numbering = Some(props::NumRef { num: list, level: 0 });
+4 -3
View File
@@ -25,6 +25,7 @@ mod stsh;
mod table;
use std::io::{Read, Seek};
use std::sync::Arc;
use wordcraft_doc::link::hyperlink_target;
use wordcraft_doc::para::InlineObject;
@@ -249,9 +250,9 @@ pub fn read_with_password(bytes: &[u8], password: Option<&str>) -> Result<Docume
doc.core = core;
}
doc.body.clear();
doc.styles = fmt::stylesheet(&raw_styles, &fonts, standard_fonts);
doc.numbering = list::parse(&table, &fib, &fonts);
let sheet: StyleSheet = doc.styles.clone();
doc.styles = Arc::new(fmt::stylesheet(&raw_styles, &fonts, standard_fonts));
doc.numbering = Arc::new(list::parse(&table, &fib, &fonts));
let sheet: StyleSheet = StyleSheet::clone(&doc.styles);
let sects = sections::parse(&word, &table, &fib);
let stories = sections::header_stories(&table, &fib);
// Subdocument layout in the CP space: main document, footnotes, headers, comments,
+4 -3
View File
@@ -2,6 +2,7 @@ use super::{
AbstractNum, DocxError, El, Level, LevelSuffix, Num, NumFormat, PartFormat, Reader, Style, StyleKind, StyleSheet, TablePart, TableStyleParts,
VAlign, flag, int, props, tw, u32_of,
};
use std::sync::Arc;
/// Built-in style names Word stores in lower case, and how the UI shows them.
pub(crate) const LOWER_NAMES: &[(&str, &str)] = &[
@@ -186,7 +187,7 @@ impl Reader<'_> {
if !sheet.styles.iter().any(|s| s.quick) {
sheet.add_default_gallery();
}
self.doc.styles = sheet;
self.doc.styles = Arc::new(sheet);
Ok(())
}
@@ -298,8 +299,8 @@ impl Reader<'_> {
a.levels = levels;
}
}
self.doc.numbering.abstracts = abstracts;
self.doc.numbering.nums = nums;
Arc::make_mut(&mut self.doc.numbering).abstracts = abstracts;
Arc::make_mut(&mut self.doc.numbering).nums = nums;
Ok(())
}
+2 -1
View File
@@ -7,6 +7,7 @@
//! in a right-to-left paragraph.
use std::io::Read;
use std::sync::Arc;
use wordcraft_doc::props::CharProps;
use wordcraft_doc::{Document, Paragraph, para_block};
@@ -189,6 +190,6 @@ fn a_language_the_run_already_has_is_kept() {
#[test]
fn an_inherited_bidi_language_is_not_repeated() {
let mut d = doc_of("سلام WordCraft دنیا", true, arial());
d.styles.default_chr.lang_bidi = Some("fa-IR".into());
Arc::make_mut(&mut d.styles).default_chr.lang_bidi = Some("fa-IR".into());
assert_eq!(langs(&d), vec![(true, None), (false, None), (true, None)]);
}
+5 -4
View File
@@ -2,6 +2,7 @@
//! reopen. An unset default is resolved as `DEFAULT_FONT_SIZE`; the writer must say so, because
//! the reader reads an omitted size as 10 pt.
use std::io::{Read, Write};
use std::sync::Arc;
use wordcraft_doc::Document;
use wordcraft_doc::props::{CharProps, DEFAULT_FONT_SIZE};
fn resolved(d: &Document) -> f32 {
@@ -19,7 +20,7 @@ fn defaults(xml: &str) -> &str {
#[test]
fn an_unset_default_size_keeps_its_effective_size_across_save_and_reopen() {
let mut d = Document::new();
d.styles.default_chr.size = None;
Arc::make_mut(&mut d.styles).default_chr.size = None;
assert_eq!(resolved(&d), DEFAULT_FONT_SIZE);
let bytes = wordcraft_docx::write(&d).unwrap();
assert!(defaults(&styles_xml(&bytes)).contains(r#"<w:sz w:val="22"/>"#));
@@ -33,7 +34,7 @@ fn an_unset_default_size_keeps_its_effective_size_across_save_and_reopen() {
fn an_explicit_default_size_is_written_as_it_is() {
for size in [8.0, 10.0, 11.0, 12.5, 18.0] {
let mut d = Document::new();
d.styles.default_chr.size = Some(size);
Arc::make_mut(&mut d.styles).default_chr.size = Some(size);
let bytes = wordcraft_docx::write(&d).unwrap();
assert_eq!(resolved(&wordcraft_docx::read(&bytes).unwrap()), size);
}
@@ -59,8 +60,8 @@ fn an_unset_default_size_does_not_take_a_styles_size() {
// The written default is the resolver's own fallback, not what the Normal style resolves to,
// so a style that sets a size keeps it and one that does not still falls back to the default.
let mut d = Document::new();
d.styles.default_chr.size = None;
if let Some(normal) = d.styles.get_mut("Normal") {
Arc::make_mut(&mut d.styles).default_chr.size = None;
if let Some(normal) = Arc::make_mut(&mut d.styles).get_mut("Normal") {
normal.chr.size = Some(14.0);
}
let reopened = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
+2 -1
View File
@@ -1,6 +1,7 @@
//! Hand-written minimal WordprocessingML fixtures (written for these tests from the spec).
use std::io::Write;
use std::sync::Arc;
use wordcraft_doc::fill::Fill;
use wordcraft_doc::para::{Anchor, FloatAlign, NoteKind, ShapeKind, Wrap};
@@ -252,7 +253,7 @@ fn omitted_paragraph_defaults_can_be_edited() {
format!(r#"<w:styles {W_NS}><w:style w:type="paragraph" w:styleId="Normal" w:default="1"><w:name w:val="Normal"/></w:style></w:styles>"#);
let mut d = wordcraft_docx::read(&docx("<w:p/>", &[("rId1", "styles", "styles.xml")], &[("word/styles.xml", &styles)])).unwrap();
assert!(!d.styles.default_para_present);
d.styles.default_para.space_after = Some(12.0);
Arc::make_mut(&mut d.styles).default_para.space_after = Some(12.0);
let again = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
assert!(again.styles.default_para_present);
assert_eq!(again.styles.default_para.space_after, Some(12.0));
+2 -1
View File
@@ -4,6 +4,7 @@
use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::sync::Arc;
use wordcraft_doc::{Block, BuildingBlock, CharProps, Document, Paragraph, para_block};
@@ -191,7 +192,7 @@ fn glossary_entries_are_read_into_the_model() {
fn edited_glossary_is_written_from_the_model() {
let mut doc = wordcraft_docx::read(&with_glossary()).unwrap();
doc.glossary[0].name = "Sign-off".into();
let num = doc.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let num = Arc::make_mut(&mut doc.numbering).add_list(wordcraft_doc::ListKind::Bullet);
let mut listed = Paragraph::with_text("Second line", CharProps::default());
listed.props.numbering = Some(wordcraft_doc::props::NumRef { num, level: 0 });
doc.glossary.push(BuildingBlock {
+3 -2
View File
@@ -5,6 +5,7 @@
use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::sync::Arc;
const W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
@@ -272,7 +273,7 @@ fn comment_references_use_the_documents_annotation_reference_style() {
assert!(!body.contains(r#"w:val="CommentReference""#), "{body}");
// A document without that style keeps the English id.
let mut doc = wordcraft_doc::Document::new();
doc.styles.styles.retain(|s| !s.name.eq_ignore_ascii_case("annotation reference"));
Arc::make_mut(&mut doc.styles).styles.retain(|s| !s.name.eq_ignore_ascii_case("annotation reference"));
let rebuilt = wordcraft_docx::read(&comments_package(true)).unwrap();
doc.body = rebuilt.body.clone();
doc.parts = rebuilt.parts.clone();
@@ -314,7 +315,7 @@ fn list_identities_survive_and_a_copied_definition_gets_none() {
// A copy of the definition (as the engine makes when restarting a list) doesn't reuse the nsid.
let mut copy = doc.numbering.abstracts[0].clone();
copy.id = 7;
doc.numbering.abstracts.push(copy);
Arc::make_mut(&mut doc.numbering).abstracts.push(copy);
let n = text(&save(&doc), "word/numbering.xml");
assert_eq!(n.matches("1F2E3D4C").count(), 1, "{n}");
}
+12 -12
View File
@@ -249,7 +249,7 @@ fn every_para_prop_round_trips() {
#[test]
fn styles_and_lists_round_trip() {
let mut d = Document::new();
d.styles.upsert(Style {
Arc::make_mut(&mut d.styles).upsert(Style {
id: "MyStyle".into(),
name: "My Style".into(),
kind: StyleKind::Paragraph,
@@ -266,7 +266,7 @@ fn styles_and_lists_round_trip() {
default: false,
table: None,
});
d.styles.upsert(Style {
Arc::make_mut(&mut d.styles).upsert(Style {
id: "MyStyleChar".into(),
name: "My Style Char".into(),
kind: StyleKind::Character,
@@ -275,10 +275,10 @@ fn styles_and_lists_round_trip() {
hidden: true,
..Default::default()
});
let bullets = d.numbering.add_list(ListKind::Bullet);
let nums = d.numbering.add_list(ListKind::Legal);
let outline = d.numbering.add_list(ListKind::Outline);
if let Some(n) = d.numbering.nums.iter_mut().find(|n| n.id == outline) {
let bullets = Arc::make_mut(&mut d.numbering).add_list(ListKind::Bullet);
let nums = Arc::make_mut(&mut d.numbering).add_list(ListKind::Legal);
let outline = Arc::make_mut(&mut d.numbering).add_list(ListKind::Outline);
if let Some(n) = Arc::make_mut(&mut d.numbering).nums.iter_mut().find(|n| n.id == outline) {
n.start_overrides.push((0, 4));
}
let mut ps = Vec::new();
@@ -568,7 +568,7 @@ fn table_style_formatting_round_trips() {
band_size: Some(3),
..Default::default()
};
d.styles.upsert(Style {
Arc::make_mut(&mut d.styles).upsert(Style {
id: "Whole".into(),
name: "Whole".into(),
kind: StyleKind::Table,
@@ -621,7 +621,7 @@ fn every_table_style_part_round_trips() {
};
st.set_table_part(part, f);
}
d.styles.upsert(st.clone());
Arc::make_mut(&mut d.styles).upsert(st.clone());
let r = rt(&d);
let got = r.styles.get("Parts").unwrap();
for part in TablePart::ALL {
@@ -1709,7 +1709,7 @@ fn kitchen_sink() -> Document {
let mut d = Document::new();
let png = tiny_png();
let key = d.add_media(png, "png");
let num = d.numbering.add_list(ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let h = d.add_part(PartKind::Header, vec![para_block(Paragraph::with_text("Head", CharProps::default()))]);
let f = d.add_part(PartKind::Footnote, vec![para_block(Paragraph::with_text("Note", CharProps::default()))]);
let c = d.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text("Comment", CharProps::default()))]);
@@ -1941,10 +1941,10 @@ fn list_level_overrides_round_trip() {
use wordcraft_doc::numbering::{Counters, Level};
use wordcraft_doc::section::NumFormat;
let mut d = Document::new();
let num = d.numbering.add_list(ListKind::Numbered);
let restart = d.numbering.restart(num).unwrap();
let num = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let restart = Arc::make_mut(&mut d.numbering).restart(num).unwrap();
let own = Level { format: NumFormat::DecimalZero, text: "%1.%2".into(), indent: 26.5, hanging: 26.5, ..Level::default() };
if let Some(n) = d.numbering.nums.iter_mut().find(|n| n.id == restart) {
if let Some(n) = Arc::make_mut(&mut d.numbering).nums.iter_mut().find(|n| n.id == restart) {
n.level_overrides = vec![(1, own.clone())];
}
let back = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
+1 -1
View File
@@ -32,7 +32,7 @@ fn sample() -> Document {
d.core.title = "Garden <notes> & more".into();
d.core.creator = "Ada".into();
let key = d.add_media(tiny_png(), "png");
let num = d.numbering.add_list(ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let head = d.add_part(PartKind::Header, vec![para_block(Paragraph::with_text("Head", CharProps::default()))]);
let foot = d.add_part(PartKind::Footnote, vec![para_block(Paragraph::with_text("Sown in May.", CharProps::default()))]);
let com = d.add_part(PartKind::Comment, vec![para_block(Paragraph::with_text("Too short?", CharProps::default()))]);
+2 -1
View File
@@ -6,6 +6,7 @@ use super::{
Block, CharProps, CmdError, CmdResult, Document, FieldCode, InlineObject, Paragraph, Path, Pos, Session, SortKey, StoryRef, TabAlign, TabLeader,
TabStop, Value, delete_selection, heading_letter, p, sel_result, sort_key,
};
use std::sync::Arc;
/// The TOC field for `references.tableOfFigures` (ECMA-376 §17.16.5.68): `\c "<label>"` lists the
/// captions that label's SEQ field numbers, `\a "<label>"` lists them without their label and
@@ -300,7 +301,7 @@ pub fn update_generated(s: &mut Session) -> Result<(), CmdError> {
/// layout cache starts over (and the saved file would gain a style it doesn't use).
fn add_entry_style(s: &mut Session) {
if s.doc.styles.get(ENTRY_STYLE).is_none() {
s.doc.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut s.doc.styles).upsert(wordcraft_doc::Style {
id: ENTRY_STYLE.into(),
name: "List Entry".into(),
kind: wordcraft_doc::StyleKind::Paragraph,
+7 -4
View File
@@ -7,6 +7,7 @@
//! typing and deleting commands.
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::control::{ContentControl, ControlKind, ControlLock, ControlRange, ListItem, MAX_CONTROL_TEXT, MAX_LIST_ITEMS};
use wordcraft_doc::para::OBJ;
use wordcraft_doc::props::{CharProps, Rgb, TextColor};
@@ -172,7 +173,7 @@ fn placeholder_style_id(doc: &wordcraft_doc::Document) -> String {
/// The placeholder style, added to the document when it hasn't one.
fn ensure_placeholder_style(s: &mut Session) {
if s.doc.styles.get(&placeholder_style_id(&s.doc)).is_none() {
s.doc.styles.styles.push(Style {
Arc::make_mut(&mut s.doc.styles).styles.push(Style {
id: PLACEHOLDER_STYLE.into(),
name: PLACEHOLDER_STYLE_NAME.into(),
kind: StyleKind::Character,
@@ -799,6 +800,8 @@ mod tests {
#[cfg(test)]
mod editing_tests {
use std::sync::Arc;
use serde_json::json;
use wordcraft_doc::StoryRef;
use wordcraft_doc::control::ControlKind;
@@ -829,8 +832,8 @@ mod editing_tests {
let typed_style = |run_props: Option<CharProps>| {
let mut s = Session::new(wordcraft_doc::Document::new());
let grey = CharProps { color: Some(TextColor::Rgb(Rgb(0x80, 0x80, 0x80))), ..Default::default() };
s.doc.styles.styles.push(style("Platzhaltertext", "Placeholder Text", grey));
s.doc.styles.styles.push(style("Eingabe", "Eingabe", CharProps { bold: Some(true), ..Default::default() }));
Arc::make_mut(&mut s.doc.styles).styles.push(style("Platzhaltertext", "Placeholder Text", grey));
Arc::make_mut(&mut s.doc.styles).styles.push(style("Eingabe", "Eingabe", CharProps { bold: Some(true), ..Default::default() }));
run(&mut s, "text.insert", json!({"text": "Name: "}));
run(&mut s, "developer.plainText", json!({"tag": "name"}));
let start = s.doc.control_at(&s.sel.focus).unwrap().start;
@@ -852,7 +855,7 @@ mod editing_tests {
// style goes all the same.
let mut s = Session::new(wordcraft_doc::Document::new());
let grey = CharProps { color: Some(TextColor::Rgb(Rgb(0x80, 0x80, 0x80))), ..Default::default() };
s.doc.styles.styles.push(style("TexteEspaceReserve", "Texte de l'espace réservé", grey));
Arc::make_mut(&mut s.doc.styles).styles.push(style("TexteEspaceReserve", "Texte de l'espace réservé", grey));
run(&mut s, "text.insert", json!({"text": "Nom : "}));
run(&mut s, "developer.plainText", json!({"tag": "nom"}));
let r = s.doc.control_at(&s.sel.focus).unwrap();
+11 -9
View File
@@ -1,6 +1,7 @@
//! Design tab: themes, style sets, colours, fonts, paragraph spacing, watermark, page colour, borders.
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::props::{Border, BorderStyle, Borders, CharProps, LineSpacing, Rgb, TextColor};
use wordcraft_doc::{Watermark, styles};
@@ -95,8 +96,8 @@ pub fn specs() -> Vec<CommandSpec> {
fn set_fonts(s: &mut Session, heading: &str, body: &str) {
s.doc.settings.major_font = heading.into();
s.doc.settings.minor_font = body.into();
s.doc.styles.default_chr.font = Some(body.into());
for st in &mut s.doc.styles.styles {
Arc::make_mut(&mut s.doc.styles).default_chr.font = Some(body.into());
for st in &mut Arc::make_mut(&mut s.doc.styles).styles {
if let Some(f) = &st.chr.font
&& (f == styles::HEADING_FONT || st.id.starts_with("Heading") || st.id == "Title")
{
@@ -108,7 +109,7 @@ fn set_fonts(s: &mut Session, heading: &str, body: &str) {
}
fn set_accent(s: &mut Session, accent: Rgb) {
for st in &mut s.doc.styles.styles {
for st in &mut Arc::make_mut(&mut s.doc.styles).styles {
if st.id.starts_with("Heading") && st.id.len() == 8 && st.id.as_bytes().get(7).is_some_and(|d| *d <= b'5')
|| st.id == "IntenseQuote"
|| st.id == "IntenseEmphasis"
@@ -173,15 +174,16 @@ fn style_set(s: &mut Session, v: &Value) -> CmdResult {
}
// Overlay the set onto the document's sheet: every other style, the latent styles and the
// document defaults stay as they are (no set defines its own defaults).
let sheet = Arc::make_mut(&mut s.doc.styles);
for mut st in fresh.styles {
match s.doc.styles.styles.iter_mut().find(|x| x.id == st.id) {
match sheet.styles.iter_mut().find(|x| x.id == st.id) {
Some(old) => {
st.default = old.default;
st.locked = old.locked;
st.builtin = old.builtin;
*old = st;
}
None => s.doc.styles.styles.push(st),
None => sheet.styles.push(st),
}
}
let (h, b) = (s.doc.settings.major_font.clone(), s.doc.settings.minor_font.clone());
@@ -200,9 +202,9 @@ fn para_spacing(s: &mut Session, v: &Value) -> CmdResult {
"double" => (0.0, 8.0, 2.0),
_ => (0.0, 8.0, 1.15),
};
s.doc.styles.default_para.space_before = Some(before);
s.doc.styles.default_para.space_after = Some(after);
s.doc.styles.default_para.line_spacing = Some(LineSpacing::Multiple(line));
Arc::make_mut(&mut s.doc.styles).default_para.space_before = Some(before);
Arc::make_mut(&mut s.doc.styles).default_para.space_after = Some(after);
Arc::make_mut(&mut s.doc.styles).default_para.line_spacing = Some(LineSpacing::Multiple(line));
sel_result(s)
}
@@ -227,7 +229,7 @@ fn scale_font_sizes(s: &mut Session, v: &Value) -> CmdResult {
c.size = c.size.map(scale);
c.size_cs = c.size_cs.map(scale);
};
let st = &mut s.doc.styles;
let st = Arc::make_mut(&mut s.doc.styles);
// An unset document default resolves to the fallback size; make it explicit so inheriting styles follow.
st.default_chr.size = Some(scale(st.default_chr.size.unwrap_or(wordcraft_doc::props::DEFAULT_FONT_SIZE)));
st.default_chr.size_cs = st.default_chr.size_cs.map(scale);
+3 -2
View File
@@ -1,6 +1,7 @@
//! File tab (Backstage): new, open, save, export, print, properties; document inspection.
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::{Block, Document, StoryRef};
use super::{pos_json, sel_result};
@@ -230,7 +231,7 @@ fn new(s: &mut Session, v: &Value) -> CmdResult {
pub(crate) fn normal_document(s: &Session) -> Document {
let mut d = s.blank_document();
if let Some(lang) = s.prefs.default_language.as_ref().or(s.editing_lang.as_ref()) {
d.styles.default_chr.lang = Some(lang.clone());
Arc::make_mut(&mut d.styles).default_chr.lang = Some(lang.clone());
}
d
}
@@ -271,7 +272,7 @@ fn open(s: &mut Session, v: &Value) -> CmdResult {
if let Some(lang) = s.prefs.default_language.as_ref().or(s.editing_lang.as_ref())
&& matches!(ext.as_str(), "txt" | "text" | "md" | "markdown")
{
doc.styles.default_chr.lang = Some(lang.clone());
Arc::make_mut(&mut doc.styles).default_chr.lang = Some(lang.clone());
}
match s.used_because() {
None => s.set_document(doc),
+7 -6
View File
@@ -4,6 +4,7 @@
//! the number and the number's font.
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::StoryRef;
use wordcraft_doc::numbering::{Level, LevelSuffix, ListKind, levels_for};
use wordcraft_doc::props::{Align, NumRef, Rgb, TextColor};
@@ -183,26 +184,26 @@ fn define(s: &mut Session, v: &Value) -> CmdResult {
Some(n) => {
// Change the list in place: every list sharing its definition follows.
let aid = s.doc.numbering.num(n).map(|x| x.abstract_id).ok_or_else(|| CmdError::Params("no such list".into()))?;
if let Some(a) = s.doc.numbering.abstracts.iter_mut().find(|a| a.id == aid) {
if let Some(a) = Arc::make_mut(&mut s.doc.numbering).abstracts.iter_mut().find(|a| a.id == aid) {
a.levels = levels.clone();
if name.is_some() {
a.name = name;
}
}
if let Some(x) = s.doc.numbering.nums.iter_mut().find(|x| x.id == n) {
if let Some(x) = Arc::make_mut(&mut s.doc.numbering).nums.iter_mut().find(|x| x.id == n) {
x.level_overrides.clear();
}
let sharing: Vec<u32> = s.doc.numbering.nums.iter().filter(|x| x.abstract_id == aid).map(|x| x.id).collect();
touch_lists(s, &sharing)?;
n
}
None => s.doc.numbering.try_add_levels(name, levels.clone())?,
None => Arc::make_mut(&mut s.doc.numbering).try_add_levels(name, levels.clone())?,
};
// Levels linked to a paragraph style number every paragraph in that style.
let mut linked = Vec::new();
for (i, l) in levels.iter().enumerate() {
let Some(id) = l.style.as_deref().map(|st| s.doc.styles.find(st).map(|x| x.id.clone()).unwrap_or_else(|| st.to_string())) else { continue };
if let Some(st) = s.doc.styles.get_mut(&id) {
if let Some(st) = Arc::make_mut(&mut s.doc.styles).get_mut(&id) {
st.para.numbering = Some(NumRef { num, level: i as u8 });
linked.push(id);
}
@@ -292,11 +293,11 @@ fn preset_levels(preset: &str) -> Option<Vec<Level>> {
fn preset(s: &mut Session, v: &Value) -> CmdResult {
let id = p::req_str(v, "preset")?;
let levels = preset_levels(id).ok_or_else(|| CmdError::Params("`preset` must be headings, article, outlineHeadings or chapter".into()))?;
let num = s.doc.numbering.try_add_levels(None, levels)?;
let num = Arc::make_mut(&mut s.doc.numbering).try_add_levels(None, levels)?;
let mut linked = Vec::new();
for n in 1..=LEVELS as u8 {
let id = super::para::ensure_heading_style(s, n);
if let Some(st) = s.doc.styles.get_mut(&id) {
if let Some(st) = Arc::make_mut(&mut s.doc.styles).get_mut(&id) {
st.para.numbering = Some(NumRef { num, level: n - 1 });
linked.push(id);
}
+2 -1
View File
@@ -1,6 +1,7 @@
//! Paragraph formatting, lists and styles (Home › Paragraph, Home › Styles).
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::collate::{SortKey, sort_key};
use wordcraft_doc::numbering::ListKind;
use wordcraft_doc::props::{Align, Border, BorderStyle, Borders, LineSpacing, NumRef, ParaProps, Rgb, TabAlign, TabLeader, TabStop};
@@ -256,7 +257,7 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("styles.addToGallery", "Add to Style Gallery", "Home › Styles", |s, v| {
let id = style_id(s, p::req_str(v, "style")?)?;
let on = p::bool(v, "value").unwrap_or(true);
if let Some(st) = s.doc.styles.get_mut(&id) {
if let Some(st) = Arc::make_mut(&mut s.doc.styles).get_mut(&id) {
st.quick = on;
}
sel_result(s)
+4 -3
View File
@@ -1,5 +1,6 @@
use super::format::fmt;
use super::{CmdError, CmdResult, LineSpacing, ListKind, NumRef, ParaProps, Session, Style, StyleKind, Value, json, p, sel_result};
use std::sync::Arc;
/// Toggle lists using effective paragraph numbering; an explicit off request removes numbering.
pub(super) fn list(s: &mut Session, v: &Value, default: ListKind) -> CmdResult {
@@ -45,9 +46,9 @@ pub(super) fn list(s: &mut Session, v: &Value, default: ListKind) -> CmdResult {
None => match kind {
ListKind::Bullet | ListKind::BulletChar(_) => match s.doc.numbering.find_kind(kind) {
Some(n) => n,
None => s.doc.numbering.try_add_list(kind)?,
None => Arc::make_mut(&mut s.doc.numbering).try_add_list(kind)?,
},
_ => s.doc.numbering.try_add_list(kind)?,
_ => Arc::make_mut(&mut s.doc.numbering).try_add_list(kind)?,
},
};
fmt(s, &|p| {
@@ -68,7 +69,7 @@ pub(super) fn restart_numbering(s: &mut Session, _: &Value) -> CmdResult {
let Some(n) = s.doc.para_at(&f).and_then(|p| p.props.numbering).filter(|n| n.num != 0) else {
return Err(CmdError::Failed("not in a numbered list".into()));
};
let new = s.doc.numbering.restart(n.num).ok_or_else(|| CmdError::Failed("bad list".into()))?;
let new = Arc::make_mut(&mut s.doc.numbering).restart(n.num).ok_or_else(|| CmdError::Failed("bad list".into()))?;
// This paragraph and the following ones of the same list use the new instance.
let paths: Vec<_> = s.doc.para_paths(f.story).into_iter().filter(|p| *p >= f.path).collect();
for path in paths {
+2 -1
View File
@@ -1,5 +1,6 @@
use super::lists::touch_all;
use super::{Block, CmdError, CmdResult, NumRef, ParaProps, Pos, Session, SortKey, Value, p, sel_result, sort_key};
use std::sync::Arc;
/// Move Paragraph Up / Down (Alt+Shift+Up/Down, ⌃⇧↑/↓ on macOS): the paragraphs the selection
/// touches swap places with the paragraph above or below, in their own container (the story or
@@ -96,7 +97,7 @@ pub(crate) fn ensure_heading_style(s: &mut Session, n: u8) -> String {
if s.doc.styles.get(&sid).is_none()
&& let Some(st) = builtin.get(&sid).cloned()
{
s.doc.styles.upsert(st);
Arc::make_mut(&mut s.doc.styles).upsert(st);
}
}
touch_all(s);
+6 -5
View File
@@ -1,6 +1,7 @@
use super::format::fmt;
use super::lists::touch_all;
use super::{CharProps, CmdError, CmdResult, ParaProps, Session, Style, StyleKind, Value, json, p, sel_result};
use std::sync::Arc;
pub(super) fn style_id(s: &Session, name: &str) -> Result<String, CmdError> {
s.doc.styles.find(name).map(|x| x.id.clone()).ok_or_else(|| CmdError::Params(format!("no style `{name}`")))
@@ -101,7 +102,7 @@ pub(super) fn create_style(s: &mut Session, v: &Value) -> CmdResult {
st.chr.fmt_change = None;
st.para = st.para.formatting();
}
s.doc.styles.upsert(st);
Arc::make_mut(&mut s.doc.styles).upsert(st);
apply_style(s, &json!({"style": id}))?;
Ok(json!({"id": id}))
}
@@ -113,7 +114,7 @@ pub(super) fn modify_style(s: &mut Session, v: &Value) -> CmdResult {
v.get("para").map(|c| serde_json::from_value(c.clone())).transpose().map_err(|e| CmdError::Params(e.to_string()))?;
let next = p::str(v, "next").map(str::to_string);
let name = p::str(v, "name").map(str::to_string);
let st = s.doc.styles.get_mut(&id).ok_or_else(|| CmdError::Params("no such style".into()))?;
let st = Arc::make_mut(&mut s.doc.styles).get_mut(&id).ok_or_else(|| CmdError::Params("no such style".into()))?;
if let Some(c) = chr {
st.chr.overlay(&c);
}
@@ -175,7 +176,7 @@ pub(super) fn update_to_match(s: &mut Session, v: &Value) -> CmdResult {
};
let chr = retarget(&target.chr, &have_c, &want_c, CHAR_KEEP)?;
let pp = if target.kind == StyleKind::Paragraph { retarget(&target.para, &have_p, &want_p, PARA_KEEP)? } else { target.para.clone() };
if let Some(st) = s.doc.styles.get_mut(&id) {
if let Some(st) = Arc::make_mut(&mut s.doc.styles).get_mut(&id) {
st.chr = chr;
st.para = pp;
}
@@ -263,7 +264,7 @@ pub(super) fn delete_style(s: &mut Session, v: &Value) -> CmdResult {
}
let mut changed = 0;
for g in &gone {
s.doc.styles.remove(&g.id);
Arc::make_mut(&mut s.doc.styles).remove(&g.id);
// Fall back to the base only while it is a style of the same kind that still exists.
let base = g.based_on.clone().filter(|b| s.doc.styles.get(b).is_some_and(|x| x.kind == g.kind));
changed += s.doc.restyle(&g.id, base);
@@ -279,7 +280,7 @@ pub(super) fn set_visibility(s: &mut Session, v: &Value) -> CmdResult {
if gallery.is_none() && hidden.is_none() {
return Err(CmdError::Params("give `gallery` and/or `hidden`".into()));
}
let st = s.doc.styles.get_mut(&id).ok_or_else(|| CmdError::Params(format!("no style `{id}`")))?;
let st = Arc::make_mut(&mut s.doc.styles).get_mut(&id).ok_or_else(|| CmdError::Params(format!("no style `{id}`")))?;
if let Some(h) = hidden {
st.hidden = h;
}
+2 -1
View File
@@ -9,6 +9,7 @@
use serde_json::{Value, json};
use std::collections::HashSet;
use std::sync::Arc;
use wordcraft_doc::para::{InlineObject, OBJ};
use wordcraft_doc::props::{CellProps, CharProps, RowProps, TableProps};
use wordcraft_doc::{Block, Blocks, Document, Path, PermRange, Pos, StoryRef};
@@ -78,7 +79,7 @@ fn restrict(s: &mut Session, v: &Value) -> CmdResult {
return Err(CmdError::Disabled("stop protection before changing the formatting restrictions".into()));
}
let names: Vec<&str> = list.iter().filter_map(Value::as_str).take(10_000).collect();
for st in &mut s.doc.styles.styles {
for st in &mut Arc::make_mut(&mut s.doc.styles).styles {
st.locked = names.iter().any(|n| *n == st.id || n.eq_ignore_ascii_case(&st.name));
}
}
+2 -2
View File
@@ -136,7 +136,7 @@ fn read_only_exception_around_a_table_lets_it_be_formatted() {
#[test]
fn formatting_restriction_refuses_attach_template_update() {
let mut t = Document::from_text("Template.");
if let Some(st) = t.styles.styles.iter_mut().find(|st| st.id == "Heading1") {
if let Some(st) = Arc::make_mut(&mut t.styles).styles.iter_mut().find(|st| st.id == "Heading1") {
st.chr.size = Some(31.0);
}
let data = crate::cmd::insert::base64_encode(&crate::io::save_bytes("t.docx", &t).unwrap());
@@ -147,7 +147,7 @@ fn formatting_restriction_refuses_attach_template_update() {
assert!(e.contains("formatting is limited"), "{e}");
assert_eq!(s.doc, before);
// Copying styles in replaces their definitions, locked flag included (#3692).
s.doc.styles.styles.iter_mut().filter(|st| st.id == "Heading1").for_each(|st| st.locked = true);
Arc::make_mut(&mut s.doc.styles).styles.iter_mut().filter(|st| st.id == "Heading1").for_each(|st| st.locked = true);
let before = s.doc.clone();
let e = s.run("styles.copyFrom", &json!({"path": "Report.dotx", "styles": ["Heading 1"], "data": data})).unwrap_err().to_string();
assert!(e.contains("formatting is limited"), "{e}");
+5 -5
View File
@@ -112,7 +112,7 @@ fn build_new(s: &Session, v: &Value) -> Result<Style, CmdError> {
fn new_style(s: &mut Session, v: &Value) -> CmdResult {
let st = build_new(s, v)?;
let id = st.id.clone();
s.doc.styles.upsert(st);
Arc::make_mut(&mut s.doc.styles).upsert(st);
s.dirty = true;
if p::bool(v, "apply").unwrap_or(true) {
apply(s, &id)?;
@@ -131,7 +131,7 @@ fn target(s: &Session, v: &Value) -> Result<String, CmdError> {
fn modify_style(s: &mut Session, v: &Value) -> CmdResult {
let st = build_modified(s, v)?;
let id = st.id.clone();
s.doc.styles.upsert(st);
Arc::make_mut(&mut s.doc.styles).upsert(st);
s.dirty = true;
if p::bool(v, "apply").unwrap_or(false) {
apply(s, &id)?;
@@ -189,7 +189,7 @@ pub fn preview(s: &Session, v: &Value) -> Option<TableStyleProps> {
build_new(s, &v).ok()?
};
let id = st.id.clone();
let mut sheet = s.doc.styles.clone();
let mut sheet = (*s.doc.styles).clone();
sheet.upsert(st);
sheet.table_style(&id)
}
@@ -214,7 +214,7 @@ pub fn delete(s: &mut Session, id: &str) -> CmdResult {
return Err(CmdError::Failed(format!("`{}` is a built-in table style and can't be deleted", st.name)));
}
let mut rebased = Vec::new();
for child in s.doc.styles.styles.iter_mut().filter(|c| c.based_on.as_deref() == Some(id.as_str())) {
for child in Arc::make_mut(&mut s.doc.styles).styles.iter_mut().filter(|c| c.based_on.as_deref() == Some(id.as_str())) {
let mut para = st.para.clone();
para.overlay(&child.para);
child.para = para;
@@ -231,7 +231,7 @@ pub fn delete(s: &mut Session, id: &str) -> CmdResult {
child.based_on = st.based_on.clone();
rebased.push(child.id.clone());
}
s.doc.styles.styles.retain(|x| x.id != id);
Arc::make_mut(&mut s.doc.styles).styles.retain(|x| x.id != id);
let fallback = table_style_id(s, "TableGrid");
let mut tables = restyle(&mut s.doc.body, &id, fallback.as_deref(), 0);
for part in s.doc.parts.values_mut() {
+13 -12
View File
@@ -9,6 +9,7 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use serde_json::{Value, json};
use wordcraft_doc::Document;
@@ -100,8 +101,8 @@ fn update_styles(s: &mut Session, v: &Value) -> CmdResult {
/// What [`update_styles`] does to a document: all of `src`'s styles and its defaults.
pub fn apply_template_styles(doc: &mut Document, src: &Document) -> Value {
doc.styles.default_chr = src.styles.default_chr.clone();
doc.styles.default_para = src.styles.default_para.clone();
Arc::make_mut(&mut doc.styles).default_chr = src.styles.default_chr.clone();
Arc::make_mut(&mut doc.styles).default_para = src.styles.default_para.clone();
let ids: Vec<String> = src.styles.styles.iter().map(|st| st.id.clone()).collect();
copy_styles(doc, src, &ids)
}
@@ -172,7 +173,7 @@ fn rename(s: &mut Session, v: &Value) -> CmdResult {
if s.doc.styles.styles.iter().any(|o| o.id != st.id && o.name.eq_ignore_ascii_case(&name)) {
return Err(CmdError::Failed(format!("a style named `{name}` already exists")));
}
if let Some(x) = s.doc.styles.get_mut(&st.id) {
if let Some(x) = Arc::make_mut(&mut s.doc.styles).get_mut(&st.id) {
x.name = name.clone();
}
touch_all(s);
@@ -346,7 +347,7 @@ pub fn copy_styles(doc: &mut Document, src: &Document, ids: &[String]) -> Value
}
};
let builtin = doc.styles.get(&target).is_some_and(|d| d.builtin) || st.builtin;
doc.styles.upsert(Style { id: target.clone(), builtin, ..st.clone() });
Arc::make_mut(&mut doc.styles).upsert(Style { id: target.clone(), builtin, ..st.clone() });
map.insert(st.id.clone(), target.clone());
order.push((st.id.clone(), target));
}
@@ -366,7 +367,7 @@ pub fn copy_styles(doc: &mut Document, src: &Document, ids: &[String]) -> Value
}
n
});
if let Some(d) = doc.styles.get_mut(&target) {
if let Some(d) = Arc::make_mut(&mut doc.styles).get_mut(&target) {
d.based_on = based_on;
d.next = next;
d.linked = linked;
@@ -389,8 +390,8 @@ fn import_num(doc: &mut Document, src: &Document, num: u32, map: &HashMap<String
for l in &mut levels {
l.style = l.style.as_deref().and_then(|s| map.get(s).cloned().or_else(|| doc.styles.get(s).map(|x| x.id.clone())));
}
doc.numbering.abstracts.push(AbstractNum { id: new_abs, name: abs.name.clone(), levels, ..Default::default() });
doc.numbering.nums.push(Num { id: new_num, abstract_id: new_abs, ..n.clone() });
Arc::make_mut(&mut doc.numbering).abstracts.push(AbstractNum { id: new_abs, name: abs.name.clone(), levels, ..Default::default() });
Arc::make_mut(&mut doc.numbering).nums.push(Num { id: new_num, abstract_id: new_abs, ..n.clone() });
done.insert(num, new_num);
Some(new_num)
}
@@ -405,17 +406,17 @@ mod tests {
/// "Box" (missing in the document) and a numbered "Steps" style.
fn template() -> Document {
let mut t = Document::new();
if let Some(h) = t.styles.get_mut("Heading1") {
if let Some(h) = Arc::make_mut(&mut t.styles).get_mut("Heading1") {
h.chr.size = Some(31.0);
}
t.styles.upsert(Style {
Arc::make_mut(&mut t.styles).upsert(Style {
id: "Box".into(),
name: "Box".into(),
kind: StyleKind::Paragraph,
based_on: Some("Normal".into()),
..Default::default()
});
t.styles.upsert(Style {
Arc::make_mut(&mut t.styles).upsert(Style {
id: "Callout".into(),
name: "Callout".into(),
kind: StyleKind::Paragraph,
@@ -423,8 +424,8 @@ mod tests {
chr: CharProps { italic: Some(true), ..Default::default() },
..Default::default()
});
let num = t.numbering.add_list(ListKind::Numbered);
t.styles.upsert(Style {
let num = Arc::make_mut(&mut t.numbering).add_list(ListKind::Numbered);
Arc::make_mut(&mut t.styles).upsert(Style {
id: "Steps".into(),
name: "Steps".into(),
kind: StyleKind::Paragraph,
+2 -1
View File
@@ -3,6 +3,7 @@ use super::{
Block, Border, BorderStyle, CmdError, CmdResult, ListKind, NumRef, OBJ, Path, Pos, Selection, Session, StoryRef, Table, Value, delete_selection,
p, sel_result, split_para, type_text_effect,
};
use std::sync::Arc;
/// Smart quotes and AutoFormat-as-you-type (lists from "* " / "1. ", dashes).
fn autoformat(s: &mut Session, text: &str) -> Option<String> {
@@ -164,7 +165,7 @@ fn list_autoformat(s: &mut Session) -> Result<bool, CmdError> {
let found = if kind.is_bullet() { s.doc.numbering.find_kind(kind) } else { None };
let num = match found {
Some(n) => n,
None => s.doc.numbering.try_add_list(kind)?,
None => Arc::make_mut(&mut s.doc.numbering).try_add_list(kind)?,
};
let para = s.doc.para_mut(f.story, &f.path)?;
para.delete(0, f.off)?;
+2 -2
View File
@@ -30,9 +30,9 @@ pub(super) fn language(s: &mut Session, v: &Value) -> CmdResult {
}
if p::bool(v, "default") == Some(true) {
let l = lang.clone().ok_or_else(|| CmdError::Params("`default` needs `lang`".into()))?;
s.doc.styles.default_chr.lang = Some(l.clone());
Arc::make_mut(&mut s.doc.styles).default_chr.lang = Some(l.clone());
// A Normal style that names its own language follows the new default.
if let Some(n) = s.doc.styles.styles.iter_mut().find(|x| x.id == "Normal")
if let Some(n) = Arc::make_mut(&mut s.doc.styles).styles.iter_mut().find(|x| x.id == "Normal")
&& n.chr.lang.is_some()
{
n.chr.lang = Some(l);
+5 -4
View File
@@ -8,6 +8,7 @@ use super::{
Block, CharProps, CmdError, CommandSpec, InlineObject, NumRef, Paragraph, Selection, StoryRef, Value, delete_selection, json, p, para_block,
sel_result,
};
use std::sync::Arc;
/// Builds the tools, review, file and insertion command table: Repeat and macros; AutoCorrect,
/// keyboard and ribbon customization and search; Compare, accessibility, inspection, protection,
@@ -299,8 +300,8 @@ pub fn specs() -> Vec<CommandSpec> {
.and_then(|x| x.props.numbering)
.filter(|n| n.num != 0)
.ok_or_else(|| CmdError::Failed("not in a numbered list".into()))?;
let new = s.doc.numbering.restart(n.num).ok_or_else(|| CmdError::Failed("bad list".into()))?;
if let Some(num) = s.doc.numbering.nums.iter_mut().find(|x| x.id == new) {
let new = Arc::make_mut(&mut s.doc.numbering).restart(n.num).ok_or_else(|| CmdError::Failed("bad list".into()))?;
if let Some(num) = Arc::make_mut(&mut s.doc.numbering).nums.iter_mut().find(|x| x.id == new) {
num.start_overrides = vec![(n.level, value)];
}
let paths: Vec<_> = s.doc.para_paths(f.story).into_iter().filter(|q| *q >= f.path).collect();
@@ -323,9 +324,9 @@ pub fn specs() -> Vec<CommandSpec> {
let fmt = wordcraft_doc::section::NumFormat::from_ooxml(p::str(v, "format").unwrap_or("decimal"));
let text = p::str(v, "text").unwrap_or("%1.").to_string();
let start = p::u64(v, "start").unwrap_or(1).min(100_000) as u32;
let id = s.doc.numbering.try_add_list(wordcraft_doc::ListKind::Numbered)?;
let id = Arc::make_mut(&mut s.doc.numbering).try_add_list(wordcraft_doc::ListKind::Numbered)?;
if let Some(a) = s.doc.numbering.abstract_of(id).map(|a| a.id)
&& let Some(abs) = s.doc.numbering.abstracts.iter_mut().find(|x| x.id == a)
&& let Some(abs) = Arc::make_mut(&mut s.doc.numbering).abstracts.iter_mut().find(|x| x.id == a)
&& let Some(l) = abs.levels.first_mut()
{
l.format = fmt;
+3 -2
View File
@@ -1,5 +1,6 @@
//! Built-in sample documents and templates (original text, generated in code).
use std::sync::Arc;
use wordcraft_doc::para::InlineObject;
use wordcraft_doc::props::{Align, Border, BorderStyle, Borders, CharProps, NumRef, Rgb, TabAlign, TabLeader, TabStop, TextColor};
use wordcraft_doc::{Block, Document, ListKind, Paragraph, PartKind, Table, para_block};
@@ -50,10 +51,10 @@ pub fn sample_document() -> Document {
quote.props.space_before = Some(12.0);
b.push(Block::Para(quote));
b.push(Block::Para(styled("What we agree on", "Heading2")));
let mut num = d.numbering.clone();
let mut num = (*d.numbering).clone();
let bullets = num.add_list(ListKind::Bullet);
let steps = num.add_list(ListKind::Numbered);
d.numbering = num;
d.numbering = Arc::new(num);
for (lead, rest) in [
("Clean as you go. ", "Leave a table the way you would like to find it."),
("Label what is yours. ", "Unlabelled supplies on the commons shelf are fair game."),
+1 -1
View File
@@ -199,7 +199,7 @@ impl Session {
self.editing_lang = tag;
let untouched = self.path.is_none() && !self.dirty && self.history.is_empty() && self.redo.is_empty();
if untouched && self.doc.plain_text(StoryRef::Body).trim().is_empty() && self.doc.paragraph_count() <= 1 {
self.doc.styles.default_chr.lang = Some(self.editing_lang.clone().unwrap_or_else(|| "en-US".into()));
Arc::make_mut(&mut self.doc.styles).default_chr.lang = Some(self.editing_lang.clone().unwrap_or_else(|| "en-US".into()));
self.rev = self.rev.wrapping_add(1);
// Not an edit: the document is as untouched as it was.
if self.made_rev == self.rev.wrapping_sub(1) {
+140 -28
View File
@@ -4,6 +4,7 @@ use wordcraft_proof::cs::install_dictionary;
mod czech_test_dict;
use serde_json::json;
use std::sync::Arc;
use wordcraft_doc::props::BorderStyle;
use wordcraft_doc::{Pos, StoryRef};
@@ -4397,7 +4398,7 @@ fn toc_page_numbers_reach_the_right_margin() {
let long = "A deliberately long section title that does not fit on one line of the table of contents and wraps onto a second line";
let mut d = wordcraft_doc::Document::from_text(&format!("Contents\n\nNo entries.\n\nShort\n{long}\nTop\nAppendix"));
{
let toc2 = d.styles.get_mut("TOC2").unwrap();
let toc2 = Arc::make_mut(&mut d.styles).get_mut("TOC2").unwrap();
toc2.para.tabs = Some(vec![
TabStop { pos: 77.0, align: TabAlign::Left, leader: TabLeader::None },
TabStop { pos: 467.2, align: TabAlign::Right, leader: TabLeader::Dot },
@@ -4534,7 +4535,7 @@ fn toc_entries_carry_heading_numbers() {
run(&mut s, "para.heading1", json!({}));
run(&mut s, "text.newParagraph", json!({}));
}
let num = s.doc.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut s.doc.numbering).add_list(wordcraft_doc::ListKind::Numbered);
for b in s.doc.body.iter_mut() {
if let wordcraft_doc::Block::Para(p) = std::sync::Arc::make_mut(b)
&& p.props.style.as_deref() == Some("Heading1")
@@ -4835,10 +4836,10 @@ fn commands_after_the_largest_ids_take_free_ones() {
s.doc.parts.insert(id, wordcraft_doc::Part { kind: wordcraft_doc::PartKind::Comment, blocks, ..Default::default() });
s.doc.comments.insert(id, wordcraft_doc::Comment { part: id, ..Default::default() });
}
s.doc.numbering.add_list(wordcraft_doc::ListKind::Bullet);
s.doc.numbering.abstracts[0].id = u32::MAX;
s.doc.numbering.nums[0].id = u32::MAX;
s.doc.numbering.nums[0].abstract_id = u32::MAX;
Arc::make_mut(&mut s.doc.numbering).add_list(wordcraft_doc::ListKind::Bullet);
Arc::make_mut(&mut s.doc.numbering).abstracts[0].id = u32::MAX;
Arc::make_mut(&mut s.doc.numbering).nums[0].id = u32::MAX;
Arc::make_mut(&mut s.doc.numbering).nums[0].abstract_id = u32::MAX;
let before = s.doc.clone();
assert_eq!(run(&mut s, "review.newComment", json!({"text": "new"})), json!({"id": 1, "story": 1}));
assert_eq!(run(&mut s, "review.reply", json!({"id": 0, "text": "re"})), json!({"id": 2}));
@@ -4935,7 +4936,7 @@ fn word_count_includes_text_boxes_by_default() {
#[test]
fn word_count_includes_numbered_list_labels_in_words_but_not_characters() {
let mut d = wordcraft_doc::Document::new();
let num_id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num_id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let mut p = wordcraft_doc::Paragraph::with_text("Apple", wordcraft_doc::CharProps::default());
p.props.numbering = Some(wordcraft_doc::props::NumRef { num: num_id, level: 0 });
d.body = vec![std::sync::Arc::new(wordcraft_doc::Block::Para(p))];
@@ -5908,6 +5909,111 @@ fn modify_style_takes_hex_colours_like_format_color() {
assert_eq!(color(&s), Some(TextColor::Rgb(Rgb(0x1B, 0x16, 0x11))), "the hex string also reads inside {{\"Rgb\": …}}");
}
#[test]
fn undo_steps_share_the_style_sheet_and_lists_until_a_command_changes_them() {
// Every undo step copied the whole style sheet and list definitions: kilobytes a style, so
// typing into a document with hundreds of styles cost megabytes a step.
let mut s = Session::new(wordcraft_doc::Document::from_text("One two three."));
for _ in 0..5 {
run(&mut s, "text.insert", json!({"text": "word "}));
run(&mut s, "caret.left", json!({}));
}
assert_eq!(s.undo_labels().len(), 5, "five undo steps");
assert!(Arc::strong_count(&s.doc.styles) >= 6, "the document and each step hold one style sheet");
assert!(Arc::strong_count(&s.doc.numbering) >= 6, "and one set of list definitions");
// A command that changes a style copies the sheet once; undo puts the shared one back.
let sheet = Arc::clone(&s.doc.styles);
run(&mut s, "styles.modify", json!({"style": "Normal", "chr": {"bold": true}}));
assert!(!Arc::ptr_eq(&sheet, &s.doc.styles));
assert_ne!(*sheet, *s.doc.styles, "the change is in the new copy only");
run(&mut s, "edit.undo", json!({}));
assert!(Arc::ptr_eq(&sheet, &s.doc.styles), "undo restores the step's own sheet");
// Lists likewise.
let lists = Arc::clone(&s.doc.numbering);
run(&mut s, "para.numbering", json!({}));
assert!(!Arc::ptr_eq(&lists, &s.doc.numbering));
assert!(s.doc.numbering.nums.len() > lists.nums.len());
run(&mut s, "edit.undo", json!({}));
assert!(Arc::ptr_eq(&lists, &s.doc.numbering));
}
/// The same at scale: 1,000 edits of a document with 600 styles and 60 lists of nine levels. The
/// history keeps the newest `MAX_UNDO` steps and drops the oldest. Its steps hold one style sheet
/// for each style change they span, however many steps there are, and one set of lists. A sheet
/// that only dropped steps held is freed.
#[test]
fn a_thousand_edits_of_a_large_document_keep_the_newest_steps_and_share_its_styles() {
use crate::doc_state::MAX_UNDO;
use std::collections::HashSet;
use std::sync::Weak;
let body: Vec<String> = (0..200).map(|i| format!("Paragraph {i} of the body, with a few words in it.")).collect();
let mut doc = wordcraft_doc::Document::from_text(&body.join("\n"));
let sheet = Arc::make_mut(&mut doc.styles);
let normal = sheet.get("Normal").cloned().unwrap();
for i in 0..600 {
let (id, name) = (format!("Custom{i}"), format!("Custom {i}"));
sheet.upsert(wordcraft_doc::Style {
id,
name,
based_on: Some("Normal".into()),
builtin: false,
default: false,
quick: false,
..normal.clone()
});
}
for _ in 0..60 {
Arc::make_mut(&mut doc.numbering).add_list(wordcraft_doc::ListKind::Legal);
}
let mut s = Session::new(doc);
// An edit types a word, except every hundredth, which changes a style: one undo step each.
// After each edit: the text, and the sheet and lists the document then had.
let edits = 1000;
let mut after = Vec::with_capacity(edits);
let mut probe = Weak::new();
for i in 0..edits {
if i % 100 == 50 {
run(&mut s, "styles.modify", json!({"style": "Normal", "chr": {"size": 10 + i / 100}}));
} else {
run(&mut s, "text.insert", json!({"text": format!("w{i} ")}));
run(&mut s, "caret.left", json!({}));
}
after.push((text(&s), Arc::downgrade(&s.doc.styles), Arc::downgrade(&s.doc.numbering)));
// Edit 50's sheet is replaced at edit 150, so the step taken before edit 150 is the last
// to hold it. That step is the oldest one kept until edit 650 pushes it out.
match i {
50 => probe = Arc::downgrade(&s.doc.styles),
649 => assert!(probe.upgrade().is_some(), "a kept step still holds edit 50's sheet"),
650 => assert!(probe.upgrade().is_none(), "dropping the last step that held edit 50's sheet frees it"),
_ => {}
}
}
assert_eq!(s.undo_steps_so_far(), edits as u64, "one undo step an edit");
assert_eq!(s.undo_depth(), MAX_UNDO);
// Undo goes back through the newest steps, newest first. Each restores the text and the very
// sheet and lists the document had after that edit: the steps share them, not copies.
let (mut sheets, mut lists) = (HashSet::new(), HashSet::new());
for k in 0..=MAX_UNDO {
let (txt, sheet, numbering) = &after[edits - 1 - k];
assert_eq!(&text(&s), txt, "after {k} undos");
assert!(Arc::ptr_eq(&s.doc.styles, &sheet.upgrade().unwrap()), "the sheet after {k} undos");
assert!(Arc::ptr_eq(&s.doc.numbering, &numbering.upgrade().unwrap()), "the lists after {k} undos");
sheets.insert(Arc::as_ptr(&s.doc.styles));
lists.insert(Arc::as_ptr(&s.doc.numbering));
if k < MAX_UNDO {
assert!(s.undo(), "undo {}", k + 1);
}
}
assert!(!s.undo(), "the oldest 500 steps were dropped: edits 0 to 499 can't be undone");
// The 501 states, the document's and the kept steps', hold six sheets (edit 450's, and those
// of the changes at 550 to 950) and one set of lists.
assert_eq!(sheets.len(), 6);
assert_eq!(lists.len(), 1);
}
/// #865: the other colours in CharProps and ParaProps (`underlineColor`, `shading`, a border's
/// `color`) take `RRGGBB` / `#RRGGBB` like `para.borders` and `table.shading`, and still `[r, g, b]`.
#[test]
@@ -7222,7 +7328,7 @@ fn deleting_a_table_style_from_manage_styles_uses_table_style_deletion() {
assert_eq!((r["deleted"][0].as_str(), r["restyled"].as_u64()), (Some("Base"), Some(1)));
assert_eq!(s.doc.table(StoryRef::Body, &tp).unwrap().props.style.as_deref(), Some("TableGrid"));
assert_eq!(s.doc.styles.table_style("Child").unwrap().parts.header_fill, Some(Rgb(0xC0, 0, 0)), "the child keeps its look");
for st in s.doc.styles.styles.iter_mut() {
for st in Arc::make_mut(&mut s.doc.styles).styles.iter_mut() {
st.builtin = false;
}
assert!(s.run("styles.delete", &json!({"style": "Table Grid"})).is_err());
@@ -7367,7 +7473,7 @@ fn delete_table_style_falls_back_and_undoes() {
fn builtin_table_styles_marked_custom_in_a_docx_cant_be_deleted() {
let mut s = s();
run(&mut s, "insert.table", json!({"rows": 1, "cols": 1}));
for st in s.doc.styles.styles.iter_mut() {
for st in Arc::make_mut(&mut s.doc.styles).styles.iter_mut() {
st.builtin = false;
}
for name in ["Table Grid", "Normal Table", "TableGrid"] {
@@ -7979,7 +8085,7 @@ fn list_buttons_follow_the_paragraphs_own_level() {
// A bulleted list whose second level is numbered: at level 2 the paragraph is numbered.
let num = s.doc.para_at(&s.sel.focus).unwrap().props.numbering.unwrap().num;
let numbered = wordcraft_doc::numbering::Level { text: "%2.".into(), ..Default::default() };
s.doc.numbering.nums.iter_mut().find(|n| n.id == num).unwrap().level_overrides.push((1, numbered));
Arc::make_mut(&mut s.doc.numbering).nums.iter_mut().find(|n| n.id == num).unwrap().level_overrides.push((1, numbered));
run(&mut s, "para.listLevel", json!({"level": 1}));
assert_eq!(state(&s), ListButtons { bullets: false, numbering: true, multilevel: false });
run(&mut s, "para.numbering", json!({}));
@@ -9366,7 +9472,7 @@ fn default_proofing_language_survives_a_restart() {
assert_eq!(second.doc.styles.default_chr.lang.as_deref(), Some("fr-FR"));
let mut english = wordcraft_doc::Document::from_text("English document");
english.styles.default_chr.lang = Some("en-GB".into());
Arc::make_mut(&mut english.styles).default_chr.lang = Some("en-GB".into());
let bytes = crate::io::save_bytes("english.docx", &english).unwrap();
let data = crate::cmd::insert::base64_encode(&bytes);
run(&mut second, "file.open", json!({"path": "english.docx", "data": data}));
@@ -10009,7 +10115,7 @@ fn ordinals_and_untagged_text_sort_and_sum_as_numbers() {
let mut s = s();
s.set_editing_language(Some("pl-PL"));
s.doc.styles.default_chr.lang = None;
Arc::make_mut(&mut s.doc.styles).default_chr.lang = None;
run(&mut s, "insert.table", json!({"rows": 3, "cols": 1}));
run(&mut s, "text.insert", json!({"text": "3,50"}));
run(&mut s, "text.tab", json!({}));
@@ -10672,8 +10778,14 @@ fn inspect_document_hidden_text_in_every_story_and_from_styles() {
run(&mut s, "insert.footer", json!({"text": "f"}));
s.sel = crate::Selection::caret(Pos::body(0, 0));
let hidden = CharProps { hidden: Some(true), ..Default::default() };
s.doc.styles.upsert(Style { id: "HideC".into(), name: "Hide C".into(), kind: StyleKind::Character, chr: hidden.clone(), ..Default::default() });
s.doc.styles.upsert(Style {
Arc::make_mut(&mut s.doc.styles).upsert(Style {
id: "HideC".into(),
name: "Hide C".into(),
kind: StyleKind::Character,
chr: hidden.clone(),
..Default::default()
});
Arc::make_mut(&mut s.doc.styles).upsert(Style {
id: "HideP".into(),
name: "Hide P".into(),
kind: StyleKind::Paragraph,
@@ -10784,7 +10896,7 @@ fn inspect_document_removes_text_hidden_by_document_defaults() {
use wordcraft_doc::props::CharProps;
use wordcraft_doc::{Paragraph, para_block};
let mut s = s();
s.doc.styles.default_chr.hidden = Some(true);
Arc::make_mut(&mut s.doc.styles).default_chr.hidden = Some(true);
s.doc.body = vec![
para_block(Paragraph::with_text("hide-default", CharProps::default())),
para_block(Paragraph::with_text("keep-default", CharProps { hidden: Some(false), ..Default::default() })),
@@ -10827,7 +10939,7 @@ fn inspect_document_keeps_content_control_markers() {
fn inspect_document_lists_tables_whose_style_hides_text() {
use wordcraft_doc::styles::{Style, StyleKind, TableStyleParts};
let mut s = s();
s.doc.styles.upsert(Style {
Arc::make_mut(&mut s.doc.styles).upsert(Style {
id: "HideHeader".into(),
name: "Hide Header".into(),
kind: StyleKind::Table,
@@ -11421,8 +11533,8 @@ fn toc_keeps_direct_number_tabs() {
d.last_section.page_w = 220.0;
d.last_section.margin_left = 20.0;
d.last_section.margin_right = 20.0;
d.styles.get_mut("TOC2").unwrap().para.tabs = Some(vec![right]);
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
Arc::make_mut(&mut d.styles).get_mut("TOC2").unwrap().para.tabs = Some(vec![right]);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
{
let p = d.para_mut(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap();
p.props.style = Some("TOCHeading".into());
@@ -11724,11 +11836,11 @@ fn style_set_keeps_imported_styles_latent_styles_and_defaults() {
body.chr.size = Some(20.0);
body.chr.color = Some(TextColor::Rgb(Rgb(0xFF, 0, 0)));
let grid = Style { id: "GridTable4".into(), name: "Grid Table 4".into(), kind: StyleKind::Table, builtin: true, ..Default::default() };
s.doc.styles.styles.push(body.clone());
s.doc.styles.styles.push(grid.clone());
s.doc.styles.latent_styles = Some("<w:latentStyles/>".into());
s.doc.styles.default_chr.lang = Some("de-DE".into());
s.doc.styles.default_chr.size = Some(11.0);
Arc::make_mut(&mut s.doc.styles).styles.push(body.clone());
Arc::make_mut(&mut s.doc.styles).styles.push(grid.clone());
Arc::make_mut(&mut s.doc.styles).latent_styles = Some("<w:latentStyles/>".into());
Arc::make_mut(&mut s.doc.styles).default_chr.lang = Some("de-DE".into());
Arc::make_mut(&mut s.doc.styles).default_chr.size = Some(11.0);
run(&mut s, "design.styleSet", json!({ "name": name }));
assert_eq!(s.doc.styles.get("BodyText"), Some(&body), "{name}: BodyText");
assert_eq!(s.doc.styles.get("GridTable4"), Some(&grid), "{name}: GridTable4");
@@ -11798,8 +11910,8 @@ fn scale_font_sizes_reaches_char_table_and_cs_sizes_and_unset_default() {
use wordcraft_doc::props::CharProps;
use wordcraft_doc::{Style, StyleKind, styles::TablePart};
let mut s = s();
s.doc.styles.default_chr.size = None;
s.doc.styles.default_chr.size_cs = Some(10.0);
Arc::make_mut(&mut s.doc.styles).default_chr.size = None;
Arc::make_mut(&mut s.doc.styles).default_chr.size_cs = Some(10.0);
let mut ch = Style { id: "Big".into(), name: "Big".into(), kind: StyleKind::Character, ..Default::default() };
ch.chr.size = Some(20.0);
ch.chr.size_cs = Some(22.0);
@@ -11808,7 +11920,7 @@ fn scale_font_sizes_reaches_char_table_and_cs_sizes_and_unset_default() {
parts.header_chr.size = Some(14.0);
parts.regions.entry(TablePart::Band2Horz).or_default().chr.size = Some(8.0);
tb.table = Some(parts);
s.doc.styles.styles.extend([ch, tb]);
Arc::make_mut(&mut s.doc.styles).styles.extend([ch, tb]);
run(&mut s, "design.scaleFontSizes", json!({"delta": 2}));
let st = &s.doc.styles;
assert_eq!(st.default_chr.size, Some(13.0), "unset default is 11 pt, made explicit");
@@ -12057,7 +12169,7 @@ fn alt_chunks_open_in_place_as_word_brings_them_in() {
fn text_from_file_brings_the_files_pictures() {
let dir = scratch_dir("text-from-file-pictures");
let mut src = Session::new(wordcraft_doc::Document::new());
let list = src.doc.numbering.add_list(wordcraft_doc::numbering::ListKind::Bullet);
let list = Arc::make_mut(&mut src.doc.numbering).add_list(wordcraft_doc::numbering::ListKind::Bullet);
// A list item, then a paragraph (a single paragraph would take the caret's formatting).
src.doc.split_paragraph(&Pos::body(0, 0)).unwrap();
src.doc.para_mut(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap().props.numbering =
@@ -12072,7 +12184,7 @@ fn text_from_file_brings_the_files_pictures() {
let mut s = s();
// A picture and a list of our own first, so the file's key and list id would point at ours.
let ours = s.doc.add_media(vec![9, 9, 9], "png");
let our_list = s.doc.numbering.add_list(wordcraft_doc::numbering::ListKind::Numbered);
let our_list = Arc::make_mut(&mut s.doc.numbering).add_list(wordcraft_doc::numbering::ListKind::Numbered);
run(&mut s, "insert.textFromFile", json!({"path": path.to_string_lossy()}));
let _ = std::fs::remove_dir_all(&dir);
let shown: Vec<String> = s
+4 -3
View File
@@ -2,6 +2,7 @@
//! commands on Persian text, arrow keys in bidirectional text, DOCX save and open.
use serde_json::json;
use std::sync::Arc;
use wordcraft_doc::props::Align;
use wordcraft_doc::{Pos, StoryRef};
@@ -118,9 +119,9 @@ fn font_commands_override_inherited_complex_script_properties_explicitly() {
use wordcraft_doc::props::CharProps;
let mut s = session("שלום ABC", true);
s.doc.styles.default_chr.size_cs = Some(30.0);
s.doc.styles.default_chr.bold_cs = Some(true);
s.doc.styles.default_chr.italic_cs = Some(true);
Arc::make_mut(&mut s.doc.styles).default_chr.size_cs = Some(30.0);
Arc::make_mut(&mut s.doc.styles).default_chr.bold_cs = Some(true);
Arc::make_mut(&mut s.doc.styles).default_chr.italic_cs = Some(true);
run(&mut s, "select.all", json!({}));
run(&mut s, "format.bold", json!({"value": false}));
run(&mut s, "format.italic", json!({"value": false}));
+4 -3
View File
@@ -59,15 +59,16 @@ fn relative_font_sizes_resolve_styles_and_preserve_other_properties() {
use wordcraft_doc::styles::{Style, StyleKind};
let mut s = mixed();
s.doc.styles.default_chr.size_cs = Some(30.0);
s.doc.styles.upsert(Style {
let styles = std::sync::Arc::make_mut(&mut s.doc.styles);
styles.default_chr.size_cs = Some(30.0);
styles.upsert(Style {
id: "Large".into(),
name: "Large".into(),
based_on: Some("Normal".into()),
chr: CharProps { size: Some(20.0), bold: Some(true), ..Default::default() },
..Default::default()
});
s.doc.styles.upsert(Style {
styles.upsert(Style {
id: "Small".into(),
name: "Small".into(),
kind: StyleKind::Character,
+19 -18
View File
@@ -3,6 +3,7 @@
//! results are what Word produces for the same keys.
use serde_json::json;
use std::sync::Arc;
use wordcraft_doc::props::{BorderStyle, VertAlign};
use wordcraft_doc::{Pos, StoryRef};
@@ -483,7 +484,7 @@ fn autocorrect_replacement_list_follows_the_texts_language() {
assert_eq!(s.doc.plain_text(StoryRef::Body), "Ala i kot ", "document default from the editing language");
let mut s = self::s();
s.set_editing_language(Some("pl-PL"));
s.doc.styles.default_chr.lang = None;
Arc::make_mut(&mut s.doc.styles).default_chr.lang = None;
typ(&mut s, "Ala i kot ");
assert_eq!(s.doc.plain_text(StoryRef::Body), "Ala i kot ", "untagged text in the editing language");
// `eng` is English too, as for proofing.
@@ -1087,7 +1088,7 @@ fn tab_before_a_later_line_of_a_paragraph_indents_the_whole_paragraph() {
assert_eq!(indents(&s), (None, None));
// A list level past the last one, from a style or set directly, goes to the last level.
let mut s = style_inherited_list(wordcraft_doc::numbering::ListKind::Numbered, false);
let style = s.doc.styles.styles.iter_mut().find(|x| x.id == "ListBase").unwrap();
let style = Arc::make_mut(&mut s.doc.styles).styles.iter_mut().find(|x| x.id == "ListBase").unwrap();
style.para.numbering = style.para.numbering.map(|n| wordcraft_doc::props::NumRef { level: u8::MAX, ..n });
run(&mut s, "tools.autocorrect", json!({"tabIndent": true}));
run(&mut s, "caret.docStart", json!({}));
@@ -1212,8 +1213,8 @@ fn a_line_that_is_not_a_short_line_of_body_text_stays_as_it_is() {
// A list item numbered by the Normal style is not body text either.
let mut q = self::s();
run(&mut q, "tools.autocorrect", json!({"headingStyles": true}));
let num = q.doc.numbering.try_add_list(wordcraft_doc::ListKind::Bullet).unwrap();
if let Some(normal) = q.doc.styles.get_mut("Normal") {
let num = Arc::make_mut(&mut q.doc.numbering).try_add_list(wordcraft_doc::ListKind::Bullet).unwrap();
if let Some(normal) = Arc::make_mut(&mut q.doc.styles).get_mut("Normal") {
normal.para.numbering = Some(wordcraft_doc::props::NumRef { num, level: 0 });
}
typ(&mut q, "Item\n\n");
@@ -2162,7 +2163,7 @@ fn typing_rtl_letters_into_an_empty_paragraph_makes_it_rtl() {
assert_eq!(bidi(&s, 1), Some(true), "Enter carries the paragraph's direction on, as in Word");
// A style that sets left-to-right wins.
let mut doc = Session::new(wordcraft_doc::Document::new());
if let Some(st) = doc.doc.styles.get_mut("Normal") {
if let Some(st) = Arc::make_mut(&mut doc.doc.styles).get_mut("Normal") {
st.para.bidi = Some(false);
}
typ(&mut doc, "سلام");
@@ -3169,15 +3170,15 @@ fn style_inherited_list(kind: wordcraft_doc::numbering::ListKind, chain: bool) -
use wordcraft_doc::props::NumRef;
use wordcraft_doc::styles::Style;
let mut doc = wordcraft_doc::Document::from_text("Alpha\nBeta");
let num = doc.numbering.try_add_list(kind).unwrap();
doc.styles.styles.push(Style {
let num = Arc::make_mut(&mut doc.numbering).try_add_list(kind).unwrap();
Arc::make_mut(&mut doc.styles).styles.push(Style {
id: "ListBase".into(),
name: "Original List".into(),
para: wordcraft_doc::ParaProps { numbering: Some(NumRef { num, level: 0 }), ..Default::default() },
..Default::default()
});
if chain {
doc.styles.styles.push(Style {
Arc::make_mut(&mut doc.styles).styles.push(Style {
id: "ListChild".into(),
name: "Original Derived".into(),
based_on: Some("ListBase".into()),
@@ -3253,13 +3254,13 @@ fn list_toggle_uses_effective_level_and_override() {
for explicit_kind in [false, true] {
let mut s = style_inherited_list(kind, true);
let opposite = if kind == ListKind::Bullet { ListKind::Numbered } else { ListKind::Bullet };
let num = s.doc.styles.styles.iter_mut().find(|x| x.id == "ListBase").unwrap().para.numbering.as_mut().unwrap();
let num = Arc::make_mut(&mut s.doc.styles).styles.iter_mut().find(|x| x.id == "ListBase").unwrap().para.numbering.as_mut().unwrap();
num.level = 1;
let num_id = num.num;
s.doc.numbering.abstracts[0].levels[0] = levels_for(opposite)[0].clone();
Arc::make_mut(&mut s.doc.numbering).abstracts[0].levels[0] = levels_for(opposite)[0].clone();
if override_level {
s.doc.numbering.abstracts[0].levels[1] = levels_for(opposite)[1].clone();
s.doc.numbering.nums[0].level_overrides.push((1, levels_for(kind)[1].clone()));
Arc::make_mut(&mut s.doc.numbering).abstracts[0].levels[1] = levels_for(opposite)[1].clone();
Arc::make_mut(&mut s.doc.numbering).nums[0].level_overrides.push((1, levels_for(kind)[1].clone()));
}
let before = s.doc.clone();
let params = if explicit_kind { json!({"kind": if kind == ListKind::Bullet { "bullet" } else { "numbered" }}) } else { json!({}) };
@@ -3288,7 +3289,7 @@ fn list_paragraph_style_survives_inherited_toggle_off() {
use wordcraft_doc::styles::Style;
for kind in [ListKind::Bullet, ListKind::Numbered] {
let mut s = style_inherited_list(kind, false);
*s.doc.styles.styles.iter_mut().find(|x| x.id == "ListParagraph").unwrap() = Style {
*Arc::make_mut(&mut s.doc.styles).styles.iter_mut().find(|x| x.id == "ListParagraph").unwrap() = Style {
id: "ListParagraph".into(),
name: "Original formatted list".into(),
based_on: Some("ListBase".into()),
@@ -3343,7 +3344,7 @@ fn a_new_list_uses_the_requested_kind_after_a_mixed_format_list() {
for (kind, command) in [(ListKind::Bullet, "para.bullets"), (ListKind::Numbered, "para.numbering")] {
let mut s = style_inherited_list(kind, false);
let opposite = if kind == ListKind::Bullet { ListKind::Numbered } else { ListKind::Bullet };
s.doc.numbering.abstracts[0].levels[0] = levels_for(opposite)[0].clone();
Arc::make_mut(&mut s.doc.numbering).abstracts[0].levels[0] = levels_for(opposite)[0].clone();
let num = s.doc.numbering.nums[0].id;
s.doc.para_mut(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap().props.numbering = Some(NumRef { num, level: 1 });
s.doc.para_mut(StoryRef::Body, &wordcraft_doc::Path::top(1)).unwrap().props = ParaProps::default();
@@ -3399,7 +3400,7 @@ fn highlight_toggle_handles_mixed_inherited_and_pending_formatting() {
assert_eq!(s.run("format.state", &json!({})).unwrap()["highlight"], json!("0000FF"));
run(&mut s, "format.highlight", json!({}));
assert_eq!(s.run("format.state", &json!({})).unwrap()["highlight"], serde_json::Value::Null);
s.doc.styles.default_chr.highlight = Some(wordcraft_doc::props::Highlight::Blue);
Arc::make_mut(&mut s.doc.styles).default_chr.highlight = Some(wordcraft_doc::props::Highlight::Blue);
run(&mut s, "format.highlight", json!({"color":"blue"}));
run(&mut s, "format.highlight", json!({}));
assert!(s.doc.body[0].as_para().unwrap().runs.iter().all(|r| r.props.highlight == Some(wordcraft_doc::props::Highlight::None)));
@@ -3461,7 +3462,7 @@ fn highlight_toggle_checks_past_the_resolved_run_limit() {
.map(|i| Run { len: 1, props: CharProps { highlight: Some(Highlight::Blue), bold: Some(i % 2 == 0), ..Default::default() } })
.collect();
if inherited {
doc.styles.default_chr.highlight = Some(Highlight::Yellow);
Arc::make_mut(&mut doc.styles).default_chr.highlight = Some(Highlight::Yellow);
} else {
doc.para_mut(StoryRef::Body, &Pos::body(1, 0).path).unwrap().runs[0].props.highlight = Some(Highlight::Yellow);
}
@@ -3521,7 +3522,7 @@ fn highlight_toggle_checks_whole_cell_marks() {
Pos::new(StoryRef::Body, path, 0)
};
if inherited {
s.doc.styles.default_chr.highlight = Some(Highlight::Yellow);
Arc::make_mut(&mut s.doc.styles).default_chr.highlight = Some(Highlight::Yellow);
}
for row in 0..2 {
for col in 0..2 {
@@ -3573,7 +3574,7 @@ fn highlight_toggle_checks_ordinary_paragraph_marks() {
use wordcraft_doc::props::Highlight;
for text in ["AB", "AB\n\nAB", "\n"] {
let mut doc = wordcraft_doc::Document::from_text(text);
doc.styles.default_chr.highlight = Some(Highlight::Blue);
Arc::make_mut(&mut doc.styles).default_chr.highlight = Some(Highlight::Blue);
let paths = doc.para_paths(StoryRef::Body);
for path in &paths {
let p = doc.para_mut(StoryRef::Body, path).unwrap();
+2 -2
View File
@@ -723,7 +723,7 @@ impl<'a> Builder<'a> {
level
})
.collect();
let numbering = &mut self.doc.numbering;
let numbering = Arc::make_mut(&mut self.doc.numbering);
let aid = numbering.abstracts.iter().map(|a| a.id + 1).max().unwrap_or(0);
numbering.abstracts.push(AbstractNum { id: aid, name: None, levels, ..Default::default() });
let nid = numbering.nums.iter().map(|n| n.id + 1).max().unwrap_or(1).max(1);
@@ -949,7 +949,7 @@ pub fn to_doc_in(flow: &Flow, section: wordcraft_doc::SectionProps) -> Document
doc.numbering = Default::default();
let uses_code = uses_kind(&flow.blocks, Kind::Code, 0);
if uses_code {
doc.styles.upsert(code_style());
Arc::make_mut(&mut doc.styles).upsert(code_style());
}
let mut b = Builder::new(&mut doc, &flow.blocks);
let body = b.blocks(&flow.blocks, 0);
+12 -11
View File
@@ -3,6 +3,7 @@ use super::limits::{MAX_ENTRY, MAX_EXTRA_SPACES, MAX_SECTIONS};
use super::styles::{Styles, column_sep, columns_of};
use super::xml::{Attrs, Ev, events, get, length, local};
use super::{Columns, Cursor, Document, FBlock, Flow, HashMap, Meta, Para, Read, SectionProps, model, page_parts};
use std::sync::Arc;
use wordcraft_doc::odf_profile::OdfProfile;
pub(super) fn read_entry(z: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str) -> Option<Vec<u8>> {
@@ -208,7 +209,7 @@ fn custom_char_styles(blocks: &mut [FBlock], styles: &Styles, para_ids: &[String
fn add_char_styles(doc: &mut Document, found: &[(String, String)], defs: &[(String, Option<String>, model::Fmt, [bool; 4])]) {
for ((_, id), (name, parent, fmt, off)) in found.iter().zip(defs) {
let based_on = parent.as_ref().and_then(|p| found.iter().find(|(o, _)| o == p).map(|(_, id)| id.clone())).filter(|b| b != id);
doc.styles.upsert(wordcraft_doc::styles::Style {
Arc::make_mut(&mut doc.styles).upsert(wordcraft_doc::styles::Style {
id: id.clone(),
name: name.clone(),
kind: wordcraft_doc::styles::StyleKind::Character,
@@ -308,7 +309,7 @@ fn add_custom_styles(doc: &mut Document, named: &[super::styles::NamedStyle], od
chr: turned_off(model::char_props(&look.fmt), def.off),
..Default::default()
};
doc.styles.upsert(style);
Arc::make_mut(&mut doc.styles).upsert(style);
}
}
@@ -343,7 +344,7 @@ fn apply_look(doc: &mut Document, kind: model::Kind, look: &super::styles::ParaL
model::Kind::Code => model::CODE_STYLE.into(),
model::Kind::Normal | model::Kind::Rule => return,
};
let Some(st) = doc.styles.get_mut(&id) else { return };
let Some(st) = Arc::make_mut(&mut doc.styles).get_mut(&id) else { return };
st.chr = model::char_props(&look.fmt);
let sp = &look.spacing;
let p = &mut st.para;
@@ -390,13 +391,13 @@ fn number_headings(doc: &mut Document, levels: &[super::styles::ListLevel]) {
level.style = Some(format!("Heading{}", i + 1));
}
}
let numbering = &mut doc.numbering;
let numbering = Arc::make_mut(&mut doc.numbering);
let aid = numbering.abstracts.iter().map(|a| a.id + 1).max().unwrap_or(0);
numbering.abstracts.push(AbstractNum { id: aid, name: None, levels: out, ..Default::default() });
let nid = numbering.nums.iter().map(|n| n.id + 1).max().unwrap_or(1).max(1);
numbering.nums.push(Num { id: nid, abstract_id: aid, ..Default::default() });
for (i, on) in numbered.iter().enumerate() {
if *on && let Some(st) = doc.styles.get_mut(&format!("Heading{}", i + 1)) {
if *on && let Some(st) = Arc::make_mut(&mut doc.styles).get_mut(&format!("Heading{}", i + 1)) {
st.para.numbering = Some(NumRef { num: nid, level: i as u8 });
}
}
@@ -938,16 +939,16 @@ pub fn import_with(bytes: &[u8], page: &SectionProps, profile: &OdfProfile) -> R
doc.sources = sources;
doc.settings.auto_hyphenation |= hyphenate;
if font.is_some() {
doc.styles.default_chr.font = font;
Arc::make_mut(&mut doc.styles).default_chr.font = font;
}
if size.is_some() {
doc.styles.default_chr.size = size;
Arc::make_mut(&mut doc.styles).default_chr.size = size;
}
if let Some(c) = color {
doc.styles.default_chr.color = Some(wordcraft_doc::props::TextColor::Rgb(c));
Arc::make_mut(&mut doc.styles).default_chr.color = Some(wordcraft_doc::props::TextColor::Rgb(c));
}
doc.styles.default_chr.kern = kern.then_some(1.0);
doc.styles.default_chr.overlay(&scripts);
Arc::make_mut(&mut doc.styles).default_chr.kern = kern.then_some(1.0);
Arc::make_mut(&mut doc.styles).default_chr.overlay(&scripts);
if let Some(t) = default_tab {
doc.settings.default_tab = t;
}
@@ -968,7 +969,7 @@ pub fn import_with(bytes: &[u8], page: &SectionProps, profile: &OdfProfile) -> R
if let Some((fmt, props)) = page_doc.notes[1] {
(doc.settings.endnote_format, doc.settings.endnote_pr) = (fmt, props);
}
let d = &mut doc.styles.default_para;
let d = &mut Arc::make_mut(&mut doc.styles).default_para;
(d.space_before, d.space_after, d.line_spacing) = (spacing.before, spacing.after, spacing.line);
(d.indent_left, d.indent_right, d.indent_first) = (spacing.left, spacing.right, spacing.first);
// Unstated, as the profile says (`widowControl`).
+2 -2
View File
@@ -71,7 +71,7 @@ fn a_style_edited_in_wordcraft_is_written_as_edited() {
);
let mut d = import(&package(&content, Some(&styles))).unwrap();
let id = d.body[0].as_para().unwrap().props.style.clone().unwrap();
if let Some(st) = d.styles.styles.iter_mut().find(|s| s.id == id) {
if let Some(st) = std::sync::Arc::make_mut(&mut d.styles).styles.iter_mut().find(|s| s.id == id) {
st.chr.size = Some(20.0);
}
let saved = export(&d).unwrap();
@@ -129,7 +129,7 @@ fn unedited_style_definitions_are_written_as_read() {
assert_eq!(s.matches("<text:notes-configuration text:note-class=\"footnote\"").count(), 1, "{s}");
assert_eq!(s.matches(r#"<style:style style:name="Standard""#).count(), 1, "{s}");
// A style changed in WordCraft: the definitions are WordCraft's.
if let Some(st) = d.styles.styles.iter_mut().find(|s| s.id == "Normal") {
if let Some(st) = std::sync::Arc::make_mut(&mut d.styles).styles.iter_mut().find(|s| s.id == "Normal") {
st.chr.bold = Some(true);
}
let s = styles_of(&export(&d).unwrap());
@@ -400,7 +400,7 @@ fn edits_leave_kept_markup_well_formed() {
}
}
let mut styled = d.clone();
if let Some(st) = styled.styles.styles.iter_mut().find(|s| s.id == "Normal") {
if let Some(st) = std::sync::Arc::make_mut(&mut styled.styles).styles.iter_mut().find(|s| s.id == "Normal") {
st.chr.italic = Some(true);
}
edits.push(styled);
+1 -1
View File
@@ -164,7 +164,7 @@ fn the_list_paragraph_style_is_written() {
p.runs = vec![wordcraft_doc::para::Run { len: 4, props: Default::default() }];
p.props = ParaProps { style: Some("ListParagraph".into()), ..Default::default() };
d.body = vec![para_block(p)];
if let Some(st) = d.styles.get_mut("ListParagraph") {
if let Some(st) = std::sync::Arc::make_mut(&mut d.styles).get_mut("ListParagraph") {
st.para.contextual_spacing = Some(true);
st.para.space_after = Some(3.0);
}
+3 -2
View File
@@ -1,6 +1,7 @@
use super::reader::Reader;
use super::state::{Dest, HeaderFooter, HfPages, MAX_PICT, PageGeom};
use super::{Cell, Document, FBlock, Flow, Para, model};
use std::sync::Arc;
/// Parse RTF bytes into the flow model.
pub fn parse(bytes: &[u8]) -> Result<Flow, String> {
@@ -305,12 +306,12 @@ pub fn import(bytes: &[u8]) -> Result<Document, String> {
}
// RTF's paragraph defaults are single spacing and no space around paragraphs; what a file
// wants beyond that it states on each paragraph.
let dp = &mut doc.styles.default_para;
let dp = &mut Arc::make_mut(&mut doc.styles).default_para;
dp.space_before = Some(0.0);
dp.space_after = Some(0.0);
dp.line_spacing = Some(wordcraft_doc::props::LineSpacing::Multiple(1.0));
if default_font.is_some() {
doc.styles.default_chr.font = default_font;
Arc::make_mut(&mut doc.styles).default_chr.font = default_font;
}
Ok(doc)
}
+6 -6
View File
@@ -38,8 +38,8 @@ fn sample() -> Document {
p.insert_text(n, ".", &CharProps::default()).unwrap();
blocks.push(para_block(p));
blocks.push(para_block(Paragraph::with_text("Second Level", CharProps::default()).styled("Heading2")));
let bul = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let bul = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
for (t, nm, lvl) in [("first bullet", bul, 0u8), ("second bullet", bul, 0), ("nested bullet", bul, 1)] {
let mut q = Paragraph::with_text(t, CharProps::default()).styled("ListParagraph");
q.props.numbering = Some(wordcraft_doc::props::NumRef { num: nm, level: lvl });
@@ -166,8 +166,8 @@ fn check_round_trip(ext: &str, rich: bool) {
/// Two bullets directly followed by two numbered items.
fn bullets_then_numbers() -> Document {
let mut d = Document::new();
let bul = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let bul = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let mut blocks = Vec::new();
for (t, nm) in [("dot one", bul), ("dot two", bul), ("num one", num), ("num two", num)] {
let mut q = Paragraph::with_text(t, CharProps::default()).styled("ListParagraph");
@@ -198,7 +198,7 @@ fn numbered_list_after_bullets_keeps_its_kind() {
fn markdown_list_items_that_start_like_blocks_keep_their_text() {
for text in ["---", "--", "1. Intro", "# Agenda", "ordinary item"] {
let mut d = Document::new();
let bul = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let bul = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
let mut p = Paragraph::with_text(text, CharProps::default()).styled("ListParagraph");
p.props.numbering = Some(wordcraft_doc::props::NumRef { num: bul, level: 0 });
d.body = vec![para_block(p)];
@@ -368,7 +368,7 @@ fn txt_export_writes_symbol_font_text_as_unicode() {
#[test]
fn txt_export_keeps_list_labels() {
let mut d = Document::new();
let bul = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let bul = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
let mut p = Paragraph::with_text("alpha", CharProps::default());
p.props.numbering = Some(wordcraft_doc::props::NumRef { num: bul, level: 0 });
d.body = vec![para_block(p), para_block(Paragraph::with_text("plain", CharProps::default()))];
+57 -57
View File
@@ -30,8 +30,8 @@ fn lay(doc: &Document) -> DocLayout {
/// Keep their explicit spacing local; ordinary document/layout defaults remain unchanged.
pub(super) fn pagination_fixture_document(text: &str) -> Document {
let mut doc = Document::from_text(text);
doc.styles.default_chr.font = Some(WRAP_TEST_FONT.into());
doc.styles.default_chr.size = Some(12.0);
Arc::make_mut(&mut doc.styles).default_chr.font = Some(WRAP_TEST_FONT.into());
Arc::make_mut(&mut doc.styles).default_chr.size = Some(12.0);
for block in &mut doc.body {
if let Block::Para(para) = Arc::make_mut(block) {
para.props.line_spacing = Some(wordcraft_doc::props::LineSpacing::Exactly(14.0));
@@ -299,7 +299,7 @@ fn contextual_gap(after: f32, before: f32, first: bool, second: bool, different_
use wordcraft_doc::props::LineSpacing;
let mut d = Document::new();
for id in ["SpacingA", "SpacingB"] {
d.styles.upsert(wordcraft_doc::Style { id: id.into(), name: id.into(), ..Default::default() });
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style { id: id.into(), name: id.into(), ..Default::default() });
}
let chr = wordcraft_doc::CharProps { font: Some("Source Sans 3".into()), size: Some(12.0), ..Default::default() };
let props = ParaProps {
@@ -375,7 +375,7 @@ fn contextual_spacing_removes_every_gap_in_a_four_paragraph_run() {
use wordcraft_doc::props::LineSpacing;
for table in [false, true] {
let mut doc = Document::new();
doc.styles.upsert(wordcraft_doc::Style { id: "SpacingA".into(), name: "SpacingA".into(), ..Default::default() });
Arc::make_mut(&mut doc.styles).upsert(wordcraft_doc::Style { id: "SpacingA".into(), name: "SpacingA".into(), ..Default::default() });
let paragraphs = ["One", "Two", "Three", "Four"]
.into_iter()
.map(|text| {
@@ -424,7 +424,7 @@ fn contextual_automatic_lists_do_not_subtract_the_gap_twice() {
use wordcraft_doc::props::{LineSpacing, NumRef};
for table in [false, true] {
let mut doc = Document::new();
let num = doc.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut doc.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let paragraphs = ["First", "Second", "Third"]
.into_iter()
.map(|text| {
@@ -698,7 +698,7 @@ fn deleted_text_adds_no_line_break_opportunity_without_markup() {
// "xx ab\u{ad}cdef" in a column where "xx ab-" and "abcdef" fit but "xx abcdef" does not; only
// the soft hyphen (5..7) is track-deleted.
let mut d = Document::from_text("xx ab\u{ad}cdef");
d.styles.default_chr.font = Some(WRAP_TEST_FONT.into());
Arc::make_mut(&mut d.styles).default_chr.font = Some(WRAP_TEST_FONT.into());
d.format_range(&Pos::body(0, 5), &Pos::body(0, 7), &|c| c.del = Some(0)).unwrap();
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.indent_right = Some(468.0 - 40.0)).unwrap();
let line_starts = |hide_deleted: bool| {
@@ -735,7 +735,7 @@ fn hidden_text_adds_no_line_break_opportunity_when_not_shown() {
// but "xx abcdef" does not. Measured in Word (hidden text not printed): "xx" / "abcdef" both times.
for hidden in ["\u{ad}", " "] {
let mut d = Document::from_text(&format!("xx ab{hidden}cdef"));
d.styles.default_chr.font = Some(WRAP_TEST_FONT.into());
Arc::make_mut(&mut d.styles).default_chr.font = Some(WRAP_TEST_FONT.into());
let end = 5 + hidden.len();
d.format_range(&Pos::body(0, 5), &Pos::body(0, end), &|c| c.hidden = Some(true)).unwrap();
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.indent_right = Some(468.0 - 40.0)).unwrap();
@@ -1749,7 +1749,7 @@ fn a_bookmark_is_no_line_break_opportunity() {
#[test]
fn lists_get_labels_and_indent() {
let mut d = Document::from_text("one\ntwo\nthree");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(2, 5), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap();
let l = lay(&d);
let labels: Vec<String> = l.pages[0]
@@ -1767,7 +1767,7 @@ fn break_only_list_has_no_label() {
use wordcraft_doc::ListKind;
for kind in [ListKind::Numbered, ListKind::Bullet] {
let mut d = Document::from_text("First\n\u{000C}\nLast\n\nAfter");
let id = d.numbering.add_list(kind);
let id = Arc::make_mut(&mut d.numbering).add_list(kind);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(4, 5), &|p| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 });
})
@@ -1803,7 +1803,7 @@ fn empty_list_paragraph_with_a_hidden_mark_counts_but_shows_nothing() {
None => "Alpha\nBeta\nGamma".to_string(),
};
let mut d = Document::from_text(&text);
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let last = d.body.len() - 1;
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(last, 5), &|p| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 });
@@ -1855,7 +1855,7 @@ fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() {
// "Before", then a numbered paragraph whose text opens with a page break: Word draws its
// number on the next page with the text, not alone at the bottom of the first.
let mut d = Document::from_text("Before\n\u{000C}Heading");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 1), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap();
let l = lay(&d);
let label_pages: Vec<usize> = l
@@ -1875,8 +1875,8 @@ fn russian_letter_list_labels_draw_cyrillic() {
// russianLower labelled items 1, 2, 3 instead of а), б), в).
use wordcraft_doc::section::NumFormat;
let mut d = Document::from_text("один\nдва\nтри");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.first_mut() {
l.format = NumFormat::RussianLower;
l.text = "%1)".into();
@@ -1906,8 +1906,8 @@ fn chinese_counting_list_labels_and_their_font_runs() {
// Issue #534: a "一、" list showed digits, drawn with glyph ids of another font.
use wordcraft_doc::section::NumFormat;
let mut d = Document::from_text("甲\n乙\n丙");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.first_mut() {
l.format = NumFormat::ChineseCounting;
l.text = "%1、".into();
@@ -1939,7 +1939,7 @@ fn chinese_counting_list_labels_and_their_font_runs() {
let got = labels(&d);
assert_eq!(got.iter().map(|(t, _)| t.as_str()).collect::<Vec<_>>(), ["一、", "二、", "三、"]);
// A decimal label next to a CJK comma mixes faces when the Latin face lacks the comma.
for a in &mut d.numbering.abstracts {
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.first_mut() {
l.format = NumFormat::Decimal;
}
@@ -1954,8 +1954,8 @@ fn chinese_counting_list_labels_and_their_font_runs() {
fn symbol_font_bullet_without_the_font_draws_a_bullet() {
// Word's default bullet: U+F0B7 in Symbol.
let mut d = Document::from_text("one");
let id = d.numbering.add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.first_mut() {
l.chr.font = Some("Symbol".into());
}
@@ -1973,8 +1973,8 @@ fn symbol_font_bullet_without_the_font_draws_a_bullet() {
fn bullet_first_line(label_font: Option<&str>) -> (f32, f32, f32, f32) {
let mut d = Document::from_text("one");
if let Some(label_font) = label_font {
let id = d.numbering.add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.first_mut() {
l.chr.font = Some(label_font.into());
}
@@ -2028,8 +2028,8 @@ fn private_use_bullet_no_font_draws_shows_the_levels_default_bullet() {
// #1683: a level-2 bullet stored as a private-use code in a font that isn't installed was
// drawn from an installed icon font (U+F081 is the Twitter logo in Font Awesome).
let mut d = Document::from_text("one");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
if let Some(l) = a.levels.get_mut(1) {
l.text = "\u{F081}".into();
l.chr.font = Some("WordCraft Missing Font 1683".into());
@@ -3509,7 +3509,7 @@ fn hidden_float_leaves_no_wrap_area_when_hidden_text_is_not_shown() {
// resolved formatting says so). Measured in Word: not printed, and the text runs full width.
let text = "Words flow around the picture here. ".repeat(30);
let mut d = Document::from_text(&text);
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "Secret".into(),
name: "Secret".into(),
kind: wordcraft_doc::StyleKind::Character,
@@ -4488,11 +4488,11 @@ fn row_bands_shade_only_with_a_band_size() {
};
let mut d = styled_table_doc(Some("GridTable4AccentBlue"), 5);
assert_eq!(shaded_rows(&d), vec![1, 3], "Grid Table 4 bands one row at a time");
if let Some(t) = d.styles.get_mut("GridTable4AccentBlue").and_then(|s| s.table.as_mut()) {
if let Some(t) = Arc::make_mut(&mut d.styles).get_mut("GridTable4AccentBlue").and_then(|s| s.table.as_mut()) {
t.band_size = None;
}
assert_eq!(shaded_rows(&d), Vec::<usize>::new(), "no band size, no bands");
if let Some(t) = d.styles.get_mut("GridTable4AccentBlue").and_then(|s| s.table.as_mut()) {
if let Some(t) = Arc::make_mut(&mut d.styles).get_mut("GridTable4AccentBlue").and_then(|s| s.table.as_mut()) {
t.band_size = Some(2);
}
assert_eq!(shaded_rows(&d), vec![1, 2], "bands of two rows");
@@ -4503,7 +4503,7 @@ fn row_bands_shade_only_with_a_band_size() {
#[test]
fn paragraph_style_and_direct_formatting_beat_table_style() {
let mut d = styled_table_doc(Some("TableGrid"), 3);
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "Spaced".into(),
name: "Spaced".into(),
based_on: Some("Normal".into()),
@@ -4529,7 +4529,7 @@ fn paragraph_style_and_direct_formatting_beat_table_style() {
assert_eq!(cell_para(&l, 1, 0).rp.space_after, 3.0, "direct formatting");
assert_eq!(cell_para(&l, 2, 0).rp.space_after, 0.0, "table style");
// Normal setting its own spacing wins too, even where it equals nothing in the defaults.
if let Some(n) = d.styles.get_mut("Normal") {
if let Some(n) = Arc::make_mut(&mut d.styles).get_mut("Normal") {
n.para.space_after = Some(10.0);
}
assert_eq!(cell_para(&lay(&d), 2, 0).rp.space_after, 10.0, "Normal");
@@ -4542,7 +4542,7 @@ fn derived_table_style_merges_its_base() {
use wordcraft_doc::styles::TableStyleParts;
let red = wordcraft_doc::Rgb(0xC0, 0, 0);
let mut d = styled_table_doc(Some("RedGrid"), 3);
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "RedGrid".into(),
name: "Red Grid".into(),
kind: wordcraft_doc::StyleKind::Table,
@@ -5168,8 +5168,8 @@ fn list_label_does_not_inflate_an_inline_picture_floor() {
use wordcraft_doc::props::{LineSpacing, NumRef};
for spacing in [LineSpacing::Multiple(1.25), LineSpacing::AtLeast(12.0)] {
let mut d = Document::from_text("");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
d.numbering.abstracts[0].levels[0].chr.size = Some(24.0);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
Arc::make_mut(&mut d.numbering).abstracts[0].levels[0].chr.size = Some(24.0);
let p = d.para_mut(StoryRef::Body, &Path::top(0)).unwrap();
p.props.line_spacing = Some(spacing);
p.props.numbering = Some(NumRef { num: id, level: 0 });
@@ -6036,7 +6036,7 @@ fn labels(l: &DocLayout) -> Vec<String> {
}
fn numbered(d: &mut Document, texts: &[&str], props: ParaProps) -> Vec<wordcraft_doc::Paragraph> {
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
texts
.iter()
.map(|t| {
@@ -7899,7 +7899,7 @@ fn tight_wrap_follows_an_ellipse() {
use wordcraft_doc::para::{Float, ShapeKind, Wrap};
let spans = |wrap| {
let mut d = wrapped_shape_doc(ShapeKind::Ellipse, 160.0, 160.0, Float { wrap, ..Default::default() });
d.styles.default_chr.font = Some(WRAP_TEST_FONT.into());
Arc::make_mut(&mut d.styles).default_chr.font = Some(WRAP_TEST_FONT.into());
line_spans(&lay(&d))
};
let (square, tight, through) = (spans(Wrap::Square), spans(Wrap::Tight), spans(Wrap::Through));
@@ -8619,7 +8619,7 @@ fn list_item_first_word_gets_no_capital_squiggle() {
l.pages[0].items.iter().map(|i| if let Placed::Lines { para, .. } = i { para.issues.iter().filter(|x| x.2).count() } else { 0 }).sum()
};
let mut d = Document::from_text("item one");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
assert_eq!(grammar_squiggles(&d), 1, "plain paragraph");
// Direct numbering.
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.numbering = Some(NumRef { num: id, level: 0 })).unwrap();
@@ -8630,7 +8630,7 @@ fn list_item_first_word_gets_no_capital_squiggle() {
p.style = Some("Bullets".into());
})
.unwrap();
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "Bullets".into(),
name: "Bullets".into(),
kind: wordcraft_doc::StyleKind::Paragraph,
@@ -8657,11 +8657,11 @@ fn unproofed_languages_and_no_proof_runs_are_not_marked() {
let text = "Nous écrivons beaucoup aujourd'hui.";
let mut d = Document::from_text(text);
assert!(issues(&d) > 0, "French words are English misspellings in an English document");
d.styles.default_chr.lang = Some("fr-FR".into());
Arc::make_mut(&mut d.styles).default_chr.lang = Some("fr-FR".into());
assert_eq!(issues(&d), 0, "no French dictionary: no marks");
d.styles.default_chr.lang = Some("pl-PL".into());
Arc::make_mut(&mut d.styles).default_chr.lang = Some("pl-PL".into());
let mut pl = Document::from_text("Płynie żeka.");
pl.styles.default_chr.lang = Some("pl-PL".into());
Arc::make_mut(&mut pl.styles).default_chr.lang = Some("pl-PL".into());
assert_eq!(issues(&pl), 1);
pl.format_range(&Pos::body(0, 0), &Pos::body(0, "Płynie żeka.".len()), &|c| c.no_proof = Some(true)).unwrap();
assert_eq!(issues(&pl), 0);
@@ -8932,8 +8932,8 @@ fn spaces_and_tabs_do_not_make_a_line_taller() {
fn a_list_label_adds_ascent_but_not_descent() {
let first_line = |label_size: Option<f32>| {
let mut d = Document::new();
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut d.numbering.abstracts {
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
for l in &mut a.levels {
l.chr.size = label_size;
}
@@ -9070,7 +9070,7 @@ fn list_indents_and_style_indents_combine_like_word() {
use wordcraft_doc::styles::{Style, StyleKind};
let text_x = |style: &str, own: ParaProps| {
let mut d = Document::new();
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let style_of = |id: &str, based: Option<&str>, para: ParaProps| Style {
id: id.into(),
name: id.into(),
@@ -9082,11 +9082,11 @@ fn list_indents_and_style_indents_combine_like_word() {
let list = ParaProps { numbering: Some(NumRef { num, level: 0 }), ..Default::default() };
let ind = ParaProps { indent_left: Some(64.8), ..Default::default() };
// List and indent in one style; list in the base, indent in the derived one; the reverse.
d.styles.upsert(style_of("Both", None, ParaProps { indent_left: Some(64.8), ..list.clone() }));
d.styles.upsert(style_of("ListBase", None, list.clone()));
d.styles.upsert(style_of("IndentOnList", Some("ListBase"), ind.clone()));
d.styles.upsert(style_of("IndentBase", None, ind));
d.styles.upsert(style_of("ListOnIndent", Some("IndentBase"), list.clone()));
Arc::make_mut(&mut d.styles).upsert(style_of("Both", None, ParaProps { indent_left: Some(64.8), ..list.clone() }));
Arc::make_mut(&mut d.styles).upsert(style_of("ListBase", None, list.clone()));
Arc::make_mut(&mut d.styles).upsert(style_of("IndentOnList", Some("ListBase"), ind.clone()));
Arc::make_mut(&mut d.styles).upsert(style_of("IndentBase", None, ind));
Arc::make_mut(&mut d.styles).upsert(style_of("ListOnIndent", Some("IndentBase"), list.clone()));
let own =
ParaProps { style: Some(style.into()), numbering: own.numbering.map(|n| NumRef { num: if n.num == 0 { 0 } else { num }, ..n }), ..own };
let mut p = wordcraft_doc::Paragraph::with_text("Text", Default::default());
@@ -9333,7 +9333,7 @@ fn section_mark_hidden_styles() {
let position = |kind: Option<StyleKind>, show_hidden| {
let mut doc = Document::from_text("\nAfter marker");
doc.styles.upsert(Style {
Arc::make_mut(&mut doc.styles).upsert(Style {
id: "HiddenMark".into(),
name: "Hidden mark".into(),
kind: kind.unwrap_or(StyleKind::Character),
@@ -9648,7 +9648,7 @@ fn kerning_follows_kerning_for_fonts_like_word() {
// A new document kerns from 1 pt, like Word's Normal template.
assert!((width(None) - width(Some(1.0))).abs() < 0.01, "new documents kern");
let mut d = Document::new();
d.styles.default_chr.kern = None;
Arc::make_mut(&mut d.styles).default_chr.kern = None;
assert!(d.styles.resolve_char(None, &Default::default()).kern == 0.0, "no Kerning for fonts: not kerned");
}
@@ -9664,7 +9664,7 @@ fn ligatures_follow_w14_ligatures() {
assert_eq!(wordcraft_fonts::shape(&face, "fi", &[], |c| c).len(), 1, "the test font draws fi as one glyph");
let glyphs = |style: Option<Ligatures>, run: Option<Ligatures>| {
let mut d = Document::new();
d.styles.upsert(Style {
Arc::make_mut(&mut d.styles).upsert(Style {
id: "Plain".into(),
name: "Plain".into(),
kind: StyleKind::Character,
@@ -9804,8 +9804,8 @@ fn automatic_spacing_drops_between_list_items() {
let plain = ParaProps { space_before: Some(0.0), space_after: Some(0.0), ..Default::default() };
let tops = |items: [ParaProps; 3], in_cell: bool| {
let mut d = Document::new();
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let other = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let other = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let fix = |p: ParaProps| ParaProps {
numbering: p.numbering.map(|n| NumRef {
num: match n.num {
@@ -10305,7 +10305,7 @@ fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() {
use wordcraft_doc::props::NumRef;
space_after_clear_with(
|d, t| {
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let p = para_props(d, t);
p.numbering = Some(NumRef { num, level: 0 });
p.space_after_auto = Some(true);
@@ -11006,7 +11006,7 @@ fn row_keep_doc() -> Document {
d.last_section.margin_bottom = 36.0;
d.last_section.margin_left = 36.0;
d.last_section.margin_right = 36.0;
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "KeptRow".into(),
name: "Kept row".into(),
para: ParaProps { keep_next: Some(true), keep_lines: Some(true), ..Default::default() },
@@ -12413,8 +12413,8 @@ fn keep_next_counts_list_indent() {
doc.last_section.margin_bottom = 20.0;
doc.last_section.margin_left = 20.0;
doc.last_section.margin_right = 20.0;
let id = doc.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let level = &mut doc.numbering.abstracts[0].levels[0];
let id = Arc::make_mut(&mut doc.numbering).add_list(wordcraft_doc::ListKind::Numbered);
let level = &mut Arc::make_mut(&mut doc.numbering).abstracts[0].levels[0];
level.indent = 100.0;
level.hanging = 0.0;
let props = ParaProps {
@@ -12841,7 +12841,7 @@ fn stacked_marks_past_the_bottom_draw_no_label() {
use wordcraft_doc::section::{SectionProps, SectionStart};
let marks = 100;
let mut d = Document::from_text(&format!("Start\n{}End", "\n".repeat(marks)));
let id = d.numbering.add_list(ListKind::Bullet);
let id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Bullet);
for i in 1..=marks {
let p = d.para_mut(StoryRef::Body, &Path::top(i)).unwrap();
p.section = Some(Box::new(SectionProps { start: SectionStart::Continuous, ..Default::default() }));
@@ -12904,7 +12904,7 @@ fn an_empty_bulleted_mark_after_a_bulleted_section_closer_draws_no_bullet() {
use wordcraft_doc::section::{SectionProps, SectionStart};
for filler in (40..140).step_by(3) {
let mut d = Document::from_text(&format!("{}file this form\n\nFailure to comply", "Instructions\n".repeat(filler)));
let id = d.numbering.add_list(ListKind::Bullet);
let id = Arc::make_mut(&mut d.numbering).add_list(ListKind::Bullet);
let mut two = SectionProps { start: SectionStart::Continuous, ..Default::default() };
two.columns.count = 2;
for i in [filler, filler + 1] {
@@ -13029,7 +13029,7 @@ fn a_list_item_that_shows_nothing_reports_no_missing_font() {
// Its number is shaped in its mark's font before the paragraph turns out to show nothing
// (#3982): that font isn't shown in another, so it isn't missing from what's drawn.
let mut d = Document::from_text("Alpha\n\nBeta");
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let id = std::sync::Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(2, 4), &|p| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 });
})
+5 -5
View File
@@ -215,7 +215,7 @@ fn rtl_indents_and_alignment_are_measured_from_the_right() {
#[test]
fn rtl_list_label_sits_on_the_right() {
let mut d = doc_of(&[("مورد اول", true), ("مورد دوم", true)]);
let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Numbered);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p: &mut ParaProps| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })
})
@@ -534,7 +534,7 @@ fn list_label_moves_with_its_text_when_the_paragraph_is_aligned() {
// margin. In Word the label stays attached to the start of the text, in both directions.
for (rtl, text) in [(false, "alpha beta"), (true, "مورد اول")] {
let mut d = doc_of(&[(text, rtl), (text, rtl), (text, rtl), (text, rtl)]);
let id = d.numbering.add_list(wordcraft_doc::ListKind::Bullet);
let id = Arc::make_mut(&mut d.numbering).add_list(wordcraft_doc::ListKind::Bullet);
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(3, 1), &|p: &mut ParaProps| {
p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })
})
@@ -576,7 +576,7 @@ fn list_label_moves_with_its_text_when_the_paragraph_is_aligned() {
fn rtl_list_label_mirrors_its_brackets() {
let mut d = doc_of(&[("סעיף", true)]);
let lvl = wordcraft_doc::numbering::Level { text: "(%1)".into(), ..Default::default() };
let num = d.numbering.try_add_levels(None, vec![lvl]).unwrap();
let num = Arc::make_mut(&mut d.numbering).try_add_levels(None, vec![lvl]).unwrap();
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p: &mut ParaProps| p.numbering = Some(wordcraft_doc::props::NumRef { num, level: 0 }))
.unwrap();
let l = lay(&d);
@@ -603,8 +603,8 @@ fn list_label_after_leading_page_break_uses_its_text_lines_alignment() {
let make = |page_break: bool| {
let contents = if page_break { format!("\u{000C}{text}") } else { text.clone() };
let mut d = doc_of(&[("Before", false), (&contents, rtl)]);
let id = d.numbering.add_list(kind);
for a in &mut d.numbering.abstracts {
let id = Arc::make_mut(&mut d.numbering).add_list(kind);
for a in &mut Arc::make_mut(&mut d.numbering).abstracts {
let level = a.levels.first_mut().unwrap();
level.start = 2;
level.indent = 14.2;
+1 -1
View File
@@ -483,7 +483,7 @@ fn widow_control_moves_a_three_line_paragraph_whole() {
for (fill, shift) in (30usize..60).flat_map(|f| (0..15).step_by(2).map(move |s| (f, s))) {
let filler = "Filler line.\n".repeat(fill);
let mut d = Document::from_text(&format!("{filler}{}\nAfter.", "word ".repeat(words)));
d.styles.default_chr.font = Some("Source Sans 3".into());
Arc::make_mut(&mut d.styles).default_chr.font = Some("Source Sans 3".into());
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.space_before = Some(shift as f32)).unwrap();
let l = lay(&d);
let pieces: Vec<(usize, usize)> = l
+3 -3
View File
@@ -88,7 +88,7 @@ fn character_styles_and_paragraph_numbers() {
// Bold a word on page 2 with the Strong character style.
d.format_range(&Pos::body(3, 8), &Pos::body(3, 11), &|c: &mut CharProps| c.style = Some("Strong".into())).unwrap();
// Number the headings 1., 2., 3. and the section under them 3.1 (full context).
let num = d.numbering.add_list(ListKind::Numbered);
let num = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
for (i, lvl) in [(0, 0u8), (2, 0), (5, 0), (6, 1)] {
d.para_mut(StoryRef::Body, &Path::top(i)).unwrap().props.numbering = Some(NumRef { num, level: lvl });
}
@@ -133,7 +133,7 @@ fn style_index_is_reused_until_the_body_or_styles_change() {
assert!(after[0].iter().any(|t| t.contains("Openings")), "{after:?}");
// A style change rebuilds it too.
let second = index(&c);
if let Some(st) = d.styles.styles.iter_mut().find(|s| s.id == "Heading1") {
if let Some(st) = Arc::make_mut(&mut d.styles).styles.iter_mut().find(|s| s.id == "Heading1") {
st.name = "Chapter Title".into();
}
let _ = pages(&d, &mut c);
@@ -145,7 +145,7 @@ fn style_index_is_reused_until_the_body_or_styles_change() {
fn heading_level_matches_a_localized_style_id() {
let paras: Vec<(&str, &str, bool)> = CHAPTERS.iter().map(|(s, t, b)| (if *s == "Heading1" { "berschrift1" } else { *s }, *t, *b)).collect();
let mut d = doc(&paras, &["STYLEREF 1"]);
let st = d.styles.styles.iter_mut().find(|s| s.id == "Heading1").unwrap();
let st = Arc::make_mut(&mut d.styles).styles.iter_mut().find(|s| s.id == "Heading1").unwrap();
st.id = "berschrift1".into();
st.name = "heading 1".into();
let h = headers(&d);
+7 -7
View File
@@ -78,7 +78,7 @@ fn sample() -> Document {
p.insert_text(n, " back to top", &CharProps { link: Some("#intro".into()), italic: Some(true), ..Default::default() }).unwrap();
b.push(para_block(p));
b.push(para_block(Paragraph::with_text("Details", CharProps::default()).styled("Heading2")));
let list = d.numbering.add_list(ListKind::Numbered);
let list = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
for t in ["alpha item", "beta item"] {
let mut q = Paragraph::with_text(t, CharProps::default()).styled("ListParagraph");
q.props.numbering = Some(NumRef { num: list, level: 0 });
@@ -310,7 +310,7 @@ fn deleted_heading_text_stays_out_of_bookmarks_and_tags() {
fn hidden_heading_text_stays_out_of_bookmarks_and_tags() {
let mut d = Document::new();
// Hidden text both directly and through a character style.
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "Secret".into(),
name: "Secret".into(),
kind: wordcraft_doc::StyleKind::Character,
@@ -421,7 +421,7 @@ fn table_style_hidden_heading_text_stays_out_of_bookmarks_and_tags() {
// Hidden by the table style's header row, so only the layout's resolution (table conditional
// formatting under the runs' own) says the text is hidden.
let mut d = Document::new();
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "SecretHeader".into(),
name: "Secret Header".into(),
kind: wordcraft_doc::StyleKind::Table,
@@ -1615,7 +1615,7 @@ fn all_caps_and_small_caps_text_extracts_as_displayed() {
// (as Word's PDFs do), and later text without caps keeps its own case: a capital glyph
// first drawn for a lowercase letter must not make every later capital read as lowercase.
let mut d = Document::new();
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "CapsPara".into(),
name: "Caps Para".into(),
kind: wordcraft_doc::StyleKind::Paragraph,
@@ -1652,14 +1652,14 @@ fn caps_list_labels_extract_as_displayed() {
// #1473: a lowercase-letter list label in an All Caps paragraph is drawn with capital glyphs;
// it reads as those capitals, and a later plain capital A still reads as "A".
let mut d = Document::new();
d.styles.upsert(wordcraft_doc::Style {
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
id: "CapsPara".into(),
name: "Caps Para".into(),
kind: wordcraft_doc::StyleKind::Paragraph,
chr: CharProps { caps: Some(true), ..Default::default() },
..Default::default()
});
let list = d.numbering.add_list(ListKind::Numbered);
let list = Arc::make_mut(&mut d.numbering).add_list(ListKind::Numbered);
let mut item = Paragraph::with_text("item", CharProps::default()).styled("CapsPara");
item.props.numbering = Some(NumRef { num: list, level: 1 });
d.body = vec![para_block(item), para_block(Paragraph::with_text("Plain Apple.", CharProps::default()))];
@@ -2059,7 +2059,7 @@ fn metadata_language_overrides_and_fallbacks_are_preserved() {
..Default::default()
};
for language in [Some("es-CR"), Some("ja"), Some(""), Some("日本語"), None] {
d.styles.default_chr.lang = language.map(str::to_owned);
Arc::make_mut(&mut d.styles).default_chr.lang = language.map(str::to_owned);
let bytes = export(&d, &opts).unwrap();
let raw = String::from_utf8_lossy(&bytes);
let pdf = hayro_syntax::Pdf::new(bytes.clone()).unwrap();
@@ -1,3 +1,5 @@
use std::sync::Arc;
use serde_json::json;
use wordcraft_engine::Session;
@@ -142,10 +144,10 @@ fn language_dialog_reads_inherited_proofing() {
a.run("select.all", json!({})).unwrap();
let proofing = wordcraft_doc::CharProps { lang: Some("en-GB".into()), no_proof: Some(true), ..Default::default() };
match source {
"default" => a.session.doc.styles.default_chr.overlay(&proofing),
"paragraph" => a.session.doc.styles.styles.iter_mut().find(|s| s.id == "Normal").unwrap().chr.overlay(&proofing),
"default" => Arc::make_mut(&mut a.session.doc.styles).default_chr.overlay(&proofing),
"paragraph" => Arc::make_mut(&mut a.session.doc.styles).styles.iter_mut().find(|s| s.id == "Normal").unwrap().chr.overlay(&proofing),
_ => {
a.session.doc.styles.styles.push(wordcraft_doc::Style {
Arc::make_mut(&mut a.session.doc.styles).styles.push(wordcraft_doc::Style {
id: "ProofingCharacter".into(),
kind: wordcraft_doc::StyleKind::Character,
chr: proofing,
@@ -170,7 +172,7 @@ fn language_dialog_ok_keeps_inherited_proofing() {
use egui_kittest::kittest::Queryable;
let mut a = app();
a.run("ui.language", json!({"value": "en"})).unwrap();
let normal = a.session.doc.styles.styles.iter_mut().find(|s| s.id == "Normal").unwrap();
let normal = Arc::make_mut(&mut a.session.doc.styles).styles.iter_mut().find(|s| s.id == "Normal").unwrap();
normal.chr.lang = Some("en-GB".into());
normal.chr.no_proof = Some(true);
a.run("select.all", json!({})).unwrap();
@@ -284,7 +284,7 @@ mod tests {
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("House.dotx");
let mut t = wordcraft_doc::Document::new();
t.styles.upsert(wordcraft_doc::styles::Style {
std::sync::Arc::make_mut(&mut t.styles).upsert(wordcraft_doc::styles::Style {
id: "Memo".into(),
name: "Memo".into(),
kind: wordcraft_doc::styles::StyleKind::Paragraph,
+3 -1
View File
@@ -184,6 +184,8 @@ mod tests;
#[cfg(test)]
mod navigation_tests {
use std::sync::Arc;
use super::*;
/// Numbered headings are listed with the number they show, and numbered body paragraphs in
@@ -193,7 +195,7 @@ mod navigation_tests {
use wordcraft_doc::props::NumRef;
use wordcraft_doc::{CharProps, ListKind, Paragraph, para_block};
let mut doc = Document::new();
let num = doc.numbering.add_list(ListKind::Legal);
let num = Arc::make_mut(&mut doc.numbering).add_list(ListKind::Legal);
let para = |text: &str, style: &str, level: Option<u8>| {
let mut p = Paragraph::with_text(text, CharProps::default()).styled(style);
p.props.numbering = level.map(|level| NumRef { num, level });
+2 -2
View File
@@ -28,10 +28,10 @@ fn font_menu_entries_leave_installed_families_unloaded() {
/// returns the painted shapes of the last one and every texture picture uploaded on the way.
fn run_gallery(mut doc: Document, style: wordcraft_doc::styles::Style) -> (Vec<egui::epaint::ClippedShape>, Vec<egui::ColorImage>) {
// Only the style under test is in the gallery.
for other in &mut doc.styles.styles {
for other in &mut Arc::make_mut(&mut doc.styles).styles {
other.quick = false;
}
doc.styles.upsert(style);
Arc::make_mut(&mut doc.styles).upsert(style);
let mut app = WordApp::new(wordcraft_engine::Session::new(doc), Default::default());
app.ui.language = "en".into();
let ctx = egui::Context::default();
+3 -2
View File
@@ -8,6 +8,7 @@
use std::collections::HashMap;
use serde_json::{Value, json};
use std::sync::Arc;
use wordcraft_doc::edit::Fragment;
use wordcraft_doc::para::OBJ;
use wordcraft_doc::props::{LineSpacing, TabAlign, TabLeader, TabStop};
@@ -637,7 +638,7 @@ fn remove_empty_note(s: &mut Session, f: &ZField) -> Option<Pos> {
fn ensure_bib_style(s: &mut Session) {
if s.doc.styles.get(BIB_STYLE).is_none() {
s.doc.styles.upsert(Style {
Arc::make_mut(&mut s.doc.styles).upsert(Style {
id: BIB_STYLE.into(),
name: BIB_STYLE.into(),
kind: StyleKind::Paragraph,
@@ -665,7 +666,7 @@ fn set_bibliography_style(s: &mut Session, c: &Call) -> Result<(), String> {
_ => Vec::new(),
};
ensure_bib_style(s);
let st = s.doc.styles.get_mut(BIB_STYLE).ok_or("the bibliography style is missing")?;
let st = Arc::make_mut(&mut s.doc.styles).get_mut(BIB_STYLE).ok_or("the bibliography style is missing")?;
st.para.indent_left = Some(twips(body));
st.para.indent_first = Some(twips(first));
st.para.line_spacing = Some(if line > 0 { LineSpacing::Multiple((line as f32 / 240.0).clamp(0.5, 5.0)) } else { LineSpacing::Multiple(1.0) });