From bd320e65a51de2fe96f8dec1952f3832a6f9ab28 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 17 Oct 2026 21:14:47 +0200 Subject: [PATCH 1/6] M2.1: pin layout tests to bundled fonts (#2729) --- ROADMAP.md | 1 + crates/fonts/Cargo.toml | 4 + crates/fonts/src/fontdb.rs | 32 +++- crates/fonts/tests/fixed_test_fonts.rs | 31 ++++ crates/fonts/tests/system_fonts.rs | 8 + crates/layout/Cargo.toml | 3 + crates/layout/src/contour.rs | 4 + crates/layout/src/fields.rs | 2 + crates/layout/src/kinsoku.rs | 1 + crates/layout/src/lib.rs | 7 + crates/layout/src/math.rs | 14 ++ crates/layout/src/outline.rs | 1 + crates/layout/src/para.rs | 6 + crates/layout/src/styleref.rs | 2 + crates/layout/src/tests.rs | 246 ++++++++++++++++++++++++- crates/layout/src/tests_bidi.rs | 21 +++ crates/layout/src/tests_complex.rs | 4 + crates/layout/src/tests_grid.rs | 2 + crates/layout/src/tests_numeral.rs | 8 + crates/layout/src/tests_pages.rs | 14 +- crates/layout/src/tests_styleref.rs | 9 + crates/layout/src/tests_textbox.rs | 4 + crates/layout/src/tests_vertical.rs | 4 + crates/layout/src/wordart.rs | 2 + docs/testing.md | 28 +++ 25 files changed, 448 insertions(+), 10 deletions(-) create mode 100644 crates/fonts/tests/fixed_test_fonts.rs create mode 100644 docs/testing.md diff --git a/ROADMAP.md b/ROADMAP.md index 40407d18b6..6700db9e2c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log | Date | What landed | +| 2026-10-17 | Layout unit tests use the pinned bundled fonts without system/user fonts or optional craft-fonts inputs (#2729); every test initializes the fixture before font lookup, and pagination fixtures are measured against that fixed set | | 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. | | 2026-10-14 | Tab stops edited on the ruler: the corner selector picks Left/Center/Right/Decimal/Bar, a click sets a stop (Alt skips the snap), dragging moves it with a guide line and position tooltip, dragging off the ruler clears it; markers draw per kind and mirror for RTL; one `para.tabs` call and one undo step per gesture | | 2026-10-12 | Fields keep `w:dirty` through DOCX saves (#877): a begin marker flagged by a generator that couldn't compute a result (pandoc-style TOC placeholders) keeps the flag on open/save — as a buffered field or a range field — so Word still refreshes it on open; computing a result (Update Field, Update Table) clears it, like Word's F9 | diff --git a/crates/fonts/Cargo.toml b/crates/fonts/Cargo.toml index 9107da44f1..d9d7812b11 100644 --- a/crates/fonts/Cargo.toml +++ b/crates/fonts/Cargo.toml @@ -8,6 +8,10 @@ description = "WordCraft fonts: font database (bundled OFL + system), metrics, g [lints] workspace = true +[features] +# Opt-in test harness support; enabling this alone never changes the application database. +test-support = [] + [dependencies] kurbo = { workspace = true } skrifa = { workspace = true } diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 5f07295138..6b02089169 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -40,6 +40,8 @@ static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), ]; +static GLOBAL: std::sync::OnceLock = std::sync::OnceLock::new(); + #[derive(Clone)] enum FontBytes { Static(&'static [u8]), @@ -894,10 +896,14 @@ impl FontDb { /// Arabic, so Arabic and Persian text draws without system fonts (#1652) (all empty without /// `CRAFT_FONTS_DIR`), whose system font scan reads `font_dirs`. pub fn with_font_dirs(font_dirs: Vec) -> Self { - let mut faces = Vec::new(); let others = crate::CRAFT_FONTS.iter().filter(|f| !f.scripts.contains(&"Jpan")); let craft = crate::japanese_document_fonts().into_iter().chain(others).map(|f| f.bytes); - for data in BUNDLED.iter().copied().chain(craft) { + Self::with_static_fonts(font_dirs, BUNDLED.iter().copied().chain(craft)) + } + + fn with_static_fonts(font_dirs: Vec, fonts: impl Iterator) -> Self { + let mut faces = Vec::new(); + for data in fonts { for f in enumerate_faces(data) { if let Some(mut face) = make_face(FontBytes::Static(data), f.index, f.family, f.style, f.legacy, f.coords) { face.local_names = f.local; @@ -948,8 +954,26 @@ impl FontDb { /// first time a lookup by family name needs them (or ahead of time by /// [`FontDb::scan_in_background`]). pub fn global() -> &'static FontDb { - static DB: std::sync::OnceLock = std::sync::OnceLock::new(); - DB.get_or_init(|| FontDb::with_font_dirs(system_font_dirs())) + GLOBAL.get_or_init(|| FontDb::with_font_dirs(system_font_dirs())) + } + + /// Initialize a test process with only the repository's pinned bundled fonts: no optional + /// craft-fonts, user fonts or system font directories. Call before any global font lookup. + /// Returns false if a normal global database was already initialized; never replaces it. + /// Repeated/concurrent calls are safe. Merely enabling `test-support` changes nothing. + #[cfg(feature = "test-support")] + pub fn init_test_fonts() -> bool { + static INIT: std::sync::OnceLock = std::sync::OnceLock::new(); + *INIT.get_or_init(|| { + let mut installed = false; + GLOBAL.get_or_init(|| { + installed = true; + let db = Self::with_static_fonts(Vec::new(), BUNDLED.iter().copied()); + db.set_system_fallback(false); + db + }); + installed + }) } fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec>> { diff --git a/crates/fonts/tests/fixed_test_fonts.rs b/crates/fonts/tests/fixed_test_fonts.rs new file mode 100644 index 0000000000..3f644ff624 --- /dev/null +++ b/crates/fonts/tests/fixed_test_fonts.rs @@ -0,0 +1,31 @@ +//! Separate process: verifies the global test fixture before any other lookup initializes it. +#![cfg(feature = "test-support")] +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use wordcraft_fonts::FontDb; + +#[test] +fn fixed_fonts_exclude_system_and_optional_faces() { + let threads: Vec<_> = (0..8).map(|_| std::thread::spawn(FontDb::init_test_fonts)).collect(); + assert!(threads.into_iter().all(|t| t.join().unwrap())); + let db = FontDb::global(); + let families = [ + "Inter", + "Inter Medium", + "Inter SemiBold", + "JetBrains Mono", + "Source Sans 3", + "Source Sans 3 Semibold", + "Source Serif 4", + "Source Serif 4 Semibold", + ]; + assert_eq!(db.families(), families); + assert_eq!(db.load_system_fonts(), 0); + assert_eq!(db.families(), families); + for family in ["Arial", "Calibri", "Carlito", "Noto Sans Arabic", "BIZ UDPGothic"] { + assert!(!db.has_family(family), "external family {family} leaked into the test fixture"); + } + assert!(db.fallback_for('你', 0).is_none(), "missing glyph lookup must not consult system fonts"); + assert_eq!(db.loaded_font_bytes(), 0); + assert!(FontDb::init_test_fonts()); +} diff --git a/crates/fonts/tests/system_fonts.rs b/crates/fonts/tests/system_fonts.rs index e31fb1fe78..d686394abf 100644 --- a/crates/fonts/tests/system_fonts.rs +++ b/crates/fonts/tests/system_fonts.rs @@ -21,3 +21,11 @@ fn a_fresh_session_finds_installed_fonts_by_name() { assert_eq!(&FontDb::global().face(family, "Regular").family, family); assert!(FontDb::global().families().contains(family)); } + +#[cfg(feature = "test-support")] +#[test] +fn test_initialization_cannot_replace_an_application_database() { + let db = FontDb::global(); + assert!(!FontDb::init_test_fonts()); + assert!(std::ptr::eq(db, FontDb::global())); +} diff --git a/crates/layout/Cargo.toml b/crates/layout/Cargo.toml index afb8f0a4a1..0a5cf79fe6 100644 --- a/crates/layout/Cargo.toml +++ b/crates/layout/Cargo.toml @@ -25,3 +25,6 @@ log = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] # The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32). js-sys = "0.3" + +[dev-dependencies] +wordcraft-fonts = { workspace = true, features = ["test-support"] } diff --git a/crates/layout/src/contour.rs b/crates/layout/src/contour.rs index 1c245b885d..e2e5fce610 100644 --- a/crates/layout/src/contour.rs +++ b/crates/layout/src/contour.rs @@ -221,6 +221,7 @@ mod tests { #[test] fn occupied_follows_the_outline() { + crate::test_fonts(); let c = diamond(false); let s = c.occupied(0.0, 10.0); assert_eq!(s.len(), 1); @@ -235,6 +236,7 @@ mod tests { #[test] fn through_keeps_interior_gaps() { + crate::test_fonts(); // A U: two prongs joined at the bottom. let u = [(0.0, 0.0), (20.0, 0.0), (20.0, 80.0), (80.0, 80.0), (80.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; let (_, tight) = Contour::new(&u, 0.0, 0.0, 0.0, false).unwrap(); @@ -246,6 +248,7 @@ mod tests { #[test] fn pictures_wrap_around_their_opaque_pixels() { + crate::test_fonts(); let png = |img: image::DynamicImage| { let mut buf = Vec::new(); img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png).unwrap(); @@ -268,6 +271,7 @@ mod tests { #[test] fn hostile_points_are_dropped() { + crate::test_fonts(); assert!(Contour::new(&[(f32::NAN, 0.0), (1.0, 1.0), (f32::INFINITY, 2.0)], 0.0, 0.0, 0.0, false).is_none()); let c = diamond(true); assert!(c.occupied(f32::NAN, 1.0).is_empty()); diff --git a/crates/layout/src/fields.rs b/crates/layout/src/fields.rs index e0300a06d1..f0f4a61ac3 100644 --- a/crates/layout/src/fields.rs +++ b/crates/layout/src/fields.rs @@ -177,6 +177,7 @@ mod tests { #[test] fn page_fields() { + crate::test_fonts(); let ctx = FieldCtx { page: 4, pages: 12, ..Default::default() }; assert_eq!(field_text("PAGE", "", &ctx), ("4".into(), true)); assert_eq!(field_text(" PAGE \\* ROMAN ", "", &ctx).0, "IV"); @@ -191,6 +192,7 @@ mod tests { #[test] fn page_references_show_their_bookmarks_page() { + crate::test_fonts(); let pages = HashMap::from([("End_1".to_string(), (3, NumFormat::Decimal)), ("Intro".to_string(), (4, NumFormat::LowerRoman))]); let ctx = FieldCtx { bookmark_pages: Arc::new(pages), bookmarks_key: 1, ..Default::default() }; assert_eq!(field_text("PAGEREF End_1 \\* Arabic \\* MERGEFORMAT", "9", &ctx), ("3".into(), true)); diff --git a/crates/layout/src/kinsoku.rs b/crates/layout/src/kinsoku.rs index 38f8df4362..b0d6b2cbea 100644 --- a/crates/layout/src/kinsoku.rs +++ b/crates/layout/src/kinsoku.rs @@ -82,6 +82,7 @@ mod tests { #[test] fn kinsoku_and_word_wrap_break_opportunities() { + crate::test_fonts(); let ja = "日本語、「漢字」。テスト"; let on = breaks(ja, true, true); for c in ["、", "」", "。", "漢"] { diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 3ff6484361..947d8b0e42 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -8,6 +8,13 @@ //! - [`hit`]: point ↔ position, caret geometry, line navigation, selection rectangles. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +/// Every unit test calls this before any font lookup (including its fixture construction). +/// A process-wide immutable choice keeps font and math caches consistent across test threads. +#[cfg(test)] +fn test_fonts() { + assert!(wordcraft_fonts::FontDb::init_test_fonts(), "layout tests must initialize fixed fonts before any global font lookup"); +} + pub mod balloons; mod contour; pub mod display; diff --git a/crates/layout/src/math.rs b/crates/layout/src/math.rs index bab58ef984..7b558a2a44 100644 --- a/crates/layout/src/math.rs +++ b/crates/layout/src/math.rs @@ -1996,6 +1996,7 @@ mod tests { #[test] fn long_display_equations_wrap_before_operators_and_indent() { + crate::test_fonts(); let sum = "y=a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w"; let mp = MathProps::default(); let full = lay_in(&Math::default(), sum, true, 1e4, &mp); @@ -2036,6 +2037,7 @@ mod tests { #[test] fn manual_breaks_start_new_lines() { + crate::test_fonts(); let mp = MathProps::default(); let nodes = vec![MNode::Run(MRun::new("a=b")), MNode::Run(MRun { brk: Some(0), ..MRun::new("+c") })]; let m = lay_in(&Math { nodes, ..Default::default() }, "", true, 400.0, &mp); @@ -2052,6 +2054,7 @@ mod tests { #[test] fn short_display_equations_stay_on_one_line() { + crate::test_fonts(); let mp = MathProps::default(); let one = lay_in(&Math::default(), "a+b=c", true, 400.0, &mp); assert_eq!(lines(&one).len(), 1); @@ -2061,6 +2064,7 @@ mod tests { #[test] fn fraction_is_stacked_with_a_rule() { + crate::test_fonts(); let flat = lay("ab", true); let f = lay("a/b", true); assert!(f.ascent > flat.ascent + 2.0 && f.descent > flat.descent + 2.0, "{f:?}"); @@ -2070,6 +2074,7 @@ mod tests { #[test] fn superscript_is_raised_and_smaller() { + crate::test_fonts(); let m = lay("x^2", false); let sizes: Vec<(f32, f32)> = m .items @@ -2085,6 +2090,7 @@ mod tests { #[test] fn operators_get_spaces_and_unary_minus_does_not() { + crate::test_fonts(); let tight = lay("ab", false); let spaced = lay("a+b", false); let unary = lay("−b", false); @@ -2095,6 +2101,7 @@ mod tests { #[test] fn display_operators_are_big_with_limits_over_and_under() { + crate::test_fonts(); let d = lay("∑_(i=1)^n▒i", true); let t = lay("∑_(i=1)^n▒i", false); assert!(d.ascent + d.descent > t.ascent + t.descent, "display sum taller"); @@ -2103,6 +2110,7 @@ mod tests { #[test] fn delimiters_grow_around_tall_content() { + crate::test_fonts(); let small = lay("(x)", true); let big = lay("((a)/(b))", true); let h = |m: &MathLayout| m.ascent + m.descent; @@ -2111,6 +2119,7 @@ mod tests { #[test] fn every_structure_lays_out() { + crate::test_fonts(); use wordcraft_doc::math::*; let x = || vec![MNode::Run(MRun::new("x"))]; let nodes = vec![ @@ -2146,6 +2155,7 @@ mod tests { #[test] fn caret_slots_cover_every_position() { + crate::test_fonts(); use wordcraft_doc::math_edit::{MathPos, move_right, units}; let math = Math { nodes: wordcraft_doc::math::parse_linear("x=(-b±√(b^2-4ac))/2a+■(a&b@c&d)"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); @@ -2171,6 +2181,7 @@ mod tests { #[test] fn empty_slots_show_placeholders_on_screen_only() { + crate::test_fonts(); let math = Math { nodes: wordcraft_doc::math::parse_linear("⬚/⬚"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); assert!(m.items.iter().all(|i| matches!(i, MItem::ScreenOnly(_) | MItem::Rect { .. })), "{:?}", m.items); @@ -2181,6 +2192,7 @@ mod tests { #[test] fn numbered_equations_fill_the_line_with_the_number_at_the_right() { + crate::test_fonts(); let math = Math { nodes: wordcraft_doc::math::parse_linear("E=mc^2#(1)"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); assert!((m.width - 400.0).abs() < 0.01, "{}", m.width); @@ -2200,6 +2212,7 @@ mod tests { #[test] fn math_alphabets() { + crate::test_fonts(); assert_eq!(math_alnum('x', false, true, MScr::Roman), '𝑥'); assert_eq!(math_alnum('h', false, true, MScr::Roman), 'ℎ'); assert_eq!(math_alnum('R', false, false, MScr::DoubleStruck), 'ℝ'); @@ -2210,6 +2223,7 @@ mod tests { #[test] fn hostile_trees_never_panic() { + crate::test_fonts(); use wordcraft_doc::math::*; // Deep nesting. let mut n = vec![MNode::Run(MRun::new("x"))]; diff --git a/crates/layout/src/outline.rs b/crates/layout/src/outline.rs index af72e838dc..27b86657c5 100644 --- a/crates/layout/src/outline.rs +++ b/crates/layout/src/outline.rs @@ -222,6 +222,7 @@ mod tests { #[test] fn show_level_and_collapse_hide_what_is_under_a_heading() { + crate::test_fonts(); // H1, body, H2, body, H1, body let levels = [1, BODY, 2, BODY, 1, BODY]; let all = OutlineView::default(); diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 6b1ea46fd4..7c517e213a 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -2743,6 +2743,7 @@ mod label_spacing_tests { #[test] fn large_label_overflow_is_not_multiplied() { + crate::test_fonts(); for (multiple, expected) in [(1.0, 25.20), (1.25, 28.80), (2.0, 39.36)] { let lines = measured_label_lines(LineSpacing::Multiple(multiple), (22.265625, 5.859375), 22.265625, false); assert!((lines[0].height - expected).abs() <= 0.25, "{multiple}: {} vs {expected}", lines[0].height); @@ -2754,12 +2755,14 @@ mod label_spacing_tests { #[test] fn small_label_descent_does_not_inflate_text_spacing() { + crate::test_fonts(); let lines = measured_label_lines(LineSpacing::Multiple(1.25), (8.499023, 3.5009766), 9.0, false); assert!((lines[0].height - 17.52).abs() <= 0.25, "{}", lines[0].height); } #[test] fn label_leading_and_earlier_font_ascent_are_retained() { + crate::test_fonts(); // Apple Symbols 24 pt: full leading gives an 18 pt label ascent. The text // pitch is still Roboto 12 pt. A shorter final fallback run must not hide it. for metrics in [(16.998047, 7.001953), (8.0, 3.0)] { @@ -2770,6 +2773,7 @@ mod label_spacing_tests { #[test] fn minimum_spacing_keeps_text_descent_and_label_leading() { + crate::test_fonts(); for (metrics, ascent, expected) in [((22.265625, 5.859375), 22.265625, 25.20), ((8.499023, 3.5009766), 9.0, 13.92), ((16.998047, 7.001953), 18.0, 20.88)] { @@ -2780,6 +2784,7 @@ mod label_spacing_tests { #[test] fn labels_keep_fixed_and_minimum_spacing_rules() { + crate::test_fonts(); for (spacing, expected) in [(LineSpacing::Exactly(24.0), 24.0), (LineSpacing::AtLeast(30.0), 30.0)] { let lines = measured_label_lines(spacing, (22.265625, 5.859375), 22.265625, false); assert!((lines[0].height - expected).abs() < 0.001); @@ -2788,6 +2793,7 @@ mod label_spacing_tests { #[test] fn label_overflow_only_affects_the_first_wrapped_line() { + crate::test_fonts(); let lines = measured_label_lines(LineSpacing::Multiple(1.25), (22.265625, 5.859375), 22.265625, true); assert!(lines.len() > 1); assert!((lines[0].height - 28.80).abs() <= 0.25); diff --git a/crates/layout/src/styleref.rs b/crates/layout/src/styleref.rs index 02a528441f..1d336903fa 100644 --- a/crates/layout/src/styleref.rs +++ b/crates/layout/src/styleref.rs @@ -347,6 +347,7 @@ mod tests { #[test] fn parses_names_levels_and_switches() { + crate::test_fonts(); let c = parse(r#"STYLEREF "Heading 1" \l \n \t \* MERGEFORMAT"#); assert_eq!(c.style, "Heading 1"); assert!(c.last && c.suppress && !c.position); @@ -359,6 +360,7 @@ mod tests { #[test] fn suppress_keeps_the_number() { + crate::test_fonts(); assert_eq!(suppress("Chapter 2.", &[("2".into(), true)]), "2"); assert_eq!(chapter_number("Chapter IV."), "IV"); } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index d6865b0d92..bfe8761507 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -61,6 +61,7 @@ fn legacy_table_gap(mode: u32, allow: bool, location: SpacingLocation) -> f32 { #[test] fn legacy_table_contextual_spacing_flag_keeps_after_space() { + crate::test_fonts(); // Original Word for Mac 16.113.3 probe: after 12 pt, before 0 pt, both contextual. // Zero before-space keeps the compatibility rule independent of ordinary overlap. for mode in [11, 12, 14] { @@ -71,6 +72,7 @@ fn legacy_table_contextual_spacing_flag_keeps_after_space() { #[test] fn legacy_table_contextual_spacing_flag_is_scoped_to_cells_and_legacy_mode() { + crate::test_fonts(); for (mode, allow) in [(11, false), (12, false), (14, false), (15, false), (15, true)] { assert!(legacy_table_gap(mode, allow, SpacingLocation::Cell).abs() < 0.01); } @@ -123,6 +125,7 @@ fn contextual_gap(after: f32, before: f32, first: bool, second: bool, different_ // Exactly 24 pt spacing isolates the extra gap from font-dependent line height. #[test] fn contextual_after_removes_a_smaller_before_gap() { + crate::test_fonts(); for header in [false, true] { let gap = contextual_gap(12.0, 8.0, true, false, false, header); assert!(gap.abs() < 0.01, "header={header}: gap={gap}, expected 0 pt"); @@ -131,6 +134,7 @@ fn contextual_after_removes_a_smaller_before_gap() { #[test] fn contextual_after_keeps_only_the_residual_before_gap() { + crate::test_fonts(); // ISO/IEC 29500-1 §17.3.1.9's worked example: max(10, 12) - 10 = 2 pt. for header in [false, true] { let gap = contextual_gap(10.0, 12.0, true, false, false, header); @@ -140,6 +144,7 @@ fn contextual_after_keeps_only_the_residual_before_gap() { #[test] fn contextual_spacing_preserves_each_side_and_style_identity() { + crate::test_fonts(); for header in [false, true] { for (after, before, first, second, different, expected) in [ (12.0, 8.0, false, true, false, 12.0), @@ -158,6 +163,7 @@ fn contextual_spacing_preserves_each_side_and_style_identity() { #[test] fn contextual_spacing_removes_every_gap_in_a_four_paragraph_run() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; for table in [false, true] { let mut doc = Document::new(); @@ -207,6 +213,7 @@ fn contextual_spacing_removes_every_gap_in_a_four_paragraph_run() { // Contextual and automatic list spacing can suppress the same after-space only once. #[test] fn contextual_automatic_lists_do_not_subtract_the_gap_twice() { + crate::test_fonts(); use wordcraft_doc::props::{LineSpacing, NumRef}; for table in [false, true] { let mut doc = Document::new(); @@ -255,6 +262,7 @@ fn lines_of(l: &DocLayout) -> usize { #[test] fn empty_doc_has_one_page() { + crate::test_fonts(); let d = Document::new(); let l = lay(&d); assert_eq!(l.pages.len(), 1); @@ -267,6 +275,7 @@ fn empty_doc_has_one_page() { #[test] fn hidden_marks_take_no_space() { + crate::test_fonts(); let mut d = Document::new(); d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("Before", Default::default()))]; for _ in 0..20 { @@ -308,6 +317,7 @@ fn hidden_marks_take_no_space() { #[test] fn long_paragraph_wraps_and_paginates() { + crate::test_fonts(); let text = "The quick brown fox jumps over the lazy dog. ".repeat(400); let d = Document::from_text(&text); let l = lay(&d); @@ -328,6 +338,7 @@ fn long_paragraph_wraps_and_paginates() { #[test] fn reading_layout_reflows_into_viewport_height_pages_without_changing_print_layout() { + crate::test_fonts(); let d = Document::from_text(&"A moderately long sentence that should wrap across reading columns. ".repeat(250)); let opts = LayoutOptions { view: ViewMode::Read, web_width: 230.0, read_height: 390.0, ..Default::default() }; let read = layout(&d, &mut LayoutCache::new(), &opts); @@ -352,6 +363,7 @@ fn reading_layout_reflows_into_viewport_height_pages_without_changing_print_layo #[test] fn hit_and_caret_agree() { + crate::test_fonts(); let d = Document::from_text("Hello world, this is WordCraft.\nSecond paragraph here."); let l = lay(&d); for off in [0, 5, 12, 31] { @@ -370,6 +382,7 @@ fn hit_and_caret_agree() { #[test] fn selection_bar_finds_the_line_beside_the_pointer() { + crate::test_fonts(); // #856: the left margin beside a line is Word's selection bar. let text = format!("{}\nShort.", "The quick brown fox jumps over the lazy dog. ".repeat(8)); let mut d = Document::from_text(&text); @@ -403,6 +416,7 @@ fn selection_bar_finds_the_line_beside_the_pointer() { #[test] fn deletions_leave_the_final_text_layout() { + crate::test_fonts(); // "Keep " + deleted "DELETEDTEXT " (5..17) + inserted "INSERTED". let mut d = Document::from_text("Keep DELETEDTEXT INSERTED"); d.format_range(&Pos::body(0, 5), &Pos::body(0, 17), &|c| c.del = Some(0)).unwrap(); @@ -433,6 +447,7 @@ fn deletions_leave_the_final_text_layout() { #[test] fn deleted_soft_hyphen_is_no_hyphenation_point_without_markup() { + crate::test_fonts(); // "ab\u{ad}cdef" in a column where "ab-" fits but the whole word does not; only the soft // hyphen is track-deleted. let mut d = Document::from_text("ab\u{ad}cdef"); @@ -451,6 +466,7 @@ fn deleted_soft_hyphen_is_no_hyphenation_point_without_markup() { #[test] fn deleted_text_adds_no_line_break_opportunity_without_markup() { + crate::test_fonts(); // "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"); @@ -469,6 +485,7 @@ fn deleted_text_adds_no_line_break_opportunity_without_markup() { #[test] fn hidden_soft_hyphen_is_no_hyphenation_point_when_hidden_text_is_not_shown() { + crate::test_fonts(); // As with deletions: "ab\u{ad}cdef" where only the soft hyphen is hidden text. let mut d = Document::from_text("ab\u{ad}cdef"); d.format_range(&Pos::body(0, 2), &Pos::body(0, 4), &|c| c.hidden = Some(true)).unwrap(); @@ -486,6 +503,7 @@ fn hidden_soft_hyphen_is_no_hyphenation_point_when_hidden_text_is_not_shown() { #[test] fn hidden_text_adds_no_line_break_opportunity_when_not_shown() { + crate::test_fonts(); // "xx ab" + hidden soft hyphen or space + "cdef", in a column where "xx ab-" and "abcdef" fit // but "xx abcdef" does not. Measured in Word (hidden text not printed): "xx" / "abcdef" both times. for hidden in ["\u{ad}", " "] { @@ -558,6 +576,7 @@ fn layout_time_ratio(doc: impl Fn(usize) -> Document, n: usize, opts: &LayoutOpt #[test] fn hidden_separators_with_automatic_hyphenation_lay_out_in_linear_time() { + crate::test_fonts(); // "hyphenation " n times with every space hidden: one joined word of 11n letters reaches the // hyphenator, whose limit checks once rescanned the word for every candidate point. let doc = |n: usize| { @@ -576,6 +595,7 @@ fn hidden_separators_with_automatic_hyphenation_lay_out_in_linear_time() { #[test] fn many_hidden_float_anchors_lay_out_in_linear_time() { + crate::test_fonts(); // n square-wrapped shapes, each anchored in its own hidden run (bold alternating, so runs do not // merge), markup on: finding each anchor's formatting once rescanned the runs from the start. let doc = |n: usize| { @@ -630,6 +650,7 @@ fn many_hidden_float_anchors_lay_out_in_linear_time() { #[test] fn fragmented_hidden_text_lays_out_like_the_visible_text() { + crate::test_fonts(); // Visible words with a hidden multi-byte char after every visible one, formatted in alternating // runs (bold on and off) so no two left-out runs are adjacent or merge. The plain paragraph has // the same visible runs without the hidden chars, so both lay out the same widths whatever font @@ -684,6 +705,7 @@ fn fragmented_hidden_text_lays_out_like_the_visible_text() { #[test] fn automatic_hyphenation_sees_only_the_text_laid_out() { + crate::test_fonts(); // Paragraph-text ends of the clusters after which a line may end with a hyphen. let hyph_ends = |d: &Document, opts: &LayoutOptions| { let l = layout(d, &mut LayoutCache::new(), opts); @@ -723,6 +745,7 @@ fn automatic_hyphenation_sees_only_the_text_laid_out() { #[test] fn page_break_char_starts_new_page() { + crate::test_fonts(); let mut d = Document::from_text("one"); d.insert_text(&Pos::body(0, 3), "\u{000C}", &Default::default()).unwrap(); let l = lay(&d); @@ -731,6 +754,7 @@ fn page_break_char_starts_new_page() { #[test] fn a_missing_roman_font_is_drawn_as_word_draws_it() { + crate::test_fonts(); // Measured in Word: a font that isn't installed is drawn in Cambria when the font table calls // it roman, in Calibri otherwise. let face = |roman: bool| { @@ -752,6 +776,7 @@ fn a_missing_roman_font_is_drawn_as_word_draws_it() { #[test] fn an_empty_section_mark_stays_on_a_full_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // Measured in Word: a page filled to the bottom, then an empty paragraph carrying a next-page // section break, makes no page for the mark; the next section starts on page 2. A plain empty @@ -775,6 +800,7 @@ fn an_empty_section_mark_stays_on_a_full_page() { #[test] fn a_bookmark_after_a_page_break_takes_no_line() { + crate::test_fonts(); // "Before", a page break, then a bookmark start (Word writes the next heading's TOC bookmark // there): the bookmark takes no room, so "After" starts at the top of page 2, as in Word. let top_of_after = |bookmark: bool| { @@ -797,6 +823,7 @@ fn a_bookmark_after_a_page_break_takes_no_line() { #[test] fn a_page_break_then_a_new_page_makes_one_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // "Before", a paragraph of only a page break, then "After" starting a page by itself: a page // break before, or a next-page section break carried by the break's own paragraph. Word's @@ -819,6 +846,7 @@ fn a_page_break_then_a_new_page_makes_one_page() { #[test] fn page_numbers_can_restart_at_zero() { + crate::test_fonts(); // `w:pgNumType w:start="0"`: a cover page numbered 0, so the page after it reads 1. let mut d = Document::from_text("Cover\u{000C}Contents"); d.last_section.page_num_start = Some(0); @@ -828,6 +856,7 @@ fn page_numbers_can_restart_at_zero() { #[test] fn center_and_right_alignment() { + crate::test_fonts(); let mut d = Document::from_text("abc\nabc\nabc"); d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 0), &|p| p.align = Some(Align::Center)).unwrap(); d.format_paragraphs(&Pos::body(2, 0), &Pos::body(2, 0), &|p| p.align = Some(Align::Right)).unwrap(); @@ -842,6 +871,7 @@ fn center_and_right_alignment() { #[test] fn tabs_land_on_default_stops() { + crate::test_fonts(); let d = Document::from_text("a\tb"); let l = lay(&d); let xb = l.caret(&Pos::body(0, 2)).unwrap().x; @@ -855,6 +885,7 @@ fn tabs_land_on_default_stops() { /// Letterheads that line up a footer with tabs under a negative indent rely on it. #[test] fn tabs_left_of_the_margin_stay_on_a_default_stop() { + crate::test_fonts(); let x_of = |text: &str, indent: f32, off: usize| { let mut d = Document::from_text(text); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.indent_left = Some(indent)).unwrap(); @@ -869,6 +900,7 @@ fn tabs_left_of_the_margin_stay_on_a_default_stop() { #[test] fn right_tab_aligns_text_end() { + crate::test_fonts(); let mut d = Document::from_text("left\tright"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| { p.tabs = Some(vec![wordcraft_doc::props::TabStop { pos: 400.0, align: wordcraft_doc::props::TabAlign::Right, leader: Default::default() }]) @@ -881,6 +913,7 @@ fn right_tab_aligns_text_end() { #[test] fn lists_get_labels_and_indent() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); let id = 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(); @@ -897,6 +930,7 @@ fn lists_get_labels_and_indent() { #[test] fn break_only_list_has_no_label() { + crate::test_fonts(); use wordcraft_doc::ListKind; for kind in [ListKind::Numbered, ListKind::Bullet] { let mut d = Document::from_text("First\n\u{000C}\nLast\n\nAfter"); @@ -927,6 +961,7 @@ fn break_only_list_has_no_label() { #[test] fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() { + crate::test_fonts(); // "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"); @@ -947,6 +982,7 @@ fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() { #[test] fn russian_letter_list_labels_draw_cyrillic() { + crate::test_fonts(); // russianLower labelled items 1, 2, 3 instead of а), б), в). use wordcraft_doc::section::NumFormat; let mut d = Document::from_text("один\nдва\nтри"); @@ -978,6 +1014,7 @@ fn russian_letter_list_labels_draw_cyrillic() { #[test] fn chinese_counting_list_labels_and_their_font_runs() { + crate::test_fonts(); // 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丙"); @@ -1027,6 +1064,7 @@ fn chinese_counting_list_labels_and_their_font_runs() { #[test] fn symbol_font_bullet_without_the_font_draws_a_bullet() { + crate::test_fonts(); // 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}')); @@ -1045,6 +1083,7 @@ fn symbol_font_bullet_without_the_font_draws_a_bullet() { #[test] fn wingdings_text_draws_and_reads_as_its_unicode_symbols() { + crate::test_fonts(); // Word stores symbol-font text both ways: plain codes ("J" = smiley) and private-use codes // (U+F0FC = check mark, what `w:sym` writes). let mut d = Document::from_text("Hi J\u{F0FC} ok"); @@ -1076,6 +1115,7 @@ fn wingdings_text_draws_and_reads_as_its_unicode_symbols() { #[test] fn tables_lay_out_cells() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 3, 468.0); t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("cell text", Default::default()))]; @@ -1095,6 +1135,7 @@ fn tables_lay_out_cells() { /// continuation row's box is `covered` (no gridline, not a hit). #[test] fn a_vertical_merge_lays_out_as_one_cell() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(3, 2, 468.0); t.rows[0].cells[0].props.vmerge = wordcraft_doc::props::VMerge::Restart; @@ -1137,6 +1178,7 @@ fn turned_lines(l: &DocLayout) -> &Placed { /// grows to fit it, and caret, clicks and drawing follow the turn. #[test] fn turned_cell_text_runs_down_the_cell() { + crate::test_fonts(); let text = "Turned cell text"; let d = turned_table(TextDirection::Down, text, None); let l = lay(&d); @@ -1175,6 +1217,7 @@ fn turned_cell_text_runs_down_the_cell() { #[test] fn turned_up_cell_text_reads_bottom_to_top() { + crate::test_fonts(); let text = "Bottom to top"; let d = turned_table(TextDirection::Up, text, None); let l = lay(&d); @@ -1190,6 +1233,7 @@ fn turned_up_cell_text_reads_bottom_to_top() { #[test] fn turned_text_wraps_in_an_exact_row() { + crate::test_fonts(); let d = turned_table(TextDirection::Down, &"word ".repeat(30), Some(72.0)); let l = lay(&d); let it = turned_lines(&l); @@ -1203,6 +1247,7 @@ fn turned_text_wraps_in_an_exact_row() { #[test] fn hostile_turned_cells_never_panic() { + crate::test_fonts(); // Empty, huge and nested turned cells, in exact rows too small for anything. for dir in [TextDirection::Down, TextDirection::Up] { for row in [None, Some(0.5), Some(1e9)] { @@ -1230,6 +1275,7 @@ fn rules_on_page0(l: &DocLayout) -> Vec { #[test] fn table_style_borders_survive_empty_tbl_borders() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); // defaults to the TableGrid style t.props.borders = Some(Borders::default()); // what an empty parses to @@ -1247,6 +1293,7 @@ fn table_style_borders_survive_empty_tbl_borders() { /// vertical edge it meets, not at its centre line. #[test] fn table_border_corners_close() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); let thick = Some(Border::single(3.0)); @@ -1272,6 +1319,7 @@ fn table_border_corners_close() { #[test] fn table_own_border_overrides_only_that_side() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); t.props.borders = Some(Borders { top: Some(Border::single(2.0)), ..Default::default() }); @@ -1284,6 +1332,7 @@ fn table_own_border_overrides_only_that_side() { #[test] fn table_nil_border_hides_style_side() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(1, 1, 468.0); // 1×1: no inside edges t.props.borders = Some(Borders::all(Border { style: BorderStyle::None, width: 0.0, color: None, space: 0.0 })); @@ -1293,6 +1342,7 @@ fn table_nil_border_hides_style_side() { #[test] fn table_without_style_stays_borderless() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); t.props.style = None; @@ -1303,6 +1353,7 @@ fn table_without_style_stays_borderless() { #[test] fn headers_and_page_fields() { + crate::test_fonts(); let mut d = Document::from_text(&"para\n".repeat(120)); let mut hp = wordcraft_doc::Paragraph::new(); hp.insert_object( @@ -1327,6 +1378,7 @@ fn headers_and_page_fields() { #[test] fn tall_first_page_header_pushes_body_down() { + crate::test_fonts(); let mut d = Document::from_text(&"para\n".repeat(120)); let tall: Vec<_> = (0..12).map(|_| wordcraft_doc::para_block(wordcraft_doc::Paragraph::new())).collect(); let id = d.add_part(wordcraft_doc::PartKind::Header, tall); @@ -1343,6 +1395,7 @@ fn tall_first_page_header_pushes_body_down() { #[test] fn cache_reuses_unchanged_paragraphs() { + crate::test_fonts(); let d = Document::from_text(&"some text here\n".repeat(50)); let mut c = LayoutCache::new(); layout(&d, &mut c, &LayoutOptions::default()); @@ -1358,6 +1411,7 @@ fn cache_reuses_unchanged_paragraphs() { /// again after an edit elsewhere measures nothing again. #[test] fn cache_keeps_one_layout_per_table_cell() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(4, 3, 468.0); for (r, row) in t.rows.iter_mut().enumerate() { @@ -1378,6 +1432,7 @@ fn cache_keeps_one_layout_per_table_cell() { #[test] fn selection_rects_cover_range() { + crate::test_fonts(); let d = Document::from_text("Hello world\nSecond line"); let l = lay(&d); let r = l.selection_rects(&d, &Pos::body(0, 6), &Pos::body(1, 6), 0); @@ -1387,6 +1442,7 @@ fn selection_rects_cover_range() { #[test] fn column_segments_take_the_same_x_range_on_every_line() { + crate::test_fonts(); let d = Document::from_text("abcdef\nabcdef\nab"); let l = lay(&d); let left = l.caret(&Pos::body(0, 2)).unwrap().x; @@ -1401,6 +1457,7 @@ fn column_segments_take_the_same_x_range_on_every_line() { #[test] fn display_has_glyphs_and_marks() { + crate::test_fonts(); let mut d = Document::from_text("Hello\tworld"); d.format_range(&Pos::body(0, 0), &Pos::body(0, 5), &|c| c.underline = Some(wordcraft_doc::props::Underline::Single)).unwrap(); let l = lay(&d); @@ -1413,6 +1470,7 @@ fn display_has_glyphs_and_marks() { #[test] fn a_tracked_paragraph_mark_shows_in_its_authors_colour() { + crate::test_fonts(); let mut d = Document::from_text("Split\nhere"); d.revisions.push(wordcraft_doc::Revision { kind: wordcraft_doc::RevisionKind::Insert, @@ -1434,6 +1492,7 @@ fn a_tracked_paragraph_mark_shows_in_its_authors_colour() { #[test] fn section_breaks_show_with_formatting_marks() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // Two sections: "One" ends the first; the second starts as `start`. let doc = |start: SectionStart| { @@ -1462,6 +1521,7 @@ fn section_breaks_show_with_formatting_marks() { #[test] fn space_between_paragraphs_is_the_larger_of_after_and_before() { + crate::test_fonts(); // (space after the first, space before the second, contextual spacing on both) -> gap let gap = |after: f32, before: f32, contextual: bool| -> f32 { let mut d = Document::from_text("One\nTwo"); @@ -1486,6 +1546,7 @@ fn space_between_paragraphs_is_the_larger_of_after_and_before() { #[test] fn space_between_paragraphs_is_their_sum_when_html_auto_spacing_is_off() { + crate::test_fonts(); // Measured in Word (compatibility modes 14 and 15): with `w:doNotUseHTMLParagraphAutoSpacing`, // 12 pt after and 12 pt before leave 24 pt; without it, 12 pt. let gaps = |sum: bool| -> (f32, f32) { @@ -1519,6 +1580,7 @@ fn space_between_paragraphs_is_their_sum_when_html_auto_spacing_is_off() { #[test] fn space_between_paragraphs_in_a_cell_follows_the_body_rule() { + crate::test_fonts(); // Three paragraphs in a cell, each 6pt before and 6pt after -> the gaps between them. let gaps = |contextual: bool| -> Vec { let mut d = Document::from_text("before\nafter"); @@ -1552,6 +1614,7 @@ fn space_between_paragraphs_in_a_cell_follows_the_body_rule() { #[test] fn sections_land_on_the_side_their_break_and_numbering_ask_for() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // "One" ends the first section on page 1; the second section starts as `start`. let pages = |start: SectionStart, restart: Option, even_odd: bool| -> Vec<(usize, u32)> { @@ -1579,6 +1642,7 @@ fn sections_land_on_the_side_their_break_and_numbering_ask_for() { #[test] fn paragraph_marks_sit_past_text_running_the_other_way() { + crate::test_fonts(); // A left-to-right paragraph ending in Hebrew, and a right-to-left one ending in Latin: the // logically last letters are at the wrong edge, but the ¶ still goes past every glyph. let mut d = Document::from_text("Hello \u{5e9}\u{5dc}\u{5d5}\u{5dd}\n\u{5e9}\u{5dc}\u{5d5}\u{5dd} abc"); @@ -1619,6 +1683,7 @@ fn border_lines_in(d: &Document, l: &DocLayout) -> Vec<(f32, f32, f32, f32)> { #[test] fn character_border_draws_one_box_around_run() { + crate::test_fonts(); use wordcraft_doc::props::Border; let mut d = Document::from_text("Hello world"); assert!(border_lines(&d).is_empty()); @@ -1640,6 +1705,7 @@ fn character_border_draws_one_box_around_run() { #[test] fn equal_adjacent_borders_share_a_box() { + crate::test_fonts(); use wordcraft_doc::props::Border; let mut d = Document::from_text("ab"); d.format_range(&Pos::body(0, 0), &Pos::body(0, 2), &|c| c.border = Some(Border::single(0.5))).unwrap(); @@ -1651,6 +1717,7 @@ fn equal_adjacent_borders_share_a_box() { #[test] fn wrapped_border_boxes_each_line() { + crate::test_fonts(); use wordcraft_doc::props::Border; let text = "boxed ".repeat(20); let mut d = Document::from_text(text.trim_end()); @@ -1662,6 +1729,7 @@ fn wrapped_border_boxes_each_line() { #[test] fn runs_differing_only_in_link_or_decoration_keep_their_own_style() { + crate::test_fonts(); let mut d = Document::from_text("Alpha Beta Gamma Delta"); let set = |d: &mut Document, a: usize, b: usize, f: &dyn Fn(&mut wordcraft_doc::props::CharProps)| { d.format_range(&Pos::body(0, a), &Pos::body(0, b), f).unwrap(); @@ -1698,6 +1766,7 @@ fn runs_differing_only_in_link_or_decoration_keep_their_own_style() { #[test] fn web_view_is_one_page() { + crate::test_fonts(); let d = Document::from_text(&"text ".repeat(3000)); let mut c = LayoutCache::new(); let l = layout(&d, &mut c, &LayoutOptions { view: ViewMode::Web, web_width: 800.0, ..Default::default() }); @@ -1707,6 +1776,7 @@ fn web_view_is_one_page() { #[test] fn hostile_props_do_not_panic() { + crate::test_fonts(); let mut d = Document::from_text("x\ny"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p| { *p = ParaProps { indent_left: Some(1e9), indent_right: Some(1e9), indent_first: Some(-1e9), ..Default::default() } @@ -1720,6 +1790,7 @@ fn hostile_props_do_not_panic() { #[test] fn layout_is_fast() { + crate::test_fonts(); let d = Document::from_text(&"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.\n".repeat(3000)); let mut c = LayoutCache::new(); @@ -1737,6 +1808,7 @@ fn layout_is_fast() { #[test] fn empty_center_tab_then_right_tab() { + crate::test_fonts(); let mut d = Document::from_text("left\t\tright"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.style = Some("Header".into())).unwrap(); let l = lay(&d); @@ -1746,6 +1818,7 @@ fn empty_center_tab_then_right_tab() { #[test] fn footnotes_sit_at_page_bottom() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(30)); let mut note = wordcraft_doc::Paragraph::with_text("The note text.", Default::default()).styled("FootnoteText"); note.props.space_after = Some(0.0); @@ -1781,6 +1854,7 @@ fn footnotes_sit_at_page_bottom() { #[test] fn deleted_note_anchors_take_no_number_and_print_no_note() { + crate::test_fonts(); use wordcraft_doc::para::NoteKind; let mut d = Document::from_text("Alpha Beta Gamma"); let mut ids = Vec::new(); @@ -1827,6 +1901,7 @@ fn deleted_note_anchors_take_no_number_and_print_no_note() { #[test] fn hidden_note_anchors_keep_their_number_and_their_note() { + crate::test_fonts(); // Measured in Word (hidden text not printed): a hidden footnote or endnote reference mark still // takes its number, so the visible footnote after it is 2, and both notes are still printed. use wordcraft_doc::para::NoteKind; @@ -1873,6 +1948,7 @@ fn hidden_note_anchors_keep_their_number_and_their_note() { #[test] fn text_wraps_around_square_float() { + crate::test_fonts(); let mut d = Document::from_text(&"Words flow around the picture here. ".repeat(30)); let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::Square, @@ -1909,6 +1985,7 @@ fn text_wraps_around_square_float() { #[test] fn freeform_arrow_end_draws_at_the_last_point() { + crate::test_fonts(); use std::sync::Arc; use wordcraft_doc::connector::ShapeExtra; use wordcraft_doc::freeform::{FreePath, Freeform}; @@ -1954,6 +2031,7 @@ fn freeform_arrow_end_draws_at_the_last_point() { #[test] fn hidden_drawings_are_neither_drawn_nor_take_room() { + crate::test_fonts(); // A hidden floating shape (VML `visibility:hidden`, `wp:docPr/@hidden`): no wrap area, nothing // drawn, as in Word for Windows. A hidden inline one takes no room in its line. let mut d = Document::from_text(&"Words flow around the picture here. ".repeat(30)); @@ -2004,6 +2082,7 @@ fn hidden_drawings_are_neither_drawn_nor_take_room() { #[test] fn deleted_float_leaves_no_wrap_area_without_markup() { + crate::test_fonts(); // A square-wrapped shape anchored at the start of the paragraph, its anchor track-deleted. let text = "Words flow around the picture here. ".repeat(30); let mut d = Document::from_text(&text); @@ -2050,6 +2129,7 @@ fn deleted_float_leaves_no_wrap_area_without_markup() { #[test] fn hidden_float_leaves_no_wrap_area_when_hidden_text_is_not_shown() { + crate::test_fonts(); // A square-wrapped shape whose anchor is hidden text, through a character style (so only the // 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); @@ -2106,6 +2186,7 @@ fn hidden_float_leaves_no_wrap_area_when_hidden_text_is_not_shown() { /// overflowed when the next page or line was counted: a panic with overflow checks on. #[test] fn extreme_page_and_line_number_starts_do_not_overflow() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); for b in d.body.iter_mut() { if let wordcraft_doc::Block::Para(p) = std::sync::Arc::make_mut(b) { @@ -2120,6 +2201,7 @@ fn extreme_page_and_line_number_starts_do_not_overflow() { #[test] fn line_numbers_borders_text_boxes() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); d.last_section.line_numbers = Some(Default::default()); d.last_section.page_borders = Some(wordcraft_doc::props::Borders::box_(wordcraft_doc::props::Border::single(1.0))); @@ -2151,6 +2233,7 @@ fn line_numbers_borders_text_boxes() { #[test] fn tall_rows_split_across_pages() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); let long = "Row text that keeps going and going so the cell grows taller than a page. ".repeat(120); @@ -2257,6 +2340,7 @@ fn cell_line_bottoms(paras: &[(&str, f32)], i: usize) -> Vec { /// cell paragraph breaks 6 + 2). It used to move the whole row to the next page. #[test] fn a_split_row_keeps_its_bottom_margin_and_widow_control() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let paras = [(text.as_str(), 0.0)]; let bottoms = cell_line_bottoms(¶s, 0); @@ -2283,6 +2367,7 @@ fn a_split_row_keeps_its_bottom_margin_and_widow_control() { /// (Word keeps the "Remarks:" line on the page) instead of moving whole. #[test] fn a_split_row_keeps_the_last_paragraphs_space_after() { + crate::test_fonts(); let paras = [("Remarks:", 0.0), ("", 8.0)]; let bottom = cell_line_bottoms(¶s, 1).last().copied().unwrap_or(0.0); // The empty paragraph's line ends 6pt above the page bottom (720): 8pt after + 4pt margin @@ -2297,6 +2382,7 @@ fn a_split_row_keeps_the_last_paragraphs_space_after() { /// continuation is as tall as the cell needing the most room. #[test] fn split_row_cells_each_continue_at_the_top() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let row = |push: f32, singles: usize| { let mut d = Document::from_text("Filler"); @@ -2357,6 +2443,7 @@ fn split_row_cells_each_continue_at_the_top() { /// room is less than its minimum height. #[test] fn exact_rows_move_on_whole() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let lay_row = |rule: wordcraft_doc::props::HeightRule| { let mut d = Document::from_text("Filler"); @@ -2397,6 +2484,7 @@ fn exact_rows_move_on_whole() { /// which squashed the nested cell to 2pt on the next page. #[test] fn a_nested_cell_cut_by_the_page_bottom_keeps_its_rest() { + crate::test_fonts(); let mut d = Document::from_text("Filler"); if let Some(wordcraft_doc::Block::Para(p)) = d.body.first_mut().map(std::sync::Arc::make_mut) { p.props.space_before = Some(400.0); @@ -2444,6 +2532,7 @@ fn a_nested_cell_cut_by_the_page_bottom_keeps_its_rest() { /// behaviour). #[test] fn a_split_at_least_row_keeps_its_height() { + crate::test_fonts(); let mut d = Document::from_text("Filler"); let mut t = Table::new(1, 1, 400.0); t.rows[0].props.height = Some(1200.0); @@ -2468,6 +2557,7 @@ fn a_split_at_least_row_keeps_its_height() { /// on a NaN cut). #[test] fn split_row_rejects_a_hostile_cut() { + crate::test_fonts(); use crate::table::{CellPart, RowLayout, split_row}; let d = Document::from_text("Line"); let l = lay(&d); @@ -2500,6 +2590,7 @@ fn split_row_rejects_a_hostile_cut() { #[test] fn repeated_header_is_followed_by_the_next_row_even_when_it_overflows() { + crate::test_fonts(); // A one-column table at the top of a letter page (body 648 pt), then an empty paragraph. Returns // the table rows on each page, in order. let rows_per_page = |t: Table| -> Vec> { @@ -2547,6 +2638,7 @@ fn repeated_header_is_followed_by_the_next_row_even_when_it_overflows() { #[test] fn repeated_header_moves_with_the_first_row_that_does_not_fit() { + crate::test_fonts(); // A paragraph that leaves `room` points of the letter page's 648 pt body, then a table with a // 20 pt header row and 40 pt one-line rows. Returns the table rows on each page. let rows_per_page = |room: f32| -> Vec> { @@ -2574,6 +2666,7 @@ fn repeated_header_moves_with_the_first_row_that_does_not_fit() { /// A drop-cap paragraph in a font larger than 1000 pt used to panic in layout (`clamp` with min > max). #[test] fn drop_cap_with_a_huge_font_lays_out() { + crate::test_fonts(); for size in [999.0, 1000.0, 1001.0, 1638.0] { let cp = wordcraft_doc::CharProps { size: Some(size), ..Default::default() }; let mut p = wordcraft_doc::Paragraph::with_text("Hello drop cap world", cp); @@ -2591,6 +2684,7 @@ fn drop_cap_with_a_huge_font_lays_out() { #[test] fn drop_cap_indents_its_lines() { + crate::test_fonts(); let mut p = wordcraft_doc::Paragraph::with_text(&"Every line of this paragraph wraps around a large first letter. ".repeat(6), Default::default()); p.props.drop_cap = Some(3); @@ -2619,6 +2713,7 @@ fn drop_cap_indents_its_lines() { #[test] fn auto_hyphenation_breaks_long_words() { + crate::test_fonts(); let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(4); let mut d = Document::from_text(&text); let off = lay(&d); @@ -2652,6 +2747,7 @@ fn auto_hyphenation_breaks_long_words() { /// with a hyphen, and words in capitals stay whole when the document says so. #[test] fn hyphenation_options_limit_hyphens_and_spare_capitals() { + crate::test_fonts(); let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(6); let lines = |d: &Document| -> Vec { lay(d).pages[0] @@ -2706,6 +2802,7 @@ fn cell_para(l: &DocLayout, row: u32, col: u32) -> Arc { /// 8 pt after and 1.15 lines. #[test] fn table_style_paragraph_props_beat_document_defaults() { + crate::test_fonts(); let d = styled_table_doc(Some("TableGrid"), 3); let plain = styled_table_doc(None, 3); let (lg, lp) = (lay(&d), lay(&plain)); @@ -2722,6 +2819,7 @@ fn table_style_paragraph_props_beat_document_defaults() { /// table style only fills in what they leave unset. #[test] fn paragraph_style_and_direct_formatting_beat_table_style() { + crate::test_fonts(); let mut d = styled_table_doc(Some("TableGrid"), 3); d.styles.upsert(wordcraft_doc::Style { id: "Spaced".into(), @@ -2759,6 +2857,7 @@ fn paragraph_style_and_direct_formatting_beat_table_style() { /// adds its own run formatting and conditional formats. #[test] fn derived_table_style_merges_its_base() { + crate::test_fonts(); use wordcraft_doc::styles::TableStyleParts; let red = wordcraft_doc::Rgb(0xC0, 0, 0); let mut d = styled_table_doc(Some("RedGrid"), 3); @@ -2825,6 +2924,7 @@ fn box_rect(l: &DocLayout, id: u32) -> wordcraft_geom::Rect { #[test] fn clicks_inside_a_text_box_hit_its_story() { + crate::test_fonts(); let mut d = Document::from_text("Before after"); let id = text_box(&mut d, 7, "Inside the box", 144.0, 72.0, Default::default()); let l = lay(&d); @@ -2848,6 +2948,7 @@ fn clicks_inside_a_text_box_hit_its_story() { #[test] fn body_text_wins_over_a_text_box_behind_it() { + crate::test_fonts(); let mut d = Document::from_text("Hi"); let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::BehindText, @@ -2869,6 +2970,7 @@ fn body_text_wins_over_a_text_box_behind_it() { #[test] fn presses_grab_pictures_anywhere_and_text_boxes_by_their_border() { + crate::test_fonts(); let mut d = Document::from_text("Words before the objects."); let id = text_box(&mut d, 0, "Inside", 144.0, 72.0, Default::default()); let shape = InlineObject::Shape { @@ -2924,6 +3026,7 @@ fn box_lines(l: &DocLayout, id: u32) -> (usize, usize, wordcraft_geom::Rect) { #[test] fn text_box_hides_text_that_does_not_fit() { + crate::test_fonts(); let long = "The quick brown fox jumps over the lazy dog. ".repeat(12); let mut d = Document::from_text("Body"); let id = text_box(&mut d, 0, &long, 144.0, 72.0, Default::default()); @@ -2944,6 +3047,7 @@ fn text_box_hides_text_that_does_not_fit() { #[test] fn square_wrap_flows_text_on_both_sides() { + crate::test_fonts(); let float = |x: f32| wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::Square, h_rel: wordcraft_doc::para::Anchor::Column, @@ -2994,6 +3098,7 @@ fn square_wrap_flows_text_on_both_sides() { /// still use it, and a word wider than the whole line still breaks where it is. #[test] fn a_word_too_wide_for_the_strip_beside_a_float_moves_on_whole() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; // A `w` x 150pt square at `x` in the 468pt column, leaving a 32pt strip on its right. Each // line: (top, left, beside the object's far side, ends inside a word, where its text ends). @@ -3076,6 +3181,7 @@ fn page_float(x: f32, y: f32) -> wordcraft_doc::para::Float { #[test] fn text_boxes_show_in_headers_footers_cells_and_notes() { + crate::test_fonts(); let mut d = Document::from_text(&"Body paragraph.\n".repeat(4)); // Header: an inline box and a box placed on the page. let hid = @@ -3119,6 +3225,7 @@ fn text_boxes_show_in_headers_footers_cells_and_notes() { #[test] fn footnotes_inside_text_boxes_are_numbered_and_placed() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(30)); let id = text_box_at(&mut d, &Pos::body(2, 0), "Box text", 144.0, 40.0, Default::default()); let e = d.end_of(StoryRef::Part(id)); @@ -3172,6 +3279,7 @@ fn box_fan_out(levels: usize, fan: usize) -> Document { #[test] fn self_showing_and_fanned_out_text_boxes_stay_bounded() { + crate::test_fonts(); // A box whose 30 shapes all show itself: laid out once. assert_eq!(lay(&box_fan_out(1, 30)).text_boxes, 1); // Chains of boxes each showing the next 30 times (30^4 expansions unbounded). @@ -3186,6 +3294,7 @@ fn self_showing_and_fanned_out_text_boxes_stay_bounded() { #[test] fn many_text_boxes_and_a_header_box_all_show() { + crate::test_fonts(); // Outermost boxes aren't budgeted: a long document's boxes, and its header's on every page, // all get their text. let mut d = Document::from_text("Paragraph with a box."); @@ -3212,6 +3321,7 @@ fn many_text_boxes_and_a_header_box_all_show() { #[test] fn a_text_box_inside_a_text_box_still_shows_its_text() { + crate::test_fonts(); let mut d = Document::from_text("Body"); let outer = text_box(&mut d, 0, "Outer ", 300.0, 200.0, Default::default()); let e = d.end_of(StoryRef::Part(outer)); @@ -3239,6 +3349,7 @@ fn picture(w: f32, h: f32, float: wordcraft_doc::para::Float) -> InlineObject { #[test] fn line_spacing_does_not_scale_pictures() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; let mut d = Document::from_text(""); if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) { @@ -3256,6 +3367,7 @@ fn line_spacing_does_not_scale_pictures() { #[test] fn multiple_line_spacing_adds_its_leading_below_the_text() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; // (spacing, expected baseline below the line top as a share of the ascent) for (spacing, share) in [(LineSpacing::Multiple(1.15), 1.0), (LineSpacing::Multiple(2.0), 1.0), (LineSpacing::Multiple(0.8), 0.8)] { @@ -3290,6 +3402,7 @@ fn multiple_line_spacing_adds_its_leading_below_the_text() { #[test] fn list_label_does_not_inflate_an_inline_picture_floor() { + crate::test_fonts(); use wordcraft_doc::props::{LineSpacing, NumRef}; for spacing in [LineSpacing::Multiple(1.25), LineSpacing::AtLeast(12.0)] { let mut d = Document::from_text(""); @@ -3310,6 +3423,7 @@ fn list_label_does_not_inflate_an_inline_picture_floor() { #[test] fn body_starts_right_below_a_tall_header() { + crate::test_fonts(); let mut d = Document::from_text("Body"); let mut hp = wordcraft_doc::Paragraph::with_text("", Default::default()); hp.insert_object(0, picture(100.0, 80.0, Default::default()), &Default::default()).unwrap(); @@ -3327,6 +3441,7 @@ fn body_starts_right_below_a_tall_header() { #[test] fn inline_picture_keeps_room_for_its_effects() { + crate::test_fonts(); let mut d = Document::from_text(""); let shadow = wordcraft_doc::para::Float { effect: [6.0, 12.0, 18.0, 27.0], ..Default::default() }; if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) { @@ -3349,6 +3464,7 @@ fn line_tops(l: &DocLayout) -> Vec { #[test] fn at_least_rows_add_cell_margins_and_border_bands() { + crate::test_fonts(); use wordcraft_doc::props::{Border, BorderStyle, Borders, HeightRule}; let mut d = Document::from_text(""); let line = Some(Border { style: BorderStyle::Single, width: 0.5, color: None, space: 0.0 }); @@ -3372,6 +3488,7 @@ fn at_least_rows_add_cell_margins_and_border_bands() { /// own lines up with a neighbour that has them, and the row is as tall as if all had them. #[test] fn a_row_shares_its_largest_top_and_bottom_margins() { + crate::test_fonts(); let mut d = Document::from_text(""); let mut t = Table::new(3, 2, 300.0); t.props.cell_margins = Some([0.0, 5.4, 0.0, 5.4]); @@ -3397,6 +3514,7 @@ fn a_row_shares_its_largest_top_and_bottom_margins() { /// and bottom margins steps 39pt; the text starts 4pt below its top). #[test] fn exact_rows_add_only_the_bottom_margin() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; let mut d = Document::from_text(""); let mut t = Table::new(2, 1, 200.0); @@ -3416,6 +3534,7 @@ fn exact_rows_add_only_the_bottom_margin() { /// each cell's text starts its left margin inside the cell. #[test] fn cell_margins_are_where_cell_text_starts() { + crate::test_fonts(); let mut d = Document::from_text(""); let mut t = Table::new(1, 2, 300.0); t.props.cell_margins = Some([0.0, 9.0, 0.0, 3.0]); @@ -3467,6 +3586,7 @@ fn justified_lines(text: &str, compat_mode: u32, align: Align) -> (usize, Vec f32 { #[test] fn floats_align_within_their_reference_area() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap}; let mut d = Document::from_text("Some text beside the pictures."); let centred = Float { wrap: Wrap::InFrontOfText, h_rel: Anchor::Margin, h_align: Some(FloatAlign::Center), x: 999.0, ..Default::default() }; @@ -3549,6 +3672,7 @@ fn floats_align_within_their_reference_area() { #[test] fn top_and_bottom_float_pushes_its_own_first_line_down() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&"Text that starts below the picture. ".repeat(10)); let f = Float { wrap: Wrap::TopAndBottom, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist_bottom: 6.0, ..Default::default() }; @@ -3564,6 +3688,7 @@ fn top_and_bottom_float_pushes_its_own_first_line_down() { #[test] fn float_follows_its_paragraph_to_the_next_page() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&format!("First page.\n{}", "Text beside the picture on page two. ".repeat(20))); let f = Float { wrap: Wrap::Square, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist: 9.0, ..Default::default() }; @@ -3585,6 +3710,7 @@ fn float_follows_its_paragraph_to_the_next_page() { /// break and then the page-anchored text box with the title; the box stays on the cover. #[test] fn a_float_anchored_after_a_page_break_stays_on_its_paragraph_s_first_page() { + crate::test_fonts(); let mut d = Document::from_text(&format!("{}\nAfter the cover", wordcraft_doc::para::PAGE_BREAK)); d.insert_object(&Pos::body(0, 1), rect_shape(200.0, 100.0, page_float(100.0, 300.0)), &Default::default()).unwrap(); let l = lay(&d); @@ -3597,6 +3723,7 @@ fn a_float_anchored_after_a_page_break_stays_on_its_paragraph_s_first_page() { #[test] fn floats_in_headers_and_table_cells_are_drawn() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text("Body text."); let logo = Float { wrap: Wrap::BehindText, h_rel: Anchor::Page, v_rel: Anchor::Page, x: 400.0, y: 20.0, ..Default::default() }; @@ -3622,6 +3749,7 @@ fn floats_in_headers_and_table_cells_are_drawn() { #[test] fn wrap_area_uses_each_distance() { + crate::test_fonts(); use wordcraft_doc::para::{Float, Wrap}; let f = Float { wrap: Wrap::Square, dist: 9.0, dist_top: 0.0, dist_bottom: 4.0, ..Default::default() }; let (r, tb, ..) = wrap_area(Rect::new(10.0, 20.0, 100.0, 50.0), &f, None).unwrap(); @@ -3634,6 +3762,7 @@ fn wrap_area_uses_each_distance() { #[test] fn float_moved_to_a_new_page_sits_at_its_paragraph_top() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&format!("First page.\n{}", "Text beside the picture on page two. ".repeat(20))); let f = Float { wrap: Wrap::Square, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist: 9.0, ..Default::default() }; @@ -3653,6 +3782,7 @@ fn float_moved_to_a_new_page_sits_at_its_paragraph_top() { #[test] fn page_relative_header_float_wraps_where_it_is_drawn() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let body_top = |wrap| { let mut d = Document::from_text("Body text."); @@ -3674,6 +3804,7 @@ fn page_relative_header_float_wraps_where_it_is_drawn() { /// at the top margin and paginates exactly as without the box. #[test] fn floating_header_text_box_leaves_the_body_at_the_top_margin() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; let body: Vec = (1..=40).map(|i| format!("Body paragraph {i}: synthetic public test content.")).collect(); let make = |float: Option| { @@ -3745,6 +3876,7 @@ fn numbered(d: &mut Document, texts: &[&str], props: ParaProps) -> Vec = (0..24).map(|i| format!("Paragraph {i} before the table.")).collect(); @@ -3920,6 +4059,7 @@ fn a_page_anchored_floating_table_after_a_full_page_moves_to_the_next() { /// on that page: no blank page between the two tables around it. #[test] fn a_page_break_at_the_bottom_of_a_full_page_leaves_no_blank_page() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; let props = ParaProps { line_spacing: Some(LineSpacing::Exactly(24.0)), space_before: Some(0.0), space_after: Some(0.0), ..Default::default() }; let para = |text: &str| { @@ -3941,6 +4081,7 @@ fn a_page_break_at_the_bottom_of_a_full_page_leaves_no_blank_page() { /// size and win over the indent in points. #[test] fn character_indents_use_the_paragraph_mark_size() { + crate::test_fonts(); let mut p = wordcraft_doc::Paragraph::with_text(&"字".repeat(80), Default::default()); p.props.indent_first = Some(5.0); p.props.indent_first_chars = Some(2.0); @@ -3956,6 +4097,7 @@ fn character_indents_use_the_paragraph_mark_size() { #[test] fn no_line_break_right_after_a_slash() { + crate::test_fonts(); // Word keeps "and/or" and web addresses whole on a line when they fit. let text = "word and/or https://example.org/keepers/log/winter ".repeat(40); let d = Document::from_text(&text); @@ -3976,6 +4118,7 @@ fn no_line_break_right_after_a_slash() { #[test] fn unequal_formats_lay_out_in_linear_time() { + crate::test_fonts(); // Runs whose formats never compare equal (NaN spacing from a hostile file) each get a style; // finding a run's style must not compare it against every earlier one. let doc = |n: usize| { @@ -3998,6 +4141,7 @@ fn unequal_formats_lay_out_in_linear_time() { #[test] fn formats_past_65536_in_a_paragraph_keep_their_own_style() { + crate::test_fonts(); // More distinct formats than a 16-bit index counts (the paragraph mark takes a style too): every // run past that limit still draws with its own link, deletion and size, not an earlier run's. let n = usize::from(u16::MAX) + 3; @@ -4030,6 +4174,7 @@ fn formats_past_65536_in_a_paragraph_keep_their_own_style() { #[test] fn hidden_float_in_a_table_cell_takes_no_room() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; // A square-wrapped shape anchored in hidden text inside a table cell, beside wrapping text. let mut d = Document::from_text("Body text."); @@ -4057,6 +4202,7 @@ fn hidden_float_in_a_table_cell_takes_no_room() { #[test] fn objects_left_out_through_the_table_style_formatting() { + crate::test_fonts(); // The table style's character formatting sits under the runs' own, as the cell is laid out. let mut p = wordcraft_doc::Paragraph::with_text("ab", Default::default()); p.insert_object(1, rect_shape(10.0, 10.0, Default::default()), &Default::default()).unwrap(); @@ -4069,6 +4215,7 @@ fn objects_left_out_through_the_table_style_formatting() { #[test] fn hidden_float_in_a_text_box_is_not_placed() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; // A shape anchored in hidden text inside a text box: no drawing, no hit area. let mut d = Document::from_text("Body text."); @@ -4090,6 +4237,7 @@ fn hidden_float_in_a_text_box_is_not_placed() { #[test] fn inline_graphic_reserves_its_height() { + crate::test_fonts(); let mut d = Document::from_text("Chart\nAfter"); let obj = InlineObject::Graphic { w: 216.0, h: 144.0, alt: String::new(), float: Default::default(), graphic: Arc::new(Default::default()) }; d.insert_object(&Pos::body(0, 0), obj, &Default::default()).unwrap(); @@ -4101,6 +4249,7 @@ fn inline_graphic_reserves_its_height() { #[test] fn graphic_items_draw_inside_the_object() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg, TextAlign}; use wordcraft_doc::props::Rgb; let items = vec![ @@ -4144,6 +4293,7 @@ fn graphic_items_draw_inside_the_object() { #[test] fn graphic_text_in_a_degenerate_rectangle_is_not_drawn() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, TextAlign}; use wordcraft_doc::props::Rgb; let text = |rect: [f32; 4], size: f32| GraphicItem::Text { @@ -4171,6 +4321,7 @@ fn flat(draws: Vec) -> Vec { #[test] fn inline_chart_is_a_figure_with_its_alt_text() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg}; use wordcraft_doc::props::Rgb; let items = vec![GraphicItem::Path { @@ -4190,6 +4341,7 @@ fn inline_chart_is_a_figure_with_its_alt_text() { #[test] fn graphic_paths_drop_segments_that_cannot_be_drawn() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg}; use wordcraft_doc::props::Rgb; // The line before the first move, the move with a NaN, the infinite line and the line after the @@ -4216,6 +4368,7 @@ fn graphic_paths_drop_segments_that_cannot_be_drawn() { #[test] fn graphic_items_scale_with_their_frame() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind}; use wordcraft_doc::props::Rgb; // A chart drawn for 1000 x 500 pt in a 500 x 250 pt frame: its items and strokes scale by half. @@ -4241,6 +4394,7 @@ fn graphic_items_scale_with_their_frame() { #[test] fn inline_picture_wider_than_a_text_box_is_fitted_to_it() { + crate::test_fonts(); // Text boxes don't clip what runs past their sides, so a 400 x 100 pt picture in a 144 pt box // is fitted to the box's 129.6 pt inside its margins instead of spilling over the page. let mut d = Document::from_text("Before"); @@ -4275,6 +4429,7 @@ fn inline_picture_wider_than_a_text_box_is_fitted_to_it() { #[test] fn inline_picture_wider_than_the_text_keeps_its_size() { + crate::test_fonts(); // A 1000 x 100 pt picture in 468 pt of text: Word draws it at its size, past the margin, and the // page edge clips it; the line is as tall as the picture (docxide-pdf's case179). let mut d = Document::from_text(""); @@ -4292,6 +4447,7 @@ fn inline_picture_wider_than_the_text_keeps_its_size() { #[test] fn a_picture_too_wide_after_a_content_control_start_stays_on_the_first_line() { + crate::test_fonts(); // The picture opening a picture content control (a logo in a text box a little too // narrow for it): the control's start marker before it takes no room, so the picture is // still the line's first item and stays there instead of leaving an empty line above it. @@ -4322,6 +4478,7 @@ fn para_at(l: &DocLayout, path: &[u32]) -> Vec<(f32, f32, usize)> { #[test] fn a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::TableFloat; let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); @@ -4349,6 +4506,7 @@ fn a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it() { #[test] fn autofit_sizes_columns_by_their_content() { + crate::test_fonts(); let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); // No width anywhere: the columns come from their content. let mut t = Table::new(1, 2, 468.0); @@ -4373,6 +4531,7 @@ fn autofit_sizes_columns_by_their_content() { #[test] fn autofit_handles_spanning_cells_long_words_and_empty_tables() { + crate::test_fonts(); let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); let table_x = |t: Table, path: &[u32]| { let mut d = Document::from_text("After"); @@ -4414,6 +4573,7 @@ fn autofit_handles_spanning_cells_long_words_and_empty_tables() { #[test] fn an_inherited_footer_sizes_its_percentage_table_by_its_own_section() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // Measured in Word: a 100%-wide footer table defined by a portrait section keeps that width on // a landscape page that inherits the footer, centred there; a landscape section naming the @@ -4445,19 +4605,19 @@ fn an_inherited_footer_sizes_its_percentage_table_by_its_own_section() { #[test] fn a_row_with_less_room_than_its_minimum_height_moves_on_whole() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; // Measured in Word: with 49 pt left on the page, a row of eight lines splits, but the same row // with an at-least height of 63.6 pt goes to the next page whole. let first_row_line_page = |min: Option| { - let lines: Vec = (1..=200).map(|i| format!("Filler {i}")).collect(); - let one_page = lay(&Document::from_text(&lines.join("\n"))).pages[0].items.iter().filter(|i| matches!(i, Placed::Lines { .. })).count(); let mut t = Table::new(1, 1, 400.0); t.rows[0].props.height = min; t.rows[0].props.height_rule = HeightRule::AtLeast; t.rows[0].cells[0].blocks = (1..=8).map(|k| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(&format!("Row line {k}"), Default::default()))).collect(); - // Fill the page, leaving two filler paragraphs' room (less than 63.6 pt) for the row. - let mut d = Document::from_text(&lines[..one_page - 2].join("\n")); + // With the pinned Source Sans 3 fallback, 23 Normal paragraphs leave 43.13 pt. + let lines: Vec = (1..=23).map(|i| format!("Filler {i}")).collect(); + let mut d = Document::from_text(&lines.join("\n")); let end = d.body.len(); d.insert_block(StoryRef::Body, &Path::top(end), wordcraft_doc::Block::Table(t)).unwrap(); let l = lay(&d); @@ -4469,6 +4629,7 @@ fn a_row_with_less_room_than_its_minimum_height_moves_on_whole() { #[test] fn text_overflowing_an_exact_row_is_clipped_to_it() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; // An exactly 12 pt row holding three lines: Word shows what fits and cuts the rest at the row's // edge, rather than drawing it over the rows below. @@ -4495,6 +4656,7 @@ fn text_overflowing_an_exact_row_is_clipped_to_it() { #[test] fn a_percentage_width_past_the_text_column_is_kept() { + crate::test_fonts(); // `w:tblW w:type="pct" w:w="6564"` (131.28%) with the left margin as a negative indent: a // cover-page footer table that spans the page edge to edge. let mut t = Table::new(1, 2, 234.0); @@ -4512,6 +4674,7 @@ fn a_percentage_width_past_the_text_column_is_kept() { #[test] fn a_floating_table_in_the_footer_leaves_the_footer_where_it_is() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::TableFloat; let footer_y = |float: bool| { @@ -4536,6 +4699,7 @@ fn a_floating_table_in_the_footer_leaves_the_footer_where_it_is() { #[test] fn a_split_row_moves_shading_below_the_cut_with_it() { + crate::test_fonts(); use crate::table::{CellPart, RowLayout, split_row}; let line_row = |y: f32| { let d = Document::from_text("Line"); @@ -4566,6 +4730,7 @@ fn a_split_row_moves_shading_below_the_cut_with_it() { #[test] fn kinsoku_keeps_a_manual_line_break_before_a_closing_bracket() { + crate::test_fonts(); // #269: kinsoku forbids a line starting with 」, but a manual line break (Shift+Enter) right // before one still ends the line there. let text = "日本語\n」テスト"; @@ -4591,6 +4756,7 @@ fn kinsoku_keeps_a_manual_line_break_before_a_closing_bracket() { /// testing follows the turned shape, not its frame. #[test] fn rotated_float_wraps_around_its_rotated_bounds() { + crate::test_fonts(); let text = "Words flow around the turned shape here. ".repeat(30); let place = |rot: f32| { let mut d = Document::from_text(&text); @@ -4646,6 +4812,7 @@ fn rotated_float_wraps_around_its_rotated_bounds() { /// its page direction inside the turn; one that does turns with the shape. #[test] fn rotated_group_turns_its_members_and_their_shadows() { + crate::test_fonts(); use wordcraft_doc::effects::{Shadow, ShapeEffects}; use wordcraft_doc::para::{Anchor, Float, GroupChild, ShapeKind, Wrap}; let member = |rot_with_shape: bool, rot: f32| InlineObject::Shape { @@ -4734,6 +4901,7 @@ fn rules_below(l: &DocLayout, pi: usize, y: f32) -> Vec { #[test] fn long_footnotes_continue_on_the_next_page() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let long = footnote(&mut d, &Pos::body(4, 4), &"A long footnote that runs on and on. ".repeat(100)); let own = footnote(&mut d, &Pos::body(14, 4), "The second page's own note."); @@ -4778,6 +4946,7 @@ fn long_footnotes_continue_on_the_next_page() { #[test] fn short_footnotes_stay_whole() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(60)); let id = footnote(&mut d, &Pos::body(10, 4), "A short note, two lines long. ".repeat(4).as_str()); let l = lay(&d); @@ -4788,6 +4957,7 @@ fn short_footnotes_stay_whole() { #[test] fn footnote_longer_than_pages_ends() { + crate::test_fonts(); // A note several pages long after a one-line body: it continues on pages of its own. let mut d = Document::from_text("Body"); let id = footnote(&mut d, &Pos::body(0, 4), &"Words in a very long note. ".repeat(1500)); @@ -4830,6 +5000,7 @@ fn comment_lines(l: &DocLayout, pi: usize) -> Vec<(StoryRef, f32, f32)> { /// reply are laid out there beside the line they comment on. #[test] fn comment_balloons_widen_pages_beside_their_anchors() { + crate::test_fonts(); let mut d = Document::from_text("Intro line.\nThe quarterly figures look strong.\nClosing line."); let id = comment_on(&mut d, 1, 4, 13, "Ada Reviewer", "Check the totals", None); comment_on(&mut d, 0, 0, 0, "Ben Editor", "Totals checked", Some(id)); @@ -4856,6 +5027,7 @@ fn comment_balloons_widen_pages_beside_their_anchors() { #[test] fn no_balloons_without_anchored_comments() { + crate::test_fonts(); let d = Document::from_text("No comments here."); let l = balloon_layout(&d); assert_eq!(l.pages[0].markup, 0.0); @@ -4872,6 +5044,7 @@ fn no_balloons_without_anchored_comments() { /// one page. Nothing panics or hangs, and the cards stay on the page. #[test] fn comment_balloons_survive_hostile_comments() { + crate::test_fonts(); let mut d = Document::from_text(&"Commented words here. ".repeat(20)); let mut ids = Vec::new(); for i in 0..120 { @@ -4925,6 +5098,7 @@ fn wrapped_shape_doc(kind: wordcraft_doc::para::ShapeKind, w: f32, h: f32, float #[test] fn tight_wrap_follows_an_ellipse() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; let spans = |wrap| line_spans(&lay(&wrapped_shape_doc(ShapeKind::Ellipse, 160.0, 160.0, Float { wrap, ..Default::default() }))); let (square, tight, through) = (spans(Wrap::Square), spans(Wrap::Tight), spans(Wrap::Through)); @@ -4962,6 +5136,7 @@ fn tight_wrap_follows_an_ellipse() { #[test] fn contour_wrap_turns_with_the_object() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; use wordcraft_doc::wrap::WrapPolygon; // A wrap polygon covering the left half of a 200 × 100 frame. @@ -4989,6 +5164,7 @@ fn contour_wrap_turns_with_the_object() { /// symbol" a bigger shape around text at its own size. Plain `\o` overstrikes its arguments. #[test] fn enclosed_characters_draw_a_shape_around_the_text() { + crate::test_fonts(); use wordcraft_doc::eq::{EncloseShape, EncloseStyle, Enclosure}; let draw = |shape: EncloseShape, style: EncloseStyle| { let mut d = Document::from_text("a b"); @@ -5043,6 +5219,7 @@ fn enclosed_characters_draw_a_shape_around_the_text() { #[test] fn ruby_text_sits_over_its_base_and_the_line_grows_to_fit() { + crate::test_fonts(); use wordcraft_doc::para::RubyAlign; let plain = Document::from_text("Plain words here"); let mut d = Document::from_text("Plain here"); @@ -5088,6 +5265,7 @@ fn ruby_text_sits_over_its_base_and_the_line_grows_to_fit() { /// only to its right; Largest: only on its wider side. #[test] fn wrap_text_keeps_text_on_one_side() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap, WrapText}; // A 100pt shape at `x` in the 468pt column; the lines beside it. let beside = |kind, wrap, x: f32, side| { @@ -5114,6 +5292,7 @@ fn wrap_text_keeps_text_on_one_side() { #[test] fn document_note_separators_and_continuation_notice() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let long = footnote(&mut d, &Pos::body(4, 4), &"A long footnote that runs on and on. ".repeat(100)); let sep = |cont: bool| { @@ -5169,6 +5348,7 @@ fn document_note_separators_and_continuation_notice() { /// frame, so the clip turns too); floating or inline. Hit testing follows the turned canvas. #[test] fn rotated_canvas_draws_its_clipped_members_inside_the_turn() { + crate::test_fonts(); use crate::display::Draw; use wordcraft_doc::para::{Anchor, CanvasStyle, Float, GroupChild, ShapeKind, Wrap}; let member = InlineObject::Shape { @@ -5258,6 +5438,7 @@ fn rotated_canvas_draws_its_clipped_members_inside_the_turn() { #[test] fn mirror_indents_swap_sides_on_even_pages() { + crate::test_fonts(); // #512: with mirrored indents the left indent is the inside one, on the right of an even page. let mut d = Document::from_text("odd page\neven page"); let last = Pos::body(1, 0); @@ -5318,6 +5499,7 @@ fn column_bottoms(l: &DocLayout) -> (usize, f32, usize, f32, (usize, f32)) { #[test] fn columns_before_a_continuous_break_are_balanced() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; for widths in [vec![], vec![(150.0, 36.0), (282.0, 0.0)]] { let l = lay(&two_column_section(SectionStart::Continuous, widths.clone())); @@ -5333,6 +5515,7 @@ fn columns_before_a_continuous_break_are_balanced() { #[test] fn columns_before_a_page_break_or_a_column_break_are_not_balanced() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; // Next-page break: the ten paragraphs fill the first column; the next section is on page 2. let l = lay(&two_column_section(SectionStart::NextPage, vec![])); @@ -5366,6 +5549,7 @@ fn note_marks(l: &DocLayout) -> HashMap> { #[test] fn footnotes_restart_on_each_page_and_section() { + crate::test_fonts(); use wordcraft_doc::section::{NoteProps, NoteRestart}; let mut d = Document::from_text(&"Body text line.\n".repeat(120)); let a = footnote(&mut d, &Pos::body(2, 4), "First note."); @@ -5391,6 +5575,7 @@ fn footnotes_restart_on_each_page_and_section() { #[test] fn endnotes_at_section_end_and_footnotes_below_text() { + crate::test_fonts(); use wordcraft_doc::section::{NotePos, NoteProps}; let mut d = Document::from_text("Alpha\nBeta\nGamma\nDelta"); let eid = d.add_part( @@ -5426,6 +5611,7 @@ fn endnotes_at_section_end_and_footnotes_below_text() { /// styles): Polish patterns and dictionary for Polish runs, English ones for English runs. #[test] fn hyphenation_and_proofing_follow_each_runs_language() { + crate::test_fonts(); // A tiny synthetic Polish dictionary (real ones are installed on the system, not bundled). wordcraft_proof::pl::install_dictionary("SET UTF-8\nSFX B Y 1\nSFX B y ego y\n", "2\nkonstytucyjny/B\npłynie\n").unwrap(); let text = "Konstytucyjnego żeka typography beautifull"; @@ -5460,6 +5646,7 @@ fn hyphenation_and_proofing_follow_each_runs_language() { /// or grammar" text none either; the document default language applies to text without its own. #[test] fn unproofed_languages_and_no_proof_runs_are_not_marked() { + crate::test_fonts(); // A tiny synthetic Polish dictionary (real ones are installed on the system, not bundled). wordcraft_proof::pl::install_dictionary("SET UTF-8\nSFX B Y 1\nSFX B y ego y\n", "2\nkonstytucyjny/B\npłynie\n").unwrap(); let opts = LayoutOptions { proofing: true, ..LayoutOptions::default() }; @@ -5484,6 +5671,7 @@ fn unproofed_languages_and_no_proof_runs_are_not_marked() { /// ignored), on every page, like the same fields in the footer. #[test] fn body_page_totals_count_every_page() { + crate::test_fonts(); let mut d = Document::from_text("Body-1\nBody-2\nBody-3"); let field = |instr: &str| InlineObject::Field { instr: instr.into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; @@ -5522,6 +5710,7 @@ fn body_page_totals_count_every_page() { /// a body paragraph. #[test] fn table_cell_footnotes_are_placed() { + crate::test_fonts(); let mut d = Document::from_text("After the table."); d.insert_block(StoryRef::Body, &Path::top(0), wordcraft_doc::Block::Table(wordcraft_doc::Table::new(1, 1, 300.0))).unwrap(); let cell = Pos { story: StoryRef::Body, path: Path(vec![0, 0, 0, 0]), off: 0 }; @@ -5537,6 +5726,7 @@ fn table_cell_footnotes_are_placed() { #[test] fn east_asian_text_takes_the_asian_font_and_latin_text_the_latin_font() { + crate::test_fonts(); // #505: one run with a Latin font and an Asian font (w:eastAsia) of its own sets each script in // its font. Prefer an installed CJK font other than the one fallback would pick anyway. let resolve = |f: &str| wordcraft_fonts::word::resolve(f, false, false); @@ -5578,6 +5768,7 @@ fn east_asian_text_takes_the_asian_font_and_latin_text_the_latin_font() { /// the body lay out a second time; one in the body still does. #[test] fn footer_page_total_needs_one_body_pass() { + crate::test_fonts(); let field = || InlineObject::Field { instr: "NUMPAGES".into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; let mut d = Document::from_text("Body-1\nBody-2\nBody-3"); @@ -5604,6 +5795,7 @@ fn footer_page_total_needs_one_body_pass() { /// Cyrillic text in an English run is not hyphenated (the English data has nothing to say about it). #[test] fn bulgarian_text_is_hyphenated_by_its_own_rules() { + crate::test_fonts(); let text = "Сричкопренасянето в професионалните документи задължително следва правописните правила, разработени от специалистите. ".repeat(6); let hyphenated = |d: &Document| -> Vec<(String, String)> { let mut out = Vec::new(); @@ -5641,6 +5833,7 @@ fn bulgarian_text_is_hyphenated_by_its_own_rules() { /// grow it). Tables of contents with big tab runs were a third taller than Word's. #[test] fn spaces_and_tabs_do_not_make_a_line_taller() { + crate::test_fonts(); use wordcraft_doc::props::CharProps; let small = CharProps { size: Some(9.0), ..Default::default() }; let big = CharProps { size: Some(20.0), ..Default::default() }; @@ -5676,6 +5869,7 @@ fn spaces_and_tabs_do_not_make_a_line_taller() { /// the page bottom. #[test] fn a_list_label_adds_ascent_but_not_descent() { + crate::test_fonts(); let first_line = |label_size: Option| { let mut d = Document::new(); let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); @@ -5706,6 +5900,7 @@ fn a_list_label_adds_ascent_but_not_descent() { /// with text is never hidden. NIH's Table A showed a blank row Word doesn't. #[test] fn a_hidden_cell_mark_takes_no_room() { + crate::test_fonts(); let row3_top = |hide: [bool; 2], second: Option<&str>| { let mut d = Document::from_text("Above"); let mut t = Table::new(3, 2, 300.0); @@ -5760,6 +5955,7 @@ fn a_hidden_cell_mark_takes_no_room() { /// an ordinary paragraph, a line of its own above the footer text. #[test] fn a_text_frame_in_a_cell_floats_beside_the_text() { + crate::test_fonts(); use wordcraft_doc::props::TextFrame; let mut d = Document::from_text("Above"); let mut t = Table::new(1, 1, 300.0); @@ -5789,6 +5985,7 @@ fn a_text_frame_in_a_cell_floats_beside_the_text() { /// the list's 0.6", and its "sample text" paragraphs with the list off at 0.9" instead of the margin. #[test] fn list_indents_and_style_indents_combine_like_word() { + crate::test_fonts(); use wordcraft_doc::props::NumRef; use wordcraft_doc::styles::{Style, StyleKind}; let text_x = |style: &str, own: ParaProps| { @@ -5840,6 +6037,7 @@ fn list_indents_and_style_indents_combine_like_word() { /// five-line kept paragraph left a third of a page empty. #[test] fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() { + crate::test_fonts(); // Page of each of the kept paragraph's 5 lines and of the first paragraph after it. let pages = |fillers: usize, kept: ParaProps, next: ParaProps| { let mut d = Document::new(); @@ -5886,6 +6084,7 @@ fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() { /// and the heading keeps it company. #[test] fn keep_with_next_carries_the_heading_when_widow_control_moves_the_next_paragraph() { + crate::test_fonts(); // Page of the heading, then of the next paragraph's lines at "Bb", "Bc" and "Be". let pages = |fillers: usize, widow: bool| -> [usize; 4] { let mut d = Document::new(); @@ -5930,6 +6129,7 @@ fn keep_with_next_carries_the_heading_when_widow_control_moves_the_next_paragrap /// over when the last one cannot start on the page, and all stay when it can. #[test] fn keep_with_next_chain_carries_over_when_the_last_paragraph_cannot_start() { + crate::test_fonts(); let pages = |fillers: usize, widow: bool| -> Vec { let mut d = Document::new(); let plain = ParaProps { @@ -5972,6 +6172,7 @@ fn keep_with_next_chain_carries_over_when_the_last_paragraph_cannot_start() { /// of a paragraph that keep lines together will not split (#966). #[test] fn keep_with_next_carries_the_heading_when_keep_lines_moves_the_next_paragraph() { + crate::test_fonts(); let pages = |fillers: usize, kept_lines: bool| -> Vec { let mut d = Document::new(); let plain = ParaProps { @@ -6016,6 +6217,7 @@ fn keep_with_next_carries_the_heading_when_keep_lines_moves_the_next_paragraph() /// many continuous breaks. #[test] fn an_empty_continuous_section_mark_takes_no_room() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // "Pa", the section's last paragraph (`mark`), "Pb", "Pc" in the next section of `cols` // columns, then "Pd" after a second break. @@ -6053,6 +6255,7 @@ fn an_empty_continuous_section_mark_takes_no_room() { /// form's field boxes sat a space-before too low, over their labels. #[test] fn paragraph_relative_floats_start_above_the_space_before() { + crate::test_fonts(); let shape_top = |before: f32, prev_after: f32, y: f32| { let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::InFrontOfText, @@ -6107,6 +6310,7 @@ fn paragraph_relative_floats_start_above_the_space_before() { /// column it started in, so a paragraph took a dozen lines a word or two wide. #[test] fn text_after_a_column_break_takes_the_new_column_s_width() { + crate::test_fonts(); let mut d = Document::new(); d.last_section.columns = wordcraft_doc::section::Columns { count: 2, space: 36.0, separator: false, widths: vec![(67.0, 52.0), (348.0, 0.0)] }; let brk = wordcraft_doc::para::COLUMN_BREAK; @@ -6127,6 +6331,7 @@ fn text_after_a_column_break_takes_the_new_column_s_width() { /// modes up to 14 (less the previous paragraph's space after) and drops it in mode 15. #[test] fn space_before_at_the_top_of_a_page_like_word() { + crate::test_fonts(); let para = |text: &str, before: f32, pbb: bool| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); p.props = ParaProps { space_before: Some(before), space_after: Some(0.0), page_break_before: Some(pbb), ..Default::default() }; @@ -6209,6 +6414,7 @@ fn space_before_at_the_top_of_a_page_like_word() { /// all unkerned). #[test] fn kerning_follows_kerning_for_fonts_like_word() { + crate::test_fonts(); use wordcraft_doc::props::CharProps; let width = |kern: Option| { let mut d = Document::new(); @@ -6234,6 +6440,7 @@ fn kerning_follows_kerning_for_fonts_like_word() { /// cell; next to another paragraph's space the larger of the two counts. #[test] fn automatic_paragraph_spacing_like_word() { + crate::test_fonts(); let auto = |before: bool, after: bool| ParaProps { space_before: Some(5.0), space_after: Some(5.0), @@ -6281,6 +6488,7 @@ fn automatic_paragraph_spacing_like_word() { /// In a table cell automatic spacing drops at the cell's top and bottom (Word for Windows). #[test] fn automatic_paragraph_spacing_drops_at_cell_edges() { + crate::test_fonts(); let cell_tops = |auto: bool| { let mut d = Document::from_text("Above"); let mut t = Table::new(1, 1, 300.0); @@ -6319,6 +6527,7 @@ fn automatic_paragraph_spacing_drops_at_cell_edges() { /// (each item automatic before and after) were 14 pt apart instead of touching. #[test] fn automatic_spacing_drops_between_list_items() { + crate::test_fonts(); use wordcraft_doc::props::NumRef; let auto = ParaProps { space_before: Some(5.0), @@ -6395,6 +6604,7 @@ fn automatic_spacing_drops_between_list_items() { /// `hyphenation_points` keeps it so whatever the patterns become.) #[test] fn automatic_hyphenation_leaves_vietnamese_whole() { + crate::test_fonts(); let text = "Những người khuyến khích chuyện trường thường nghiêng nghiêng nhưng thành phố Nguyễn Trường Thượng xuyên suốt chương trình. ".repeat(8); let mut d = Document::from_text(&text); @@ -6411,6 +6621,7 @@ fn automatic_hyphenation_leaves_vietnamese_whole() { /// them), and so is the text drawn on them; a paragraph's shading is an ordinary fill. #[test] fn highlight_fill_is_marked_as_a_text_background() { + crate::test_fonts(); use wordcraft_doc::props::Highlight; let cp = wordcraft_doc::CharProps { highlight: Some(Highlight::Yellow), ..Default::default() }; let mut p = wordcraft_doc::Paragraph::with_text("Marked", cp); @@ -6437,6 +6648,7 @@ fn highlight_fill_is_marked_as_a_text_background() { /// its glyphs; other lines (here a paragraph border) don't. #[test] fn underline_on_a_highlight_carries_the_text_background() { + crate::test_fonts(); use wordcraft_doc::props::{Highlight, Underline}; let cp = wordcraft_doc::CharProps { highlight: Some(Highlight::Teal), underline: Some(Underline::Single), strike: Some(true), ..Default::default() }; @@ -6511,6 +6723,7 @@ fn assert_contiguous(ranges: &[std::ops::Range], len: usize) { /// #1352: a PAGE field after a page break inside its paragraph shows the page it is on. #[test] fn page_field_after_intra_paragraph_break_shows_its_page() { + crate::test_fonts(); let brk = wordcraft_doc::para::PAGE_BREAK; let d = page_field_doc(0, &["A", &format!("{brk}B")]); let l = lay(&d); @@ -6522,6 +6735,7 @@ fn page_field_after_intra_paragraph_break_shows_its_page() { /// #1352: a paragraph running over onto the next page shows that page's number in its tail. #[test] fn page_field_in_a_split_paragraph_tail_shows_its_page() { + crate::test_fonts(); let long = "words that fill the page ".repeat(400); let d = page_field_doc(0, &["Head", &long]); let l = lay(&d); @@ -6537,6 +6751,7 @@ fn page_field_in_a_split_paragraph_tail_shows_its_page() { /// again for the wider results, and the paragraph's text is still placed exactly once. #[test] fn page_fields_from_page_nine_to_ten_rebreak_the_rest() { + crate::test_fonts(); let pieces: Vec = (0..900).map(|i| format!(" word{i}")).collect(); let pieces: Vec<&str> = pieces.iter().map(String::as_str).collect(); let d = page_field_doc(8, &pieces); @@ -6569,6 +6784,7 @@ fn page_fields_from_page_nine_to_ten_rebreak_the_rest() { /// shows the page the footnote is on (the paragraph's own page map doesn't leak into it). #[test] fn footnote_page_field_from_a_split_paragraph_shows_its_page() { + crate::test_fonts(); let long = "words that fill the page ".repeat(200); let mut d = page_field_doc(0, &["Head", &long, "Tail"]); let end = d.end_of(StoryRef::Body); @@ -6644,6 +6860,7 @@ fn equation_numbers(l: &DocLayout) -> Vec { /// for a later page's PAGE fields) doesn't count its numbered equations twice: they read (1), (2), (3). #[test] fn relaid_paragraphs_keep_their_equation_numbers() { + crate::test_fonts(); let eq = |s: &str| InlineObject::Equation { linear: s.into(), display: true, @@ -6714,6 +6931,7 @@ fn para_pieces(l: &DocLayout, t: usize) -> (Vec<(usize, usize, usize, f32)>, usi #[test] fn space_after_stays_clear_of_the_footnote_separator() { + crate::test_fonts(); // #1116, measured in Word: on a page with footnotes a paragraph ending there keeps its space // after above the separator, or its last lines (two, with widow control) go to the next page // with the note. @@ -6742,6 +6960,7 @@ fn space_after_stays_clear_of_the_footnote_separator() { #[test] fn space_after_may_overflow_the_bottom_margin_without_notes() { + crate::test_fonts(); // Without footnotes nothing changes: the last line only has to fit, its space after may run // past the bottom margin (Word). let mut tight = 0; @@ -6784,6 +7003,7 @@ fn para_props(d: &mut Document, i: usize) -> &mut ParaProps { #[test] fn space_after_stays_clear_when_only_the_next_paragraph_has_contextual_spacing() { + crate::test_fonts(); // Contextual spacing on the next paragraph alone drops its space before, not this one's space // after (as the spacing between them): the space after still stays clear of the separator. space_after_clear_with(|d, t| para_props(d, t + 1).contextual_spacing = Some(true), 6.0); @@ -6791,6 +7011,7 @@ fn space_after_stays_clear_when_only_the_next_paragraph_has_contextual_spacing() #[test] fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() { + crate::test_fonts(); // Automatic spacing drops only between two list items with labels: before a paragraph whose // list doesn't exist (no label) a list item's automatic space after stays clear. use wordcraft_doc::props::NumRef; @@ -6808,6 +7029,7 @@ fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() { #[test] fn cell_widths_override_grid() { + crate::test_fonts(); for fixed in [false, true] { let mut t = Table::new(2, 3, 450.0); t.props.fixed = fixed; @@ -6831,6 +7053,7 @@ fn cell_widths_override_grid() { #[test] fn ambiguous_widths_keep_grid() { + crate::test_fonts(); let mut t = Table::new(2, 3, 450.0); t.grid = vec![40.0, 175.0, 235.0]; for row in &mut t.rows { @@ -6925,6 +7148,7 @@ fn assert_clear_of(lines: &[(f32, f32, f32, f32)], r: Rect, what: &str) { /// #1374: a paragraph that runs on from column 1 into column 2 wraps around a shape in column 2. #[test] fn paragraph_continued_into_next_column_wraps_around_a_shape_there() { + crate::test_fonts(); let r = Rect::new(340.0, 300.0, 100.0, 100.0); let d = column_wrap_doc(2, Some(page_shape(r.x, r.y, r.w, r.h)), 450); let l = lay(&d); @@ -6939,6 +7163,7 @@ fn paragraph_continued_into_next_column_wraps_around_a_shape_there() { /// from it) doesn't make the paragraph going into that column be laid out again. #[test] fn paragraph_continued_into_next_column_ignores_shapes_well_clear_of_it() { + crate::test_fonts(); let d = column_wrap_doc(2, Some(page_shape(0.0, 300.0, 20.0, 100.0)), 450); WRAP_RELAYS.with(|c| c.set(0)); let l = lay(&d); @@ -6951,6 +7176,7 @@ fn paragraph_continued_into_next_column_ignores_shapes_well_clear_of_it() { /// (reaching below its body) doesn't push them aside there. #[test] fn paragraph_continued_onto_next_page_is_broken_for_that_page() { + crate::test_fonts(); // On page 1 from 640 down past the page bottom, at the left of the text. let d = column_wrap_doc(1, Some(page_shape(72.0, 640.0, 150.0, 150.0)), 900); let l = lay(&d); @@ -6971,6 +7197,7 @@ fn paragraph_continued_onto_next_page_is_broken_for_that_page() { /// the first: the paragraph isn't laid out again (no wrap relay), and both pieces share one layout. #[test] fn paragraph_continued_into_next_column_without_shapes_is_not_laid_out_again() { + crate::test_fonts(); let d = column_wrap_doc(2, None, 450); WRAP_RELAYS.with(|c| c.set(0)); let l = lay(&d); @@ -6991,6 +7218,7 @@ fn paragraph_continued_into_next_column_without_shapes_is_not_laid_out_again() { /// when its lines in column 2 are broken again around a shape there. #[test] fn paragraph_own_float_is_not_replaced_when_its_rest_wraps_in_the_next_column() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; let r = Rect::new(340.0, 300.0, 100.0, 100.0); let mut d = column_wrap_doc(2, Some(page_shape(r.x, r.y, r.w, r.h)), 450); @@ -7057,6 +7285,7 @@ fn text_clusters(pl: &ParaLayout) -> Vec<(usize, usize, Vec, f32, f32)> { /// caret, selection and hit testing (#1581). #[test] fn caps_draw_sharp_s_as_two_capitals_in_one_cluster() { + crate::test_fonts(); let width = |pl: &ParaLayout| text_clusters(pl).iter().map(|c| c.4).sum::(); let plain_strase = { let d = Document::from_text("STRASE"); @@ -7104,6 +7333,7 @@ fn caps_draw_sharp_s_as_two_capitals_in_one_cluster() { /// A caps run in a symbol font draws each code as its symbol: ß never becomes two glyphs (#1581). #[test] fn caps_in_a_symbol_font_never_expand_characters() { + crate::test_fonts(); let pl = strasse_with(&|c| { c.caps = Some(true); c.font = Some("Wingdings".into()); @@ -7121,6 +7351,7 @@ fn caps_in_a_symbol_font_never_expand_characters() { /// is the page's last block. A page with one block keeps it at the top. #[test] fn section_vertical_alignment_top_center_justify_bottom() { + crate::test_fonts(); use wordcraft_doc::section::PageVAlign; let para = |text: &str| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); @@ -7170,6 +7401,7 @@ fn section_vertical_alignment_top_center_justify_bottom() { /// section's (Justified then Top: spread; Top then Justified: stays at the top). #[test] fn a_sections_last_page_keeps_its_own_vertical_alignment() { + crate::test_fonts(); use wordcraft_doc::section::{PageVAlign, SectionProps}; let para = |text: &str| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); @@ -7197,6 +7429,7 @@ fn a_sections_last_page_keeps_its_own_vertical_alignment() { /// after inside the text area, so "Three" with 72 pt after sits 72 pt above the bottom margin. #[test] fn justified_page_keeps_the_last_paragraphs_space_after() { + crate::test_fonts(); let mut d = Document::new(); d.body = ["One", "Two", "Three"] .iter() @@ -7219,6 +7452,7 @@ fn justified_page_keeps_the_last_paragraphs_space_after() { /// #1320: a justified page with several newspaper columns stays at the top (not spread). #[test] fn justified_multi_column_pages_stay_at_the_top() { + crate::test_fonts(); let laid = |va| { let mut d = two_column_section(wordcraft_doc::section::SectionStart::NextPage, Vec::new()); d.para_mut(StoryRef::Body, &Path::top(9)).unwrap().section.as_mut().unwrap().valign = va; @@ -7238,6 +7472,7 @@ fn justified_multi_column_pages_stay_at_the_top() { #[test] fn row_borders_stay_on_row() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 200.0); t.props.style = None; @@ -7253,6 +7488,7 @@ fn row_borders_stay_on_row() { #[test] fn row_nil_overrides_style() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(1, 1, 200.0); t.rows[0].props.exceptions = Some(Box::new(wordcraft_doc::props::TableProps { diff --git a/crates/layout/src/tests_bidi.rs b/crates/layout/src/tests_bidi.rs index 5d20141f6f..0d0a2602d5 100644 --- a/crates/layout/src/tests_bidi.rs +++ b/crates/layout/src/tests_bidi.rs @@ -58,6 +58,7 @@ fn at(s: &str, n: usize) -> usize { #[test] fn visual_order_follows_uax9_rule_l2() { + crate::test_fonts(); // The example from UAX #9 / unicode-bidi: levels 0 0 0 1 1 1 2 2. assert_eq!(visual_order(&[0, 0, 0, 1, 1, 1, 2, 2]), vec![0, 1, 2, 6, 7, 5, 4, 3]); assert_eq!(visual_order(&[1, 1, 1]), vec![2, 1, 0]); @@ -69,6 +70,7 @@ fn visual_order_follows_uax9_rule_l2() { #[test] fn levels_for_persian_with_latin_and_numbers() { + crate::test_fonts(); assert!(bidi_levels("plain English", false).is_empty(), "pure LTR paragraphs skip the algorithm"); let s = "سلام Word ۱۴۰۳"; let lv = bidi_levels(s, true); @@ -84,6 +86,7 @@ fn levels_for_persian_with_latin_and_numbers() { #[test] fn rtl_paragraph_is_right_aligned_and_reads_right_to_left() { + crate::test_fonts(); let s = "سلام دنیا"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -100,6 +103,7 @@ fn rtl_paragraph_is_right_aligned_and_reads_right_to_left() { #[test] fn ltr_paragraph_with_embedded_persian_word() { + crate::test_fonts(); let s = "Hello سلام world"; let d = doc_of(&[(s, false)]); let l = lay(&d); @@ -110,6 +114,7 @@ fn ltr_paragraph_with_embedded_persian_word() { #[test] fn numbers_and_latin_inside_persian_keep_their_order() { + crate::test_fonts(); // "Price 250 toman" in Persian, with a Latin product name and Persian punctuation. let s = "قیمت 250 تومان برای Word، امسال ۱۴۰۳."; let d = doc_of(&[(s, true)]); @@ -131,6 +136,7 @@ fn numbers_and_latin_inside_persian_keep_their_order() { #[test] fn caret_and_hit_agree_in_bidi_text() { + crate::test_fonts(); for (s, rtl) in [("سلام world ۱۲۳", true), ("Hello سلام دنیا end", false), ("کتاب «WordCraft» (نسخه ۲)", true)] { let d = doc_of(&[(s, rtl)]); let l = lay(&d); @@ -151,6 +157,7 @@ fn caret_and_hit_agree_in_bidi_text() { #[test] fn visual_steps_cover_the_line_in_order() { + crate::test_fonts(); let s = "سلام world"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -181,6 +188,7 @@ fn visual_steps_cover_the_line_in_order() { #[test] fn selection_of_mixed_text_can_be_two_pieces() { + crate::test_fonts(); // Logical range "lo wo" of "Hello سلام world"? Select across the Persian word's start: // "lo سل" is two visual pieces (the end of Hello, and the right part of سلام). let s = "Hello سلام world"; @@ -195,6 +203,7 @@ fn selection_of_mixed_text_can_be_two_pieces() { #[test] fn rtl_indents_and_alignment_are_measured_from_the_right() { + crate::test_fonts(); let s = "متن فارسی"; let mut d = doc_of(&[(s, true), (s, true), (s, true)]); let p0 = Pos::body(0, 0); @@ -213,6 +222,7 @@ fn rtl_indents_and_alignment_are_measured_from_the_right() { #[test] fn rtl_list_label_sits_on_the_right() { + crate::test_fonts(); let mut d = doc_of(&[("مورد اول", true), ("مورد دوم", true)]); let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p: &mut ParaProps| { @@ -234,6 +244,7 @@ fn rtl_list_label_sits_on_the_right() { #[test] fn rtl_empty_and_trailing_lines_mirror() { + crate::test_fonts(); let d = doc_of(&[("", true)]); let l = lay(&d); assert!((caret_x(&l, 0, 0) - 540.0).abs() < 0.5, "empty right-to-left paragraph: caret at the right margin"); @@ -247,6 +258,7 @@ fn rtl_empty_and_trailing_lines_mirror() { #[test] fn bidi_display_draws_every_glyph_inside_the_line() { + crate::test_fonts(); let s = "این یک متن آزمایشی با English و عدد ۱۲۳ است. ".repeat(12); let d = doc_of(&[(&s, true)]); let l = lay(&d); @@ -273,6 +285,7 @@ fn bidi_display_draws_every_glyph_inside_the_line() { #[test] fn hostile_bidi_text_never_panics() { + crate::test_fonts(); // Explicit embeddings, isolates, unmatched pops, marks with no base, lone surrogates' stand-ins. let junk = [ "\u{202B}\u{202B}\u{202C}\u{202C}\u{202C}abc\u{2067}سلام\u{2069}\u{2069}", @@ -318,6 +331,7 @@ fn cluster_glyphs(glyphs: &[wordcraft_fonts::ShapedGlyph]) -> Vec> { #[test] fn persian_letters_join_and_brackets_mirror() { + crate::test_fonts(); let Some(face) = arabic_face() else { eprintln!("skipped: no font with Persian letters installed"); return; @@ -347,6 +361,7 @@ fn persian_letters_join_and_brackets_mirror() { #[test] fn persian_text_in_a_latin_font_stays_in_one_fallback_face() { + crate::test_fonts(); if arabic_face().is_none() { eprintln!("skipped: no font with Persian letters installed"); return; @@ -365,6 +380,7 @@ fn persian_text_in_a_latin_font_stays_in_one_fallback_face() { #[test] fn glyph_runs_carry_their_text_for_pdf_export() { + crate::test_fonts(); let s = "سلام WordCraft می‌خواهم"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -394,6 +410,7 @@ fn glyph_runs_carry_their_text_for_pdf_export() { #[test] fn character_border_boxes_a_whole_rtl_word() { + crate::test_fonts(); let s = "کتاب خوب"; let mut d = doc_of(&[(s, true)]); let b = wordcraft_doc::props::Border { space: 0.0, ..wordcraft_doc::props::Border::single(0.5) }; @@ -418,6 +435,7 @@ fn character_border_boxes_a_whole_rtl_word() { #[test] fn inline_picture_in_rtl_text_sits_in_its_place() { + crate::test_fonts(); // "متن [picture] بیشتر": in a right-to-left paragraph the picture is drawn where its cluster // is on the line (left of the first word), not at its logical x. let s = "متن بیشتر"; @@ -455,6 +473,7 @@ fn inline_picture_in_rtl_text_sits_in_its_place() { /// margin and border are drawn on its right. Clicks find cells by where they are drawn. #[test] fn rtl_table_lays_its_columns_out_right_to_left() { + crate::test_fonts(); use wordcraft_doc::props::{Border, Borders}; use wordcraft_doc::{Path, Table}; let mut d = Document::from_text("before\nafter"); @@ -502,6 +521,7 @@ fn rtl_table_lays_its_columns_out_right_to_left() { /// first column at the right, and the line between them stays midway. #[test] fn rtl_section_puts_its_first_column_on_the_right() { + crate::test_fonts(); use wordcraft_doc::section::Columns; let text = "Columns flow from the right edge in a right-to-left section. ".repeat(150); let mut d = Document::from_text(&text); @@ -522,6 +542,7 @@ fn rtl_section_puts_its_first_column_on_the_right() { #[test] fn list_label_moves_with_its_text_when_the_paragraph_is_aligned() { + crate::test_fonts(); // Issue #1159: centring or end-aligning a list item moved the text and left the bullet at the // margin. In Word the label stays attached to the start of the text, in both directions. for (rtl, text) in [(false, "alpha beta"), (true, "مورد اول")] { diff --git a/crates/layout/src/tests_complex.rs b/crates/layout/src/tests_complex.rs index d8e6b693f0..e61c11246e 100644 --- a/crates/layout/src/tests_complex.rs +++ b/crates/layout/src/tests_complex.rs @@ -29,6 +29,7 @@ const MYANMAR: &str = "\u{1019}\u{103C}\u{1014}\u{103A}\u{1019}\u{102C}\u{1005}\ #[test] fn myanmar_syllables_are_single_clusters() { + crate::test_fonts(); let l = lay(&Document::from_text(MYANMAR)); let pl = para_layout(&l, 0); let starts: Vec = pl.clusters.iter().map(|c| c.start).collect(); @@ -46,6 +47,7 @@ fn myanmar_syllables_are_single_clusters() { #[test] fn myanmar_without_spaces_wraps_between_syllables() { + crate::test_fonts(); let text = MYANMAR.repeat(80); let l = lay(&Document::from_text(&text)); let pl = para_layout(&l, 0); @@ -71,6 +73,7 @@ fn myanmar_face() -> Option { #[test] fn myanmar_prevowel_is_drawn_before_its_consonant() { + crate::test_fonts(); let Some(face) = myanmar_face() else { eprintln!("skipped: no Myanmar font with OpenType tables installed"); return; @@ -92,6 +95,7 @@ fn myanmar_prevowel_is_drawn_before_its_consonant() { #[test] fn myanmar_in_a_latin_font_falls_back_to_a_regular_myanmar_face() { + crate::test_fonts(); if myanmar_face().is_none() { eprintln!("skipped: no Myanmar font with OpenType tables installed"); return; diff --git a/crates/layout/src/tests_grid.rs b/crates/layout/src/tests_grid.rs index fc2c9005a7..a71a78bb90 100644 --- a/crates/layout/src/tests_grid.rs +++ b/crates/layout/src/tests_grid.rs @@ -41,6 +41,7 @@ fn lines(l: &DocLayout) -> (usize, Vec) { #[test] fn line_grid_snaps_line_heights_and_fixes_lines_per_page() { + crate::test_fonts(); let text = vec!["Grid line"; 80].join("\n"); let d = with_grid(&text, DocGridType::Lines, 0); let (first, hs) = lines(&lay(&d)); @@ -64,6 +65,7 @@ fn line_grid_snaps_line_heights_and_fixes_lines_per_page() { #[test] fn character_grid_places_east_asian_text_on_the_pitch() { + crate::test_fonts(); let text = "日本語の文書abc"; // The default font size plus 2.5 pt: a pitch unlike any font's advance. let lines = wordcraft_doc::SectionProps { doc_grid: Some(DocGrid { kind: DocGridType::Lines, ..Default::default() }), ..Default::default() }; diff --git a/crates/layout/src/tests_numeral.rs b/crates/layout/src/tests_numeral.rs index 9de3e8ee8f..542d14516c 100644 --- a/crates/layout/src/tests_numeral.rs +++ b/crates/layout/src/tests_numeral.rs @@ -37,6 +37,7 @@ fn line_gids(l: &DocLayout, text: &str) -> Vec> { #[test] fn parse_accepts_the_four_modes_any_case() { + crate::test_fonts(); assert_eq!(NumeralMode::parse("arabic"), Some(NumeralMode::Arabic)); assert_eq!(NumeralMode::parse("hindi"), Some(NumeralMode::Hindi)); assert_eq!(NumeralMode::parse("context"), Some(NumeralMode::Context)); @@ -50,6 +51,7 @@ fn parse_accepts_the_four_modes_any_case() { #[test] fn map_substitutes_digits_both_ways() { + crate::test_fonts(); for d in '0'..='9' { let hindi = char::from_u32(0x660 + (d as u32 - '0' as u32)).unwrap(); assert_eq!(NumeralMode::Hindi.map(d, false), hindi); @@ -73,6 +75,7 @@ fn map_substitutes_digits_both_ways() { #[test] fn resolve_uses_the_given_system_locale() { + crate::test_fonts(); for m in [NumeralMode::Arabic, NumeralMode::Hindi, NumeralMode::Context] { assert_eq!(m.resolve(Some("ar-EG")), m, "only System is resolved"); } @@ -87,6 +90,7 @@ fn resolve_uses_the_given_system_locale() { #[test] fn context_mode_on_a_very_long_digit_run_stays_linear() { + crate::test_fonts(); // Every digit used to scan back over all the digits and spaces before it (O(n²)). let text = format!("سنة {}", "1 ".repeat(100_000)); let start = std::time::Instant::now(); @@ -100,6 +104,7 @@ fn context_mode_on_a_very_long_digit_run_stays_linear() { #[test] fn hindi_mode_reshapes_digits_but_keeps_offsets() { + crate::test_fonts(); let text = "Year 2024!"; let latin = lay_opts(text, false, NumeralMode::Arabic); let hindi = lay_opts(text, false, NumeralMode::Hindi); @@ -121,6 +126,7 @@ fn hindi_mode_reshapes_digits_but_keeps_offsets() { #[test] fn arabic_mode_converts_hindi_digits_back() { + crate::test_fonts(); let text = "عام ٢٠٢٤"; let hindi = lay_opts(text, true, NumeralMode::Hindi); let arabic = lay_opts(text, true, NumeralMode::Arabic); @@ -139,6 +145,7 @@ fn arabic_mode_converts_hindi_digits_back() { #[test] fn context_mode_follows_run_direction() { + crate::test_fonts(); // Right-to-left paragraph: digits behave like Hindi mode. let rtl = "سنة 2024"; let ctx = line_gids(&lay_opts(rtl, true, NumeralMode::Context), rtl); @@ -158,6 +165,7 @@ fn context_mode_follows_run_direction() { #[test] fn remap_range_maps_display_bytes_to_original() { + crate::test_fonts(); use crate::para::remap_range; // "a5ب": '5' (1 byte) displays as '٥' (2 bytes), so display offsets shift after it. let orig = "a5ب"; diff --git a/crates/layout/src/tests_pages.rs b/crates/layout/src/tests_pages.rs index c19252cb5f..6e08c1328b 100644 --- a/crates/layout/src/tests_pages.rs +++ b/crates/layout/src/tests_pages.rs @@ -35,6 +35,7 @@ fn body_left(l: &DocLayout) -> Vec { #[test] fn mirrored_margins_put_the_inside_margin_toward_the_binding() { + crate::test_fonts(); // #452: inside 108 (plus a 10pt gutter), outside 36. Odd pages: inside on the left; even // pages: on the right. let mut d = Document::from_text(&"Body line\n".repeat(90)); @@ -55,6 +56,7 @@ fn mirrored_margins_put_the_inside_margin_toward_the_binding() { #[test] fn inside_and_outside_floats_swap_sides_on_even_pages() { + crate::test_fonts(); // #1181: shapes Word places in the outside margin of a book (mirrored margins) all came out // in the right margin. Inside 108 plus a 10pt gutter, outside 36; the paragraph on page 2 // carries the same six shapes as page 1's, told apart by their widths. @@ -124,6 +126,7 @@ fn inside_and_outside_floats_swap_sides_on_even_pages() { #[test] fn outside_floating_tables_swap_sides_on_even_pages() { + crate::test_fonts(); // #1181, for a floating table (`w:tblpXSpec="outside"`): on page 2 of a book the outside is // the left. One positioned from the page edge stays there on both pages. use wordcraft_doc::para::{Anchor, FloatAlign}; @@ -159,6 +162,7 @@ fn outside_floating_tables_swap_sides_on_even_pages() { #[test] fn a_tall_footer_pushes_the_body_up() { + crate::test_fonts(); // #453: an eight-line footer reaches far above the 72pt bottom margin. let mut d = Document::from_text(&"Body line\n".repeat(60)); let id = d.add_part(PartKind::Footer, paras("Footer line", 8)); @@ -184,6 +188,7 @@ fn a_tall_footer_pushes_the_body_up() { #[test] fn a_linked_tall_header_pushes_the_next_sections_body_down() { + crate::test_fonts(); // #454: section 2 has no header of its own and shows section 1's (link to previous). let mut d = Document::from_text("First section\nSecond section"); let id = d.add_part(PartKind::Header, paras("Header line", 8)); @@ -203,6 +208,7 @@ fn a_linked_tall_header_pushes_the_next_sections_body_down() { #[test] fn long_endnotes_continue_on_new_pages() { + crate::test_fonts(); // #456: an 80-paragraph endnote after a one-line body. let mut d = Document::from_text("Body"); let id = d.add_part(PartKind::Endnote, paras("Endnote paragraph", 80)); @@ -230,6 +236,7 @@ fn long_endnotes_continue_on_new_pages() { #[test] fn caret_in_a_continued_footnote_is_found_on_its_page() { + crate::test_fonts(); // #457: one long footnote paragraph split over pages 1 and 2. let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let id = d.add_part( @@ -264,6 +271,7 @@ fn caret_in_a_continued_footnote_is_found_on_its_page() { /// own pages). The footer shows each letter's last page, not the file's stale result. #[test] fn page_references_in_footers_show_their_bookmarks_page() { + crate::test_fonts(); let field = |instr: &str| InlineObject::Field { instr: instr.into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; let footer = |d: &mut Document, mark: &str| { @@ -379,6 +387,7 @@ fn mark(d: &mut Document, path: &Path, off: usize, name: &str) { /// section numbered in roman numerals are found on the page and in the format they land in. #[test] fn page_references_find_bookmarks_in_split_paragraphs_tables_and_roman_sections() { + crate::test_fonts(); // One long paragraph over two pages, marked at its start and its end. let mut d = Document::from_text(&"Long paragraph text running on. ".repeat(160)); let n = d.para(StoryRef::Body, &Path::top(0)).unwrap().len(); @@ -423,6 +432,7 @@ fn page_references_find_bookmarks_in_split_paragraphs_tables_and_roman_sections( /// A text box in a footer resolves its page references; one in the body keeps its result. #[test] fn page_references_in_footer_text_boxes_are_resolved() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; let text_box = |d: &mut Document| { let mut p = Paragraph::new(); @@ -475,15 +485,17 @@ fn page_references_in_footer_text_boxes_are_resolved() { #[test] fn widow_control_moves_a_three_line_paragraph_whole() { + crate::test_fonts(); // A three-line paragraph with room for two at the bottom of a page can't leave a widow (two // and one) nor an orphan (one and two): Word moves it whole. A four-line one splits two and // two. Sweep the paragraph down the page a point at a time and check every split. + // Shift the filler immediately before the probe, so the sweep tests partial-line room. let mut split4 = false; for (want, words) in [(3usize, 45usize), (4, 55)] { 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.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.space_before = Some(shift as f32)).unwrap(); + d.format_paragraphs(&Pos::body(fill - 1, 0), &Pos::body(fill - 1, 0), &|p| p.space_before = Some(shift as f32)).unwrap(); let l = lay(&d); let pieces: Vec<(usize, usize)> = l .pages diff --git a/crates/layout/src/tests_styleref.rs b/crates/layout/src/tests_styleref.rs index cba5d9f8c5..5e549eda0b 100644 --- a/crates/layout/src/tests_styleref.rs +++ b/crates/layout/src/tests_styleref.rs @@ -52,6 +52,7 @@ const CHAPTERS: &[(&str, &str, bool)] = &[ #[test] fn each_page_s_header_shows_its_chapter() { + crate::test_fonts(); let d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1" \* MERGEFORMAT"#, "STYLEREF 1", "STYLEREF heading1 \\l"]); let h = headers(&d); assert_eq!(h.len(), 4); @@ -62,6 +63,7 @@ fn each_page_s_header_shows_its_chapter() { #[test] fn search_from_the_bottom_with_l() { + crate::test_fonts(); let paras = [("Heading2", "First", false), ("Heading2", "Second", false), ("Normal", "Body", false), ("Heading2", "Third", true)]; let d = doc(¶s, &[r#"STYLEREF "Heading 2""#, r#"STYLEREF "Heading 2" \l"#]); assert_eq!(headers(&d), [vec!["First", "Second"], vec!["Third", "Third"]]); @@ -69,6 +71,7 @@ fn search_from_the_bottom_with_l() { #[test] fn a_missing_style_or_no_paragraph_of_it_is_an_error() { + crate::test_fonts(); let d = doc(CHAPTERS, &[r#"STYLEREF "No Such Style""#, r#"STYLEREF "Heading 3""#, "STYLEREF 12"]); let err = styleref::NO_TEXT; assert_eq!(headers(&d)[0], [err, err, err]); @@ -77,6 +80,7 @@ fn a_missing_style_or_no_paragraph_of_it_is_an_error() { #[test] fn before_the_first_heading_the_next_one_shows() { + crate::test_fonts(); let paras = [("Normal", "Preface", false), ("Heading1", "Chapter", true)]; let d = doc(¶s, &[r#"STYLEREF "Heading 1""#, r#"STYLEREF "Heading 1" \p"#]); assert_eq!(headers(&d), [vec!["Chapter", "below"], vec!["Chapter", "below"]]); @@ -84,6 +88,7 @@ fn before_the_first_heading_the_next_one_shows() { #[test] fn character_styles_and_paragraph_numbers() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF Strong"#, r#"STYLEREF "Heading 1" \n"#, r#"STYLEREF "Heading 2" \w"#, r#"STYLEREF "Heading 1" \s"#]); // 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(); @@ -100,6 +105,7 @@ fn character_styles_and_paragraph_numbers() { /// Editing a heading updates the headers that show it, through the paragraph cache. #[test] fn headers_follow_heading_edits() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#]); let mut c = LayoutCache::new(); let first = layout(&d, &mut c, &LayoutOptions::default()); @@ -115,6 +121,7 @@ fn headers_follow_heading_edits() { /// documents lay out on every keystroke); an edited heading or a changed style rebuilds it. #[test] fn style_index_is_reused_until_the_body_or_styles_change() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#]); let mut c = LayoutCache::new(); let index = |c: &LayoutCache| c.styleref.as_ref().map(|(_, _, ix)| ix.clone()).unwrap(); @@ -143,6 +150,7 @@ fn style_index_is_reused_until_the_body_or_styles_change() { /// `STYLEREF 1` finds the style named `heading 1` when its id is localized (#1286). #[test] fn heading_level_matches_a_localized_style_id() { + crate::test_fonts(); 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(¶s, &["STYLEREF 1"]); let st = d.styles.styles.iter_mut().find(|s| s.id == "Heading1").unwrap(); @@ -157,6 +165,7 @@ fn heading_level_matches_a_localized_style_id() { /// edited heading still updates the headers that show it. #[test] fn body_edits_keep_styleref_headers_cached() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#, r#"STYLEREF "Heading 1" \p"#]); let mut c = LayoutCache::new(); let opts = LayoutOptions::default(); diff --git a/crates/layout/src/tests_textbox.rs b/crates/layout/src/tests_textbox.rs index 5147133ab8..47dd8f8b05 100644 --- a/crates/layout/src/tests_textbox.rs +++ b/crates/layout/src/tests_textbox.rs @@ -51,6 +51,7 @@ fn frame_of(l: &DocLayout, id: u32) -> Rect { #[test] fn align_text_and_text_direction_place_the_text_in_the_box() { + crate::test_fonts(); let at = |body: TextBody| { let mut d = Document::from_text("x"); let id = text_box(&mut d, 0, "Hi", 144.0, 144.0, body); @@ -81,6 +82,7 @@ fn align_text_and_text_direction_place_the_text_in_the_box() { #[test] fn linked_boxes_continue_the_text() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo"); let text = "Words that run on well past the first little box. ".repeat(6); let a = text_box(&mut d, 0, &text, 144.0, 48.0, TextBody::default()); @@ -119,6 +121,7 @@ fn linked_boxes_continue_the_text() { #[test] fn hostile_link_chains_stay_bounded() { + crate::test_fonts(); // Boxes linking in a circle show their own text; a box linking to itself too. let mut d = Document::from_text("one\ntwo\nthree"); let a = text_box(&mut d, 0, "A", 72.0, 36.0, TextBody::default()); @@ -136,6 +139,7 @@ fn hostile_link_chains_stay_bounded() { #[test] fn own_margins_and_resize_to_fit_keep_the_text_in_view() { + crate::test_fonts(); // #743: a cover page's two-line box with no margins (`lIns="0"`…) is just tall enough for // its text; with Word's default margins its second line would be cut. let two = |h: f32, body: TextBody| { diff --git a/crates/layout/src/tests_vertical.rs b/crates/layout/src/tests_vertical.rs index aaec1c9ef5..782f92cbb7 100644 --- a/crates/layout/src/tests_vertical.rs +++ b/crates/layout/src/tests_vertical.rs @@ -30,6 +30,7 @@ fn body_lines(l: &DocLayout) -> Vec<(f32, f32, TextDirection)> { #[test] fn top_to_bottom_section_stacks_lines_from_the_right() { + crate::test_fonts(); let d = vertical_doc(TextDirection::Down); let l = lay(&d); let page = &l.pages[0]; @@ -70,6 +71,7 @@ fn top_to_bottom_section_stacks_lines_from_the_right() { #[test] fn bottom_to_top_section_stacks_lines_from_the_left() { + crate::test_fonts(); let l = lay(&vertical_doc(TextDirection::Up)); assert!(body_lines(&l).iter().all(|(.., t)| *t == TextDirection::Up)); let c0 = l.caret(&Pos::body(0, 0)).unwrap(); @@ -81,6 +83,7 @@ fn bottom_to_top_section_stacks_lines_from_the_left() { #[test] fn east_asian_characters_stand_upright_in_top_to_bottom_text() { + crate::test_fonts(); let l = lay(&vertical_doc(TextDirection::Down)); let d = vertical_doc(TextDirection::Down); let draws = display::page_display(&d, &l.pages[0], &Default::default()); @@ -106,6 +109,7 @@ fn east_asian_characters_stand_upright_in_top_to_bottom_text() { #[test] fn headers_stay_horizontal_and_a_vertical_section_starts_a_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; let mut d = Document::from_text("Horizontal\nVertical"); // Section 1 ends after paragraph 0 (continuous), section 2 is vertical. diff --git a/crates/layout/src/wordart.rs b/crates/layout/src/wordart.rs index 0938b9b7fa..0ebcaa6d86 100644 --- a/crates/layout/src/wordart.rs +++ b/crates/layout/src/wordart.rs @@ -294,6 +294,7 @@ mod tests { #[test] fn arch_baselines_lie_on_the_arc() { + crate::test_fonts(); let frame = Rect::new(10.0, 20.0, 200.0, 100.0); let w = Warp::new(&TextWarp::new("textArchUp").unwrap(), frame, 0.25).unwrap(); let (cx, cy, rx, ry) = (110.0, 70.0, 100.0, 50.0); @@ -320,6 +321,7 @@ mod tests { #[test] fn flat_and_unknown_warps_draw_flat() { + crate::test_fonts(); let frame = Rect::new(0.0, 0.0, 100.0, 50.0); assert!(Warp::new(&TextWarp::new("textPlain").unwrap(), frame, 0.2).is_none()); assert!(Warp::new(&TextWarp::new("textCanUp").unwrap(), frame, 0.2).is_none()); diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..4b166bfaa4 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,28 @@ +# Layout test fonts + +Layout unit tests use only the 13 font files already pinned under `assets/fonts/`: Source Sans 3, +Source Serif 4, Inter and JetBrains Mono, with their bundled styles. Family listings also include +legacy style names such as Inter Medium. They do not load installed fonts, user font preferences, +or the optional `CRAFT_FONTS_DIR` input. No download or extra font checkout is required. + +Every layout unit test starts with `crate::test_fonts();`, before constructing its fixture or +resolving a font. This initializes the process-wide database once, including when a single test is +run by name. The `wordcraft-fonts/test-support` development feature exposes the initializer; +enabling the feature alone has no effect. Initialization fails if an application database already +exists, rather than replacing a database whose faces may be in the shaping or math caches. +Other test binaries and application builds keep their normal font discovery behavior. + +Uninstalled requested families use the normal substitution lists, restricted to this fixed set. +In particular, the default Aptos request resolves to Source Sans 3. These fonts are not Calibri +metric substitutes: Word measurements remain reference behavior, while fixture dimensions must +account for the pinned font's metrics. Do not recalibrate a fixture from the same layout behavior +it is meant to test. When changing a pagination fixture, check that undoing its original layout +fix still makes the regression test fail. + +The fixed set does not cover every script or include an OpenType math font. Tests with an +existing unavailable-script guard still take that guard. Additional script or math font fixtures +belong in `storytold/craft-fonts`, with a reproducible test input before enabling them here. + +Run the suite with `cargo test -p wordcraft-layout`. The isolated font initialization regression +runs with `cargo test -p wordcraft-fonts --features test-support --test fixed_test_fonts`. +`cargo xtask ci` also runs it through the layout crate's development dependency. From 715f491d293ee8b25fa828e80b5879825e8c9d27 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 18 Oct 2026 01:22:15 -0400 Subject: [PATCH 2/6] docs: drop references to competing commercial software --- README.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 41b097cbd2..3f93d4c924 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@

WordCraft

- Writing and document design; an open-source, clean-room reimplementation of Microsoft Word, rebuilt in pure Rust. + Writing and document design; an open-source, modern word processor, built in pure Rust.

- A fast, open-source word processor with the Word workflow you already know: the ribbon, styles, + A fast, open-source word processor with a familiar workflow: the ribbon, styles, tables, track changes, references and mail merge. It reads and writes .docx, runs natively on macOS, Windows, Linux and BSD, and in the browser via WebAssembly.
By the ArtCraft team. @@ -86,10 +86,10 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes ## Why WordCraft -- **Familiar.** Word's ribbon tabs, groups, shortcuts and behaviour: Enter continues a list, +- **Familiar.** Ribbon tabs, groups, shortcuts and behaviour: Enter continues a list, Tab demotes it, Ctrl/⌘+B bolds the word under the caret, the Styles gallery previews styles live, F4 repeats, F7 checks spelling, F8 extends the selection. -- **Your files.** Opens and saves .docx (OOXML), and also .odt, .rtf, .html, .md, .tex (LaTeX), .txt; opens Word 97-2003 .doc; exports PDF +- **Your files.** Opens and saves .docx (OOXML), and also .odt, .rtf, .html, .md, .tex (LaTeX), .txt; opens legacy 97-2003 .doc; exports PDF with real, selectable text, links and bookmarks. - **Fast.** Paragraph layout is cached, so typing in a 188-page document re-lays it out in about 1.4 ms; pages render on demand. @@ -118,7 +118,7 @@ Every screenshot below is WordCraft itself, rendered offscreen by its own UI tes | **Review** | Spelling and grammar with suggestions (English and Polish, by each run's language), thesaurus, word count, comments in margin balloons or a pane, track changes, accept/reject, compare documents, restrict editing, accessibility checker, document inspector | | **Mailings** | Mail merge from CSV, merge fields, address block, greeting line, rules, preview, finish to a document; envelopes and labels | | **View** | Print layout, web layout, draft, read mode, focus, zoom, one/multiple pages, page width, Navigation pane, rulers, gridlines, dark mode; interface in English, 简体中文, 繁體中文, 日本語, Português (Brasil), Español, Українська, Српски / Srpski, Eesti, Türkçe, Русский, Italiano, Deutsch, Polski, Čeština, Magyar, Français, Български, Tiếng Việt, 한국어 or Svenska (follows the system language by default) | -| **Files** | .docx read/write (opens in Word), Word 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images | +| **Files** | .docx read/write, legacy 97-2003 .doc import, PDF export, .odt, .rtf, .html, .md, .tex, .txt import/export, page images | The honest picture, area by area, is in [ROADMAP.md](ROADMAP.md), the [parity assessment](docs/target-app-parity.md) and the generated @@ -189,9 +189,9 @@ covers every target starting with it (`wordcraft*=debug`). The logger is | L1 | `wordcraft-doc`, `wordcraft-fonts`, `wordcraft-proof` | document model and editing; fonts and shaping; spelling, grammar, hyphenation | | L2 | `wordcraft-layout`, `wordcraft-docx`, `wordcraft-formats` | line breaking, pagination, tables, notes, hit testing; OOXML; ODT/RTF/HTML/Markdown/TXT | | L3 | `wordcraft-render`, `wordcraft-pdf` | rasteriser (vello_cpu); PDF ([craft-pdf](docs/shared-pdf.md), Krilla) | -| L4 | `wordcraft-engine` | session, undo, 422 commands, Word feature catalog | +| L4 | `wordcraft-engine` | session, undo, 422 commands, document feature catalog | | L5 | `wordcraft-mcp` | MCP server | -| L6 | `wordcraft-ui-egui` | the Word-style front end (swappable) | +| L6 | `wordcraft-ui-egui` | the word processor front end (swappable) | | apps | `wordcraft`, `wordcraft-cli`, `wordcraft-web` | desktop, command line, browser | `cargo xtask ci` runs formatting, clippy, ~800 tests, the asset-attribution check, the layering @@ -199,12 +199,12 @@ check and the wasm build. Contributor and agent instructions: [AGENTS.md](AGENTS ## Roadmap -WordCraft is in **alpha**. It covers 88% of Word's ribbon features with commands today; counting -depth, file fidelity, stability and the rest, we estimate it is about 60% of the way to replacing -all of Word for real work, about 55% for a typical professional's weekly work and about 63% for -everyday essentials. Beta needs testing against real-world .docx files, keeping charts and SmartArt -through a round trip, pagination closer to Word's, a stability sweep and native printing. Details -and estimates: [ROADMAP.md](ROADMAP.md) and [where we fall short](docs/gaps.md). +WordCraft is in **alpha**. It covers 88% of standard ribbon features with commands today; counting +depth, file fidelity, stability and the rest, we estimate it is about 60% ready for real work, +about 55% for a typical professional's weekly work and about 63% for everyday essentials. Beta +needs testing against real-world .docx files, keeping charts and SmartArt through a round trip, +closer pagination fidelity, a stability sweep and native printing. Details and estimates: +[ROADMAP.md](ROADMAP.md) and [where we fall short](docs/gaps.md). ## Downloads @@ -247,7 +247,7 @@ WordCraft compiles DirectX 12 shaders with FXC, the compiler built into Windows, also avoids DirectX 12 altogether. **If the app is slow on an older Windows PC:** when DirectX 12 finds no graphics card with a -driver for it (for example Intel 4th-generation graphics on Windows 11), it only offers Microsoft's +driver for it (for example Intel 4th-generation graphics on Windows 11), it only offers the built-in software renderer, which draws every frame on the processor. In that case WordCraft uses OpenGL instead, without setting anything. The log file records the choice (lines starting with `graphics:`). `WGPU_BACKEND` still overrides it, e.g. `$env:WGPU_BACKEND = "gl"` or `"dx12"`. @@ -377,8 +377,6 @@ ArtCraft Team and are not covered by this license. They may be used only unmodif part of this repository and WordCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them. -Microsoft and Microsoft Word are trademarks of the Microsoft group of companies. Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. WordCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Microsoft Corporation or Adobe Inc.; these names are used only to describe the workflows it is compatible with. -

ArtCraft
Made by the ArtCraft team and community. From f4c109a0ab6c72977c7609cc36e49fa0aee2c13c Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 18 Oct 2026 10:44:06 -0400 Subject: [PATCH 3/6] docs: clarify that feature percentage is measured against tracked features in docs/gaps.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f93d4c924..7a1c2bd1cf 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ check and the wasm build. Contributor and agent instructions: [AGENTS.md](AGENTS ## Roadmap -WordCraft is in **alpha**. It covers 88% of standard ribbon features with commands today; counting +WordCraft is in **alpha**. It covers 88% of the ribbon features tracked in [`docs/gaps.md`](docs/gaps.md) with commands today; counting depth, file fidelity, stability and the rest, we estimate it is about 60% ready for real work, about 55% for a typical professional's weekly work and about 63% for everyday essentials. Beta needs testing against real-world .docx files, keeping charts and SmartArt through a round trip, From 0b3733749dbe97220b28ed6b482ffaaaab385b4e Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 18 Oct 2026 12:50:08 -0400 Subject: [PATCH 4/6] docs: update feature catalog parity link and qualify OpenGL/WARP fallback --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7a1c2bd1cf..02a9481fa4 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,8 @@ check and the wasm build. Contributor and agent instructions: [AGENTS.md](AGENTS ## Roadmap -WordCraft is in **alpha**. It covers 88% of the ribbon features tracked in [`docs/gaps.md`](docs/gaps.md) with commands today; counting +WordCraft is in **alpha**. It has commands for 97% of the ribbon and menu features in its feature catalog +([`docs/parity-checklist.md`](docs/parity-checklist.md)) today; counting depth, file fidelity, stability and the rest, we estimate it is about 60% ready for real work, about 55% for a typical professional's weekly work and about 63% for everyday essentials. Beta needs testing against real-world .docx files, keeping charts and SmartArt through a round trip, @@ -248,8 +249,9 @@ also avoids DirectX 12 altogether. **If the app is slow on an older Windows PC:** when DirectX 12 finds no graphics card with a driver for it (for example Intel 4th-generation graphics on Windows 11), it only offers the built-in -software renderer, which draws every frame on the processor. In that case WordCraft uses OpenGL -instead, without setting anything. The log file records the choice (lines starting with +software renderer, which draws every frame on the processor. WordCraft normally uses OpenGL instead, +without setting anything. If OpenGL has no usable adapter, it can use DirectX 12's WARP software +adapter. The log file records the choice (lines starting with `graphics:`). `WGPU_BACKEND` still overrides it, e.g. `$env:WGPU_BACKEND = "gl"` or `"dx12"`. ### macOS From 19094d4f3aa507b6cd57ce5e2295ebcbecab15b0 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Mon, 19 Oct 2026 02:20:09 +0200 Subject: [PATCH 5/6] Layout tests: start every test with test_fonts and refit three fixtures to the pinned fonts Adds crate::test_fonts() to the 95 layout tests that lacked it (several arrived with upstream merges). Three fixtures were sized for installed fonts and failed with the pinned set: exact_row_moves_whole (one line less of filler), an_at_least_row_at_the_top_of_a_page_splits_instead_of_moving_on (60 lines so the split row does not spill an empty page) and the inline text box test (FILLER 16). Co-Authored-By: Claude Sonnet 5.5 --- crates/layout/src/kinsoku.rs | 1 + crates/layout/src/math.rs | 1 + crates/layout/src/para.rs | 1 + crates/layout/src/table.rs | 3 ++ crates/layout/src/tests.rs | 73 +++++++++++++++++++++++++++- crates/layout/src/tests_autospace.rs | 6 +++ crates/layout/src/tests_complex.rs | 6 +++ crates/layout/src/tests_pages.rs | 2 + crates/layout/src/tests_styleref.rs | 2 + crates/layout/src/tests_textbox.rs | 3 +- crates/layout/src/wordart.rs | 3 ++ 11 files changed, 98 insertions(+), 3 deletions(-) diff --git a/crates/layout/src/kinsoku.rs b/crates/layout/src/kinsoku.rs index 7359c52f81..5a1099b96e 100644 --- a/crates/layout/src/kinsoku.rs +++ b/crates/layout/src/kinsoku.rs @@ -113,6 +113,7 @@ mod tests { #[test] fn auto_space_pairs() { + crate::test_fonts(); assert_eq!(auto_space('院', '9', true, true), Some(AsianSide::Left)); assert_eq!(auto_space('A', 'カ', true, true), Some(AsianSide::Right)); assert_eq!(auto_space('院', '9', true, false), None); diff --git a/crates/layout/src/math.rs b/crates/layout/src/math.rs index 6d3b5cbdb5..493a860d0c 100644 --- a/crates/layout/src/math.rs +++ b/crates/layout/src/math.rs @@ -2169,6 +2169,7 @@ mod tests { /// drawn from a fallback font, not as a missing-glyph box. #[test] fn characters_the_math_font_lacks_fall_back() { + crate::test_fonts(); let (face, _) = wordcraft_fonts::math::resolve_math("Cambria Math"); let Some(c) = ['⨌', '⫿', '⟅', 'ア', '中', '가'].into_iter().find(|c| !face.covers(*c) && FontDb::global().fallback_for(*c, face.id()).is_some()) diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 0bfe73bcb4..caf8441310 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -3173,6 +3173,7 @@ mod tests { #[test] fn symbols_ignore_roman_hint() { + crate::test_fonts(); let mut d = Document::new(); d.settings.roman_fonts = vec!["Symbol (PCL6)".into(), "Wingdings".into(), "No Such Roman Font".into()]; for family in ["Symbol (PCL6)", "Wingdings"] { diff --git a/crates/layout/src/table.rs b/crates/layout/src/table.rs index 2b3cd1c086..912e6d3e25 100644 --- a/crates/layout/src/table.rs +++ b/crates/layout/src/table.rs @@ -1287,6 +1287,7 @@ mod width_tests { #[test] fn autofit_shares_donor_loss() { + crate::test_fonts(); let table = Table::new(1, 4, 260.0); let mut grid = [30.0, 120.0, 20.0, 90.0]; let content = [(28.0, 28.0), (40.0, 120.0), (40.0, 40.0), (30.0, 90.0)]; @@ -1296,6 +1297,7 @@ mod width_tests { #[test] fn autofit_exhausts_donors() { + crate::test_fonts(); let table = Table::new(1, 2, 50.0); let mut grid = [30.0, 20.0]; autofit(&mut grid, &[(25.0, 25.0), (40.0, 40.0)], &table, 50.0); @@ -1304,6 +1306,7 @@ mod width_tests { #[test] fn autofit_keeps_fitting_widths() { + crate::test_fonts(); let table = Table::new(1, 3, 120.0); let mut grid = [30.0, 40.0, 50.0]; autofit(&mut grid, &[(10.0, 10.0), (20.0, 20.0), (30.0, 30.0)], &table, 120.0); diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 1bb2898534..326690eee8 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -757,6 +757,7 @@ fn page_break_char_starts_new_page() { /// beating `w:default`); a drop-down shows its selected entry whatever result was saved. #[test] fn legacy_form_fields_are_drawn() { + crate::test_fonts(); let field = |form: &str| InlineObject::Field { instr: "FORMCHECKBOX".into(), result: String::new(), @@ -996,6 +997,7 @@ fn ptab(align: wordcraft_doc::para::PTabAlign, relative_to: wordcraft_doc::para: /// the gap. #[test] fn right_positional_tab_ends_text_on_the_right_margin() { + crate::test_fonts(); use wordcraft_doc::para::{PTabAlign, PTabBase}; let d = ptab_doc("Left", "Right", ptab(PTabAlign::Right, PTabBase::Margin, wordcraft_doc::props::TabLeader::Dot), (0.0, 36.0)); let l = lay(&d); @@ -1009,6 +1011,7 @@ fn right_positional_tab_ends_text_on_the_right_margin() { /// and right indents, not between the margins. #[test] fn center_positional_tab_relative_to_indents_centres_between_them() { + crate::test_fonts(); use wordcraft_doc::para::{PTabAlign, PTabBase}; let d = ptab_doc("a", "Mid", ptab(PTabAlign::Center, PTabBase::Indent, wordcraft_doc::props::TabLeader::None), (36.0, 108.0)); let l = lay(&d); @@ -1022,6 +1025,7 @@ fn center_positional_tab_relative_to_indents_centres_between_them() { /// that place. #[test] fn positional_tab_behind_the_text_moves_to_the_next_line() { + crate::test_fonts(); use wordcraft_doc::para::{PTabAlign, PTabBase}; let before = "word ".repeat(14); let d = ptab_doc(&before, "X", ptab(PTabAlign::Left, PTabBase::Margin, wordcraft_doc::props::TabLeader::None), (0.0, 0.0)); @@ -1036,6 +1040,7 @@ fn positional_tab_behind_the_text_moves_to_the_next_line() { /// doesn't leave it blank to go to the next line, even when its place is behind the indent. #[test] fn positional_tab_after_only_a_bookmark_stays_on_its_line() { + crate::test_fonts(); use wordcraft_doc::para::{PTabAlign, PTabBase}; let mut d = ptab_doc("", "X", ptab(PTabAlign::Left, PTabBase::Margin, wordcraft_doc::props::TabLeader::None), (36.0, 0.0)); d.insert_object(&Pos::body(0, 0), InlineObject::BookmarkStart { name: "top".into() }, &Default::default()).unwrap(); @@ -1048,6 +1053,7 @@ fn positional_tab_after_only_a_bookmark_stays_on_its_line() { #[test] fn narrow_cells_keep_their_width() { + crate::test_fonts(); let mut p = wordcraft_doc::Paragraph::with_text("WW", wordcraft_doc::CharProps { size: Some(6.0), ..Default::default() }); p.props.space_before = Some(0.0); p.props.space_after = Some(0.0); @@ -1256,6 +1262,7 @@ fn symbol_font_bullet_without_the_font_draws_a_bullet() { #[test] fn pcl6_symbol_draws_greater_equal() { + crate::test_fonts(); let mut d = Document::from_text("\u{F0B3}"); d.format_range(&Pos::body(0, 0), &Pos::body(0, 3), &|c| c.font = Some("Symbol (PCL6)".into())).unwrap(); let l = lay(&d); @@ -1267,6 +1274,7 @@ fn pcl6_symbol_draws_greater_equal() { #[test] fn private_use_bullet_no_font_draws_shows_the_levels_default_bullet() { + crate::test_fonts(); // #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"); @@ -1584,6 +1592,7 @@ fn headers_and_page_fields() { /// typed after them wraps to the next line. Same in the body and in a header. #[test] fn caret_after_hanging_spaces_stays_at_the_margin() { + crate::test_fonts(); let spaces = format!("a{}", " ".repeat(200)); for header in [false, true] { for text in [spaces.clone(), format!("{spaces}b")] { @@ -2604,6 +2613,7 @@ fn a_split_row_keeps_its_bottom_margin_and_widow_control() { /// as usual. #[test] fn a_cell_emptied_by_widow_control_takes_its_row_to_the_next_page() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; // Letter, 1 inch margins: 648 pt of text. `filler` lines of exactly 14 pt, then a row whose // second cell holds one paragraph of `lines` lines, 14 pt each. @@ -2694,6 +2704,7 @@ fn row_content_pieces(text: &str, props: ParaProps, room: f32) -> Vec<(usize, us #[test] fn row_split_ignores_afterspace() { + crate::test_fonts(); let two = ["Top", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string()); for (text, room, expected) in [("Label", 18.0, vec![(0, 1), (1, 3)]), (two.as_str(), 32.0, vec![(0, 2), (2, 2)])] { let props = ParaProps { space_after: Some(8.0), widow_control: Some(true), ..Default::default() }; @@ -2703,6 +2714,7 @@ fn row_split_ignores_afterspace() { #[test] fn row_split_respects_widows() { + crate::test_fonts(); let two = ["Top", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string()); let three = ["Top", "Middle", "Bottom"].join(&wordcraft_doc::para::LINE_BREAK.to_string()); for (text, widow, room, expected) in [ @@ -3085,6 +3097,7 @@ fn drop_cap_layout(set: impl Fn(&mut wordcraft_doc::props::ParaProps)) -> std::s /// letter left of it, the distance away; the letter takes the chosen font. #[test] fn drop_cap_options_place_the_letter() { + crate::test_fonts(); let x0 = |pl: &crate::para::ParaLayout, k: usize| pl.lines.get(k).and_then(|l| l.xs.first().copied()).unwrap_or(f32::NAN); let letter_x = |pl: &crate::para::ParaLayout| x0(pl, 0) + pl.glyphs.first().map(|g| g.dx).unwrap_or(f32::NAN); @@ -3938,6 +3951,7 @@ fn exact_rows_add_only_the_bottom_margin() { #[test] fn exact_row_moves_whole() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; let lines: Vec = (1..=200).map(|i| format!("Filler {i}")).collect(); let one_page = lay(&Document::from_text(&lines.join("\n"))).pages[0].items.iter().filter(|i| matches!(i, Placed::Lines { .. })).count(); @@ -3949,7 +3963,7 @@ fn exact_row_moves_whole() { p.insert_object(0, picture(30.0, height, Default::default()), &Default::default()).unwrap(); t.rows[0].cells[0].blocks.push(wordcraft_doc::para_block(p)); } - let mut d = Document::from_text(&lines[..one_page - 2].join("\n")); + let mut d = Document::from_text(&lines[..one_page - 1].join("\n")); let end = d.body.len(); d.insert_block(StoryRef::Body, &Path::top(end), wordcraft_doc::Block::Table(t)).unwrap(); let l = lay(&d); @@ -4179,6 +4193,7 @@ fn floats_in_headers_and_table_cells_are_drawn() { /// its own keeps it (Word, observed with synthetic documents). #[test] fn automatic_text_in_a_styled_text_box_takes_the_box_s_colour() { + crate::test_fonts(); let glyph_colors = |auto_color: Option| { let mut d = Document::from_text("Body"); let id = text_box(&mut d, 4, "Auto", 200.0, 60.0, page_float(100.0, 300.0)); @@ -4209,6 +4224,7 @@ fn automatic_text_in_a_styled_text_box_takes_the_box_s_colour() { /// stays there. #[test] fn header_cell_float_placed_on_the_page() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap}; let mut d = Document::from_text(&"Body text of the contract. ".repeat(400)); let wm = |outside_cell: bool| Float { @@ -4627,6 +4643,7 @@ fn no_line_break_right_after_a_slash() { /// "and/or" whole), so the slash word had no break before it and split between two letters. #[test] fn a_word_after_a_slash_wraps_whole() { + crate::test_fonts(); let (head, slash, tail) = ("ЖСН енгізілмеген жағдайда толтырылады", " /", "заполняется в случае неприсвоения ИИН"); let text = format!("{head}{slash}{tail}"); let italic = wordcraft_doc::CharProps { italic: Some(true), ..Default::default() }; @@ -4671,6 +4688,7 @@ fn a_word_after_a_slash_wraps_whole() { /// #2963 happened in a fixed-width table cell: the cell's own width is the line width there. #[test] fn a_word_after_a_slash_wraps_whole_in_a_fixed_cell() { + crate::test_fonts(); let (head, slash, tail) = ("ЖСН енгізілмеген жағдайда толтырылады", " /", "заполняется в случае неприсвоения ИИН"); let text = format!("{head}{slash}{tail}"); let italic = wordcraft_doc::CharProps { italic: Some(true), ..Default::default() }; @@ -4718,6 +4736,7 @@ fn a_word_after_a_slash_wraps_whole_in_a_fixed_cell() { /// slash keeps it on the line. #[test] fn a_no_break_space_before_a_slash_still_holds() { + crate::test_fonts(); let starts = |nb: char| -> (usize, usize) { // Varying word lengths, so the lines end at different places in the pattern. let text: String = (0..80).map(|i| format!("caption{nb}/word {} ", "w".repeat(1 + i % 5))).collect(); @@ -5078,6 +5097,7 @@ fn inline_picture_wider_than_the_text_keeps_its_size() { /// the anchor an empty line of its own above it (Word). #[test] fn a_float_anchor_does_not_push_a_wide_picture_to_a_second_line() { + crate::test_fonts(); let lines_of = |with_anchor: bool| { let mut d = Document::from_text(""); let mut at = Pos::body(0, 0); @@ -5306,6 +5326,7 @@ fn lines_in_cell(n: usize, what: &str) -> Vec= 2 && pages.iter().all(|p| p == &vec![0]), "{pages:?}"); // A 100 pt header row, then a row at least 600 pt with 80 lines: under the header only 548 pt @@ -5391,6 +5415,7 @@ fn an_at_least_row_at_the_top_of_a_page_splits_instead_of_moving_on() { #[test] fn repeated_header_moves_with_a_first_row_that_moves_on_whole() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; // A 20 pt header, then a row at least 200 pt holding eight lines, with 100 pt left: the row // moves on whole, so the header goes with it rather than staying alone at the foot of page 1. @@ -6425,6 +6450,7 @@ fn hyphenation_and_proofing_follow_each_runs_language() { /// style's; a paragraph that switches the style's list off (`num: 0`) is plain again. #[test] fn list_item_first_word_gets_no_capital_squiggle() { + crate::test_fonts(); use wordcraft_doc::props::NumRef; let opts = LayoutOptions { proofing: true, ..LayoutOptions::default() }; let grammar_squiggles = |d: &Document| -> usize { @@ -6485,6 +6511,7 @@ fn unproofed_languages_and_no_proof_runs_are_not_marked() { /// ignored), on every page, like the same fields in the footer. #[test] fn shared_section_page_totals() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; use wordcraft_doc::section::{SectionProps, SectionStart}; #[derive(Clone, Copy)] @@ -6864,6 +6891,7 @@ fn a_text_frame_in_a_cell_floats_beside_the_text() { /// were drawn over each other (#2357). #[test] fn a_frame_inside_a_text_box_flows_with_its_text() { + crate::test_fonts(); use wordcraft_doc::props::TextFrame; let mut d = Document::from_text("Body"); let id = text_box(&mut d, 0, "", 150.0, 120.0, Default::default()); @@ -7115,6 +7143,7 @@ fn keep_with_next_carries_the_heading_when_keep_lines_moves_the_next_paragraph() #[test] fn section_mark_hidden_styles() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; use wordcraft_doc::section::{SectionProps, SectionStart}; use wordcraft_doc::{CharProps, Style, StyleKind}; @@ -7160,6 +7189,7 @@ fn section_mark_hidden_styles() { #[test] fn empty_page_section_keeps_line() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; let mut doc = Document::from_text("\nAfter empty section\n\nReference prefix\n\nAfter middle mark"); for index in 0..6 { @@ -7412,6 +7442,7 @@ fn kerning_follows_kerning_for_fonts_like_word() { /// turned them off. Without a value WordCraft keeps its default, standard and contextual. #[test] fn ligatures_follow_w14_ligatures() { + crate::test_fonts(); use wordcraft_doc::props::{CharProps, Ligatures}; use wordcraft_doc::styles::{Style, StyleKind}; let text = "office affinity"; @@ -7450,6 +7481,7 @@ fn ligatures_follow_w14_ligatures() { /// historical `hlig`, discretional `dlig`. #[test] fn ligature_flags_map_to_opentype_features() { + crate::test_fonts(); use wordcraft_doc::props::Ligatures; let f = |t: &[u8; 4], v: u32| wordcraft_fonts::feature_value(t, v); let hd = Ligatures { historical: true, discretional: true, ..Default::default() }; @@ -8498,6 +8530,7 @@ fn justified_multi_column_pages_stay_at_the_top() { /// it, and a deletion that isn't left out keeps its line. #[test] fn a_paragraph_left_out_whole_takes_no_room() { + crate::test_fonts(); let mut d = Document::from_text("One\nGone\nThree"); d.format_range(&Pos::body(1, 0), &Pos::body(2, 0), &|c| c.del = Some(0)).unwrap(); let three_top = @@ -8517,6 +8550,7 @@ fn a_paragraph_left_out_whole_takes_no_room() { /// lines across the page at its zero height). #[test] fn a_paragraph_left_out_whole_draws_no_borders_or_shading() { + crate::test_fonts(); let mut d = Document::from_text("One\nGone\nThree"); let line = Some(Border::single(1.0)); d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 0), &|p| { @@ -8566,6 +8600,7 @@ fn row_nil_overrides_style() { /// Continued text keeps the cell's border inset, as on its first fragment. #[test] fn split_cell_reuses_top_inset() { + crate::test_fonts(); use wordcraft_doc::props::{HeightRule, LineSpacing}; for (width, margin) in [(0.0, 0.0), (0.5, 0.0), (2.0, 0.0), (0.0, 6.0), (0.5, 6.0), (2.0, 6.0)] { for spacing in [LineSpacing::Exactly(10.0), LineSpacing::AtLeast(10.0)] { @@ -8617,6 +8652,7 @@ fn split_cell_reuses_top_inset() { /// Images and their floating hit areas move together without changing size or crop. #[test] fn split_cell_moves_image_frame() { + crate::test_fonts(); use crate::table::{CellPart, RowLayout, split_row}; use wordcraft_doc::para::Wrap; for top in [0.0, 0.5, 6.5, 8.0] { @@ -8725,6 +8761,7 @@ fn row_keep_pages(d: &Document) -> Vec { #[test] fn kept_row_moves_with_next() { + crate::test_fonts(); for rule in [wordcraft_doc::props::HeightRule::AtLeast, wordcraft_doc::props::HeightRule::Exact] { let mut d = row_keep_doc(); d.table_mut(StoryRef::Body, &Path::top(1)).unwrap().rows[1].props.height_rule = rule; @@ -8737,6 +8774,7 @@ fn kept_row_moves_with_next() { #[test] fn kept_row_allows_next_split() { + crate::test_fonts(); let mut d = row_keep_doc(); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); t.rows[1].props.height = None; @@ -8749,6 +8787,7 @@ fn kept_row_allows_next_split() { #[test] fn kept_rows_move_as_chain() { + crate::test_fonts(); let mut d = row_keep_doc(); d.para_mut(StoryRef::Body, &Path::top(0)).unwrap().props.line_spacing = Some(wordcraft_doc::props::LineSpacing::Exactly(170.0)); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); @@ -8766,6 +8805,7 @@ fn kept_rows_move_as_chain() { #[test] fn oversized_kept_rows_finish() { + crate::test_fonts(); let mut d = row_keep_doc(); d.table_mut(StoryRef::Body, &Path::top(1)).unwrap().rows[1].props.height = Some(400.0); let l = lay(&d); @@ -8775,6 +8815,7 @@ fn oversized_kept_rows_finish() { #[test] fn kept_row_can_split_itself() { + crate::test_fonts(); let mut d = row_keep_doc(); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); t.rows[0].props.height = None; @@ -8790,6 +8831,7 @@ fn kept_row_can_split_itself() { #[test] fn first_cell_controls_keep() { + crate::test_fonts(); for (first, last, pages) in [(true, false, [1, 1]), (false, true, [0, 1]), (true, true, [1, 1]), (false, false, [0, 1])] { let mut d = row_keep_doc(); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); @@ -8809,6 +8851,7 @@ fn first_cell_controls_keep() { #[test] fn first_para_controls_row_keep() { + crate::test_fonts(); let mut d = row_keep_doc(); let mut p = Paragraph::with_text("Last paragraph", Default::default()); p.props = d.para(StoryRef::Body, &Path(vec![1, 0, 0, 0])).unwrap().props.clone(); @@ -8822,6 +8865,7 @@ fn first_para_controls_row_keep() { #[test] fn nested_keep_leaves_outer_row() { + crate::test_fonts(); let mut d = row_keep_doc(); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); t.rows[0].props.height = Some(60.0); @@ -8835,6 +8879,7 @@ fn nested_keep_leaves_outer_row() { #[test] fn kept_prefix_stays_together() { + crate::test_fonts(); let mut d = row_keep_doc(); let t = d.table_mut(StoryRef::Body, &Path::top(1)).unwrap(); t.rows[1].props.height = Some(400.0); @@ -8849,6 +8894,7 @@ fn kept_prefix_stays_together() { /// even when its anchor paragraph has no inline picture (synthetic two-cell probe). #[test] fn cell_frame_centres_empty() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; use wordcraft_doc::props::{HeightRule, VAlign}; let mut d = Document::new(); @@ -8882,6 +8928,7 @@ fn cell_frame_centres_empty() { #[test] fn section_start_summed_spacing() { + crate::test_fonts(); for summed in [false, true] { let mut d = Document::from_text("Section end\nSection start"); d.settings.sum_paragraph_spacing = summed; @@ -8906,6 +8953,7 @@ fn section_start_summed_spacing() { #[test] fn partial_margins_inherit() { + crate::test_fonts(); let height = |top, bottom| { let mut table = Table::new(1, 1, 200.0); table.props.style = None; @@ -8937,6 +8985,7 @@ fn partial_margins_inherit() { /// Exact line pitch keeps the text baseline even when an equation has a deep denominator. #[test] fn exact_math_keeps_run_baseline() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; for display in [false, true] { for spacing in [LineSpacing::Exactly(12.0), LineSpacing::AtLeast(12.0), LineSpacing::Multiple(1.0)] { @@ -8989,6 +9038,7 @@ fn exact_math_keeps_run_baseline() { #[test] fn float_font_keeps_line_height() { + crate::test_fonts(); use wordcraft_doc::CharProps; use wordcraft_doc::para::{Float, Wrap}; @@ -9013,6 +9063,7 @@ fn float_font_keeps_line_height() { /// Native ink inside a group is drawn as drawing-level ink is: its fallback picture, in its place. #[test] fn ink_member_of_a_group_draws_its_fallback_picture() { + crate::test_fonts(); use crate::display::Draw; use std::sync::Arc; use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind}; @@ -9060,6 +9111,7 @@ fn ink_member_of_a_group_draws_its_fallback_picture() { /// again without clearing the cache. #[test] fn the_word_being_typed_has_no_spelling_squiggle() { + crate::test_fonts(); let d = Document::from_text("helo wrld\nfine text here"); let rev = d.para(StoryRef::Body, &Path::top(0)).unwrap().rev; let marks = |l: &DocLayout, grammar: bool| -> Vec { @@ -9122,6 +9174,7 @@ fn cell_break_doc(text: &str) -> Document { #[test] fn cell_breaks_take_no_height() { + crate::test_fonts(); let height = |d: &Document| { lay(d).pages[0] .items @@ -9162,6 +9215,7 @@ fn cell_breaks_take_no_height() { #[test] fn cell_break_cache_is_scoped() { + crate::test_fonts(); let mut d = cell_break_doc("\u{000c}Heading"); let Block::Table(t) = &*d.body[0] else { panic!("table") }; let p = t.rows[0].cells[0].blocks[0].clone(); @@ -9171,6 +9225,7 @@ fn cell_break_cache_is_scoped() { #[test] fn cell_breaks_with_bookmarks() { + crate::test_fonts(); for offset in [0, 1, 3] { let mut d = cell_break_doc("\u{000c}\u{000c}\u{000c}Heading"); let Block::Table(t) = Arc::make_mut(&mut d.body[0]) else { panic!("table") }; @@ -9197,6 +9252,7 @@ fn cell_breaks_with_bookmarks() { #[test] fn cell_break_only_is_marker() { + crate::test_fonts(); let d = cell_break_doc("\u{000c}"); let l = lay(&d); let para = l.pages[0] @@ -9212,6 +9268,7 @@ fn cell_break_only_is_marker() { #[test] fn subpoint_grid_keeps_widths() { + crate::test_fonts(); for gap in [0.05, 0.5, 1.0, 3.0] { let mut d = Document::new(); let mut table = Table::new(1, 3, 300.0); @@ -9240,6 +9297,7 @@ fn subpoint_grid_keeps_widths() { #[test] fn invalid_grid_keeps_fallback() { + crate::test_fonts(); for width in [0.0, -1.0, f32::NAN, f32::INFINITY] { let mut d = Document::new(); let mut table = Table::new(1, 2, 300.0); @@ -9265,6 +9323,7 @@ fn invalid_grid_keeps_fallback() { #[test] fn tiny_grid_scales_to_width() { + crate::test_fonts(); for unit in [0.05, 0.25, 0.5, f32::MIN_POSITIVE, f32::from_bits(1)] { for ratio in [1.0, 3.0] { let mut d = Document::new(); @@ -9311,6 +9370,7 @@ fn absolute_footer(text: &str, height: f32) -> Document { #[test] fn footer_absolute_height() { + crate::test_fonts(); let mut base = absolute_footer("", 20.0); let id = base.last_section.footers.default.unwrap(); base.parts.get_mut(&id).unwrap().blocks.remove(1); @@ -9347,6 +9407,7 @@ fn footer_absolute_height() { #[test] fn footer_float_wraps_body() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; const FLOAT_TOP: f32 = 100.0; const FLOAT_RIGHT: f32 = 468.0; @@ -9390,6 +9451,7 @@ fn footer_float_wraps_body() { #[test] fn footer_table_wrap_bounds() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::{HeightRule, LineSpacing, TableFloat}; const FLOAT_TOP: f32 = 100.0; @@ -9420,6 +9482,7 @@ fn footer_table_wrap_bounds() { #[test] fn header_table_collision() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::{HeightRule, TableFloat}; @@ -9485,6 +9548,7 @@ fn band_row_table(top: Option) -> Table { #[test] fn shared_band_row_minimum() { + crate::test_fonts(); let hidden = Border { style: BorderStyle::None, width: 0.0, color: None, space: 0.0 }; let shown = Border { style: BorderStyle::Single, width: SHARED_BAND, color: None, space: 0.0 }; // The previous row already paints the shared edge. Its band still clears the next cell. @@ -9506,6 +9570,7 @@ fn shared_band_row_minimum() { #[test] fn shared_band_carries_heading() { + crate::test_fonts(); use wordcraft_doc::props::{HeightRule, LineSpacing}; for (page_height, heading_page) in [(150.0, 1), (156.0, 0)] { let mut doc = Document::new(); @@ -9545,6 +9610,7 @@ fn shared_band_carries_heading() { #[test] fn shared_band_exact_rows() { + crate::test_fonts(); let mut table = band_row_table(None); for row in &mut table.rows { row.props.height_rule = wordcraft_doc::props::HeightRule::Exact; @@ -9565,6 +9631,7 @@ fn shared_band_exact_rows() { #[test] fn shared_band_borderless() { + crate::test_fonts(); let mut table = band_row_table(None); for row in &mut table.rows { row.cells[0].props.borders = Some(Borders::default()); @@ -9586,6 +9653,7 @@ fn shared_band_borderless() { #[test] fn shared_band_merge_boundary() { + crate::test_fonts(); use wordcraft_doc::props::VMerge; let mut table = band_row_table(None); table.rows[0].cells[0].props.vmerge = VMerge::Restart; @@ -9606,6 +9674,7 @@ fn shared_band_merge_boundary() { #[test] fn shared_band_grid_spans() { + crate::test_fonts(); let mut table = band_row_table(None); table.rows.truncate(2); table.grid = vec![100.0; 3]; diff --git a/crates/layout/src/tests_autospace.rs b/crates/layout/src/tests_autospace.rs index 025527642c..ebcd573bac 100644 --- a/crates/layout/src/tests_autospace.rs +++ b/crates/layout/src/tests_autospace.rs @@ -41,6 +41,7 @@ fn quarter(d: &Document) -> f32 { #[test] fn digits_and_latin_beside_asian_text_get_a_quarter_of_the_font_size() { + crate::test_fonts(); let mut d = Document::from_text("法院94年A字"); let q = quarter(&d); assert!(q > 0.0); @@ -56,6 +57,7 @@ fn digits_and_latin_beside_asian_text_get_a_quarter_of_the_font_size() { #[test] fn the_space_crosses_runs_and_takes_the_asian_size() { + crate::test_fonts(); let mut d = Document::from_text("漢A漢"); let size = |s: f32| move |c: &mut CharProps| c.size = Some(s); let p = para0(&mut d); @@ -67,6 +69,7 @@ fn the_space_crosses_runs_and_takes_the_asian_size() { #[test] fn spaces_punctuation_hangul_and_latin_alone_get_nothing() { + crate::test_fonts(); for text in ["例 a", "5 例", "5、", "、8", "abc 123", "漢한", "한A", "漢%", "(A)"] { let d = Document::from_text(text); assert!(gaps(&d).iter().all(|g| *g == 0.0), "{text}: {:?}", gaps(&d)); @@ -75,6 +78,7 @@ fn spaces_punctuation_hangul_and_latin_alone_get_nothing() { #[test] fn a_field_result_is_classed_by_the_text_it_shows() { + crate::test_fonts(); let mut d = Document::from_text("漢漢"); let field = InlineObject::Field { instr: "MERGEFIELD n".into(), result: "12".into(), locked: false, dirty: false, code: false, form: String::new() }; @@ -90,6 +94,7 @@ fn a_field_result_is_classed_by_the_text_it_shows() { #[test] fn a_character_grid_the_paragraph_snaps_to_turns_it_off() { + crate::test_fonts(); let with = |kind: DocGridType, snap: bool| { let mut d = Document::from_text("法院94年"); d.last_section.doc_grid = Some(DocGrid { kind, line_pitch: 18.0, char_space: 0 }); @@ -107,6 +112,7 @@ fn a_character_grid_the_paragraph_snaps_to_turns_it_off() { #[test] fn the_space_takes_no_room_at_the_end_of_a_justified_line() { + crate::test_fonts(); let text = "abcdef 1漢漢漢"; let mut d = Document::from_text(text); let pl = first_para(&lay(&d)); diff --git a/crates/layout/src/tests_complex.rs b/crates/layout/src/tests_complex.rs index b1495a43d3..6a2147ab2b 100644 --- a/crates/layout/src/tests_complex.rs +++ b/crates/layout/src/tests_complex.rs @@ -136,6 +136,7 @@ fn cluster_faces(text: &str, font: Option<&str>) -> Vec<(usize, wordcraft_fonts: #[test] fn a_missing_thai_font_draws_the_whole_run_in_one_thai_face() { + crate::test_fonts(); if !thai_font_installed() { eprintln!("skipped: no Thai font installed"); return; @@ -158,6 +159,7 @@ fn a_missing_thai_font_draws_the_whole_run_in_one_thai_face() { #[test] fn a_thai_substitute_without_latin_hands_the_run_to_one_face_covering_it() { + crate::test_fonts(); if !thai_font_installed() { eprintln!("skipped: no Thai font installed"); return; @@ -181,6 +183,7 @@ fn a_thai_substitute_without_latin_hands_the_run_to_one_face_covering_it() { #[test] fn latin_text_in_a_missing_latin_font_stays_in_its_substitute_beside_thai() { + crate::test_fonts(); if !thai_font_installed() { eprintln!("skipped: no Thai font installed"); return; @@ -212,6 +215,7 @@ fn latin_text_in_a_missing_latin_font_stays_in_its_substitute_beside_thai() { #[test] fn thai_in_an_installed_latin_font_keeps_the_latin_face_for_latin() { + crate::test_fonts(); if !thai_font_installed() { eprintln!("skipped: no Thai font installed"); return; @@ -227,6 +231,7 @@ fn thai_in_an_installed_latin_font_keeps_the_latin_face_for_latin() { #[test] fn a_missing_latin_font_with_a_thai_complex_script_font_keeps_both() { + crate::test_fonts(); if !thai_font_installed() { eprintln!("skipped: no Thai font installed"); return; @@ -248,6 +253,7 @@ fn a_missing_latin_font_with_a_thai_complex_script_font_keeps_both() { #[test] fn a_missing_roman_thai_font_is_not_drawn_in_cambria() { + crate::test_fonts(); // Word draws a missing font the font table classes as roman in Cambria, but a missing Thai // font keeps its Thai substitute: the same face whether or not the table calls it roman. let db = wordcraft_fonts::FontDb::global(); diff --git a/crates/layout/src/tests_pages.rs b/crates/layout/src/tests_pages.rs index 7c00821f59..c8857daeba 100644 --- a/crates/layout/src/tests_pages.rs +++ b/crates/layout/src/tests_pages.rs @@ -519,6 +519,7 @@ fn widow_control_moves_a_three_line_paragraph_whole() { #[test] fn footer_border_limits_body() { + crate::test_fonts(); use wordcraft_doc::props::{Border, Borders, HeightRule}; let mut d = Document::from_text(&"Body line\n".repeat(40)); d.last_section.page_h = 400.0; @@ -548,6 +549,7 @@ fn footer_border_limits_body() { /// number format is known, isn't taken for the page's own footer. #[test] fn a_roman_page_number_in_the_footer_stays_roman_with_the_page_count_in_the_body() { + crate::test_fonts(); let field = |instr: &str| InlineObject::Field { instr: instr.into(), result: String::new(), diff --git a/crates/layout/src/tests_styleref.rs b/crates/layout/src/tests_styleref.rs index b7e4f7d5dc..9cf44d8d11 100644 --- a/crates/layout/src/tests_styleref.rs +++ b/crates/layout/src/tests_styleref.rs @@ -210,6 +210,7 @@ fn field_char_xs(d: &Document) -> Vec<(String, Vec)> { /// also in a right-to-left header paragraph. #[test] fn hebrew_styleref_result_reads_right_to_left() { + crate::test_fonts(); let d = doc(&[("Heading1", "פרק א", false), ("Normal", "Text.", false)], &[r#"STYLEREF "Heading 1""#]); let got = field_char_xs(&d); let (text, xs) = &got[0]; @@ -278,6 +279,7 @@ fn rtl_header(d: &mut Document) { /// Hebrew word in brackets in a left-to-right result draws its brackets the right way round. #[test] fn field_results_mirror_brackets_with_their_bidi_runs() { + crate::test_fonts(); let mut got = Vec::new(); // The field-code view of a PAGE field in a right-to-left header. let mut d = doc(&[("Normal", "Text.", false)], &["PAGE"]); diff --git a/crates/layout/src/tests_textbox.rs b/crates/layout/src/tests_textbox.rs index 5710170d87..d8d7d41ae3 100644 --- a/crates/layout/src/tests_textbox.rs +++ b/crates/layout/src/tests_textbox.rs @@ -185,6 +185,7 @@ fn own_margins_and_resize_to_fit_keep_the_text_in_view() { #[test] fn inline_box_that_resizes_to_fit_takes_its_texts_height() { + crate::test_fonts(); // #2791: an inline box with Resize shape to fit text, saved 10 cm tall around three short // lines, shrinks to them as in Word; the page break after it stays on the first page. let brk = wordcraft_doc::para::PAGE_BREAK; @@ -213,4 +214,4 @@ fn inline_box_that_resizes_to_fit_takes_its_texts_height() { /// Filler paragraphs before the box in [`inline_box_that_resizes_to_fit_takes_its_texts_height`]: /// enough that a 10 cm box no longer leaves room for the page break on the first page. -const FILLER: usize = 22; +const FILLER: usize = 16; diff --git a/crates/layout/src/wordart.rs b/crates/layout/src/wordart.rs index d2b8c78e7d..790bcfbc11 100644 --- a/crates/layout/src/wordart.rs +++ b/crates/layout/src/wordart.rs @@ -374,6 +374,7 @@ mod tests { /// #2534: the legacy font effects become text effects, under any WordArt effects. #[test] fn legacy_font_effects_are_drawn_as_text_effects() { + crate::test_fonts(); use wordcraft_doc::CharProps; let rc = |c: CharProps| wordcraft_doc::styles::StyleSheet::default().resolve_char(None, &CharProps { size: Some(20.0), ..c }); let red = Rgb(200, 0, 0); @@ -405,6 +406,7 @@ mod tests { /// #2534: synthetic bold widens the outline, or adds one in the fill's colour. #[test] fn embolden_widens_or_adds_the_outline() { + crate::test_fonts(); let red = Rgb(200, 0, 0); let mut plain = TextEffects::default(); embolden(&mut plain, red, 20.0); @@ -429,6 +431,7 @@ mod tests { /// draws its glyphs plainly. #[test] fn effects_on_tracked_and_turned_text() { + crate::test_fonts(); use crate::display::{DisplayOptions, Draw, InsertMark, page_display}; use wordcraft_doc::props::TextDirection; use wordcraft_doc::{CharProps, Document, Paragraph, Revision, RevisionKind, para_block}; From 9b6d326730c5f5557b418d2a5e9cb83c631ad6c3 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Tue, 20 Oct 2026 01:45:42 +0200 Subject: [PATCH 6/6] #2729: initialize pinned fonts in current ligature and caret tests --- ROADMAP.md | 1 + crates/layout/src/tests.rs | 1 + crates/layout/src/tests_ligature.rs | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 4ea8644828..ed05219e12 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -141,6 +141,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target - 2026-10-15: Find opens Navigation on Results; editing the query restores Results after manual Headings/Pages navigation. Typed and selected-text searches retain document contents (#2096). | Date | What landed | +| 2026-10-20 | Pinned layout test fonts also initialize before the new caret-height and ligature fixtures, keeping isolated test runs independent of system fonts (#2729). | | 2026-10-19 | Save As, Export and Open no longer do nothing on Linux desktops whose file chooser portal fails to show its dialog (#2999): a "cancel" quicker than anyone could make one asks again without the parent window, then opens the built-in file browser. | | 2026-10-18 | Long navigation headings wrap onto further lines inside the resizable pane instead of ending in an ellipsis: the row grows with the text and the pane keeps its width (#759, #3015). | | 2026-10-17 | Layout unit tests use the pinned bundled fonts without system/user fonts or optional craft-fonts inputs (#2729); every test initializes the fixture before font lookup, and pagination fixtures are measured against that fixed set | diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 3c364cc12b..e201078574 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -9712,6 +9712,7 @@ fn shared_band_grid_spans() { #[test] fn caret_height_ignores_line_spacing() { + crate::test_fonts(); // #2920: extra line spacing is leading below the text; the caret stays the height of the font. use wordcraft_doc::props::LineSpacing; let caret_height = |spacing: Option| { diff --git a/crates/layout/src/tests_ligature.rs b/crates/layout/src/tests_ligature.rs index 18fb611d33..506a61b38c 100644 --- a/crates/layout/src/tests_ligature.rs +++ b/crates/layout/src/tests_ligature.rs @@ -44,6 +44,7 @@ fn has_fi_ligature(l: &DocLayout) -> bool { #[test] fn caret_between_the_letters_of_a_ligature_is_inside_it() { + crate::test_fonts(); let l = lay(&doc("fi")); assert!(has_fi_ligature(&l), "the default font should draw fi as a ligature"); let (a, b, c) = (caret_x(&l, 0), caret_x(&l, 1), caret_x(&l, 2)); @@ -62,6 +63,7 @@ fn fi_width_with_spacing(sp: Option) -> (DocLayout, f32) { #[test] fn character_spacing_inside_a_ligature_is_kept() { + crate::test_fonts(); let (l0, plain) = fi_width_with_spacing(None); assert!(has_fi_ligature(&l0), "the default font should draw fi as a ligature"); for sp in [3.0_f32, -0.5] { @@ -75,6 +77,7 @@ fn character_spacing_inside_a_ligature_is_kept() { #[test] fn clicks_inside_a_ligature_land_between_its_letters() { + crate::test_fonts(); let l = lay(&doc("fi")); let (a, b, c) = (caret_x(&l, 0), caret_x(&l, 1), caret_x(&l, 2)); let cr = l.caret(&Pos::body(0, 1)).unwrap(); @@ -88,6 +91,7 @@ fn clicks_inside_a_ligature_land_between_its_letters() { #[test] fn every_letter_of_a_three_letter_ligature_has_its_own_caret() { + crate::test_fonts(); let l = lay(&doc("ffi")); assert!(has_fi_ligature(&l), "the default font should draw ffi as a ligature"); let xs: Vec = (0..=3).map(|o| caret_x(&l, o)).collect(); @@ -96,6 +100,7 @@ fn every_letter_of_a_three_letter_ligature_has_its_own_caret() { #[test] fn a_combining_mark_stays_in_its_letters_cluster() { + crate::test_fonts(); let l = lay(&doc("fi\u{301}")); let pl = para_layout(&l); let offs = pl.line_offsets(0); @@ -104,6 +109,7 @@ fn a_combining_mark_stays_in_its_letters_cluster() { #[test] fn selection_edges_inside_a_ligature_use_the_same_positions() { + crate::test_fonts(); let d = doc("fi"); let l = lay(&d); let rects = l.selection_rects(&d, &Pos::body(0, 0), &Pos::body(0, 1), 0); @@ -134,6 +140,7 @@ fn lam_alef_ligature() -> Option { #[test] fn a_right_to_left_ligature_splits_mirrored() { + crate::test_fonts(); let Some(l) = lam_alef_ligature() else { eprintln!("skipped: no lam-alef ligature font"); return; @@ -146,6 +153,7 @@ fn a_right_to_left_ligature_splits_mirrored() { #[test] fn a_right_to_left_ligature_is_drawn_from_its_left_edge() { + crate::test_fonts(); // The glyph stays on lam, the rightmost letter; it must still start at alef's left edge. let Some(l) = lam_alef_ligature() else { eprintln!("skipped: no lam-alef ligature font"); @@ -162,6 +170,7 @@ fn a_right_to_left_ligature_is_drawn_from_its_left_edge() { #[test] fn an_emoji_zwj_sequence_stays_one_cluster() { + crate::test_fonts(); // 👩‍💻: woman, zero-width joiner, laptop — one grapheme, one caret stop on each side. let t = "a\u{1F469}\u{200D}\u{1F4BB}b"; let l = lay(&Document::from_text(t)); @@ -173,6 +182,7 @@ fn an_emoji_zwj_sequence_stays_one_cluster() { #[test] fn an_indic_conjunct_stays_one_cluster() { + crate::test_fonts(); // क्ष: ka, virama, ssa — one conjunct; the caret never stops inside it. let l = lay(&Document::from_text("\u{915}\u{94D}\u{937}")); let offs = para_layout(&l).line_offsets(0); @@ -181,6 +191,7 @@ fn an_indic_conjunct_stays_one_cluster() { #[test] fn a_long_word_never_wraps_inside_a_ligature() { + crate::test_fonts(); // A narrow page forces an emergency break in the unbroken word; whatever the offset, a line // never ends between a ligature's glyph and its glyphless letters. let mut seen_ligature = false; @@ -203,6 +214,7 @@ fn a_long_word_never_wraps_inside_a_ligature() { #[test] fn a_hyphenation_point_never_splits_a_ligature() { + crate::test_fonts(); // Soft hyphens sit between the letters of "ffi"; a line never ends between a ligature's glyph // and its glyphless letters, whether or not the soft hyphen leaves the ligature intact. for k in 0..24 {