mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-31 22:07:06 -06:00
refactor(engine): move cmd/page tests to cmd/page/tests.rs
This commit is contained in:
@@ -766,438 +766,4 @@ fn shifted_by_split(p: &Pos, at: &Pos, new: &Pos) -> Pos {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn doc() -> Session {
|
||||
Session::new(wordcraft_doc::Document::from_text("one\ntwo\nthree"))
|
||||
}
|
||||
|
||||
fn with_section(f: impl Fn(&mut Value)) -> Value {
|
||||
let mut v = serde_json::to_value(SectionProps::default()).unwrap();
|
||||
f(&mut v);
|
||||
json!({"section": v})
|
||||
}
|
||||
|
||||
/// `layout.pageSetup {section}` checks the values like the dedicated commands do.
|
||||
#[test]
|
||||
fn page_setup_section_is_validated() {
|
||||
let mut s = doc();
|
||||
let before = sect(&s);
|
||||
// Refused with an error, nothing changes: non-finite (overflowing), margins that leave no text room.
|
||||
for (what, p) in [
|
||||
("inf width", with_section(|v| v["pageW"] = json!(1e300))),
|
||||
("inf margin", with_section(|v| v["marginTop"] = json!(1e300))),
|
||||
("inf header", with_section(|v| v["header"] = json!(1e300))),
|
||||
("inf column space", with_section(|v| v["columns"]["space"] = json!(1e300))),
|
||||
("inf column width", with_section(|v| v["columns"]["widths"] = json!([[1e300, 0.0], [100.0, 0.0]]))),
|
||||
("inf line number distance", with_section(|v| v["lineNumbers"] = json!({"distance": 1e300}))),
|
||||
("inf grid pitch", with_section(|v| v["docGrid"] = json!({"type": "lines", "linePitch": 1e300, "charSpace": 0}))),
|
||||
("inf border space", with_section(|v| v["pageBorders"] = json!({"top": {"style": "single", "width": 1.0, "space": 1e300}}))),
|
||||
("inf border width", with_section(|v| v["pageBorders"] = json!({"top": {"style": "single", "width": 1e300, "space": 1.0}}))),
|
||||
("margins > page", with_section(|v| v["marginLeft"] = json!(5000.0))),
|
||||
("margins eat the height", with_section(|v| v["marginTop"] = json!(700.0))),
|
||||
("gutter eats the width", with_section(|v| v["gutter"] = json!(600.0))),
|
||||
("negative page", with_section(|v| v["pageW"] = json!(-5.0))),
|
||||
("zero page height", with_section(|v| v["pageH"] = json!(0.0))),
|
||||
] {
|
||||
assert!(s.run("layout.pageSetup", &p).is_err(), "{what}");
|
||||
assert_eq!(sect(&s), before, "{what}");
|
||||
}
|
||||
// Clamped.
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(100.0);
|
||||
v["pageH"] = json!(100.0);
|
||||
v["marginTop"] = json!(-10.0);
|
||||
v["marginBottom"] = json!(0.0);
|
||||
v["marginLeft"] = json!(0.0);
|
||||
v["marginRight"] = json!(0.0);
|
||||
v["header"] = json!(-3.0);
|
||||
v["footer"] = json!(1e6);
|
||||
v["columns"]["count"] = json!(1000);
|
||||
v["columns"]["space"] = json!(-4.0);
|
||||
v["lineNumbers"] = json!({"countBy": 0, "start": 0, "distance": 1e6, "restart": "page"});
|
||||
v["docGrid"] = json!({"type": "lines", "linePitch": 1e9, "charSpace": 2_000_000_000});
|
||||
v["pageNumStart"] = json!(4_000_000_000u64);
|
||||
v["footnotePr"] = json!({"numStart": 4_000_000_000u64});
|
||||
v["endnotePr"] = json!({"numStart": 0});
|
||||
v["pageBorders"] =
|
||||
json!({"top": {"style": "single", "width": 1e6, "space": -5.0}, "left": {"style": "single", "width": 0.0, "space": 1e6}});
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_w, x.page_h), (100.0, 100.0));
|
||||
assert_eq!(x.margin_top, 0.0);
|
||||
assert_eq!(x.header, 0.0);
|
||||
assert_eq!(x.footer, x.page_h, "a footer distance stays on the page");
|
||||
assert_eq!(x.columns.count, 12);
|
||||
assert_eq!(x.columns.space, 0.0);
|
||||
let l = x.line_numbers.unwrap();
|
||||
assert_eq!((l.count_by, l.start, l.distance), (1, 1, 1584.0));
|
||||
let g = x.doc_grid.unwrap();
|
||||
assert_eq!((g.line_pitch, g.char_space), (wordcraft_doc::section::MAX_GRID_PITCH, wordcraft_doc::section::MAX_CHAR_SPACE));
|
||||
assert_eq!(x.page_num_start, Some(100_000));
|
||||
assert_eq!(x.footnote_pr.num_start, Some(wordcraft_doc::section::MAX_NOTE_START));
|
||||
assert_eq!(x.endnote_pr.num_start, Some(1));
|
||||
let b = x.page_borders.unwrap();
|
||||
let (t, lf) = (b.top.unwrap(), b.left.unwrap());
|
||||
assert_eq!((t.width, t.space), (6.0, 0.0));
|
||||
assert_eq!((lf.width, lf.space), (0.25, 31.0));
|
||||
// Huge pages clamp; columns and widths agree; orientation follows the sides.
|
||||
let mut s = doc();
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(1e5);
|
||||
v["pageH"] = json!(792.0);
|
||||
v["landscape"] = json!(false);
|
||||
v["columns"]["count"] = json!(3);
|
||||
v["columns"]["widths"] = json!(vec![json!([1.0, 1e5]); 40]);
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!(x.page_w, 1584.0);
|
||||
assert!(x.landscape, "wider than tall is landscape");
|
||||
assert_eq!(x.columns.widths.len(), 12);
|
||||
assert_eq!(x.columns.count, 12, "the count follows the widths");
|
||||
assert!(x.columns.widths.iter().all(|(w, sp)| *w >= 18.0 && (0.0..=300.0).contains(sp)));
|
||||
// One width is no custom layout: equal columns.
|
||||
s.run("layout.pageSetup", &with_section(|v| v["columns"]["widths"] = json!([[200.0, 0.0]]))).unwrap();
|
||||
assert!(sect(&s).columns.widths.is_empty());
|
||||
}
|
||||
|
||||
/// A valid section still applies as given.
|
||||
#[test]
|
||||
fn page_setup_section_applies_a_valid_one() {
|
||||
let mut s = doc();
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(595.0);
|
||||
v["pageH"] = json!(842.0);
|
||||
v["marginLeft"] = json!(50.0);
|
||||
v["columns"]["count"] = json!(2);
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_w, x.page_h, x.margin_left, x.columns.count), (595.0, 842.0, 50.0, 2));
|
||||
}
|
||||
|
||||
/// A page smaller than the usual 36 pt of text room is fine with no margins; any margin or
|
||||
/// gutter on it is refused.
|
||||
#[test]
|
||||
fn page_setup_section_allows_a_tiny_page_without_margins() {
|
||||
let tiny = |m: f32, g: f32| {
|
||||
with_section(|v| {
|
||||
v["pageW"] = json!(7.2);
|
||||
v["pageH"] = json!(20.0);
|
||||
for k in ["marginTop", "marginBottom", "marginLeft", "marginRight"] {
|
||||
v[k] = json!(m);
|
||||
}
|
||||
v["gutter"] = json!(g);
|
||||
})
|
||||
};
|
||||
let mut s = doc();
|
||||
let before = sect(&s);
|
||||
assert!(s.run("layout.pageSetup", &tiny(1.0, 0.0)).is_err(), "a margin on a tiny page");
|
||||
assert!(s.run("layout.pageSetup", &tiny(0.0, 1.0)).is_err(), "a gutter on a tiny page");
|
||||
assert_eq!(sect(&s), before);
|
||||
s.run("layout.pageSetup", &tiny(0.0, 0.0)).unwrap();
|
||||
assert_eq!((sect(&s).page_w, sect(&s).page_h), (7.2, 20.0));
|
||||
}
|
||||
|
||||
/// Layout › Text Direction turns the section, or the table cell the caret is in; one undo step.
|
||||
#[test]
|
||||
fn text_direction_turns_the_section_or_the_cell() {
|
||||
use wordcraft_doc::props::TextDirection;
|
||||
let mut s = doc();
|
||||
s.run("layout.textDirection", &json!({"value": "down"})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
assert_eq!(text_direction_at(&s), TextDirection::Down);
|
||||
// Without a value: the next one, like Word's button.
|
||||
s.run("layout.textDirection", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Up);
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
assert!(s.run("layout.textDirection", &json!({"value": "diagonal"})).is_err());
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
// In a table cell it turns the cell, not the section.
|
||||
s.run("insert.table", &json!({"rows": 1, "cols": 2})).unwrap();
|
||||
assert!(s.sel.focus.path.cell().is_some());
|
||||
s.run("layout.textDirection", &json!({"value": "btLr"})).unwrap();
|
||||
assert_eq!(text_direction_at(&s), TextDirection::Up);
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
}
|
||||
|
||||
/// #1206: each Margins and Columns preset is recognised after it's applied (Mirrored and
|
||||
/// Office 2003 share three margins; mirror margins tell them apart), and a value no preset
|
||||
/// gives is none of them.
|
||||
#[test]
|
||||
fn page_setup_presets_are_recognised() {
|
||||
let mut s = doc();
|
||||
for (id, _) in MARGIN_PRESETS {
|
||||
s.run("layout.margins", &json!({"preset": id})).unwrap();
|
||||
assert_eq!(margins_preset_at(&s), Some(id));
|
||||
}
|
||||
s.run("layout.margins", &json!({"left": 80.0})).unwrap();
|
||||
assert_eq!(margins_preset_at(&s), None);
|
||||
for (params, id) in [
|
||||
(json!({"count": 2}), "two"),
|
||||
(json!({"preset": "left"}), "left"),
|
||||
(json!({"count": 3}), "three"),
|
||||
(json!({"preset": "right"}), "right"),
|
||||
(json!({"count": 1}), "one"),
|
||||
] {
|
||||
s.run("layout.columns", ¶ms).unwrap();
|
||||
assert_eq!(columns_preset_at(&s), Some(id), "{params}");
|
||||
}
|
||||
s.run("layout.columns", &json!({"count": 4})).unwrap();
|
||||
assert_eq!(columns_preset_at(&s), None);
|
||||
}
|
||||
|
||||
/// #852: a continuous section break with text selected goes before the selection and keeps
|
||||
/// it, as in Word; it used to delete the text, so the first section came out empty. Saved and
|
||||
/// opened again, both sections' text lays out on page 1, the second below the first.
|
||||
#[test]
|
||||
fn section_break_keeps_the_selected_text() {
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo"));
|
||||
s.run("select.text", &json!({"text": "one"})).unwrap();
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
assert_eq!(s.selected_text(), "one");
|
||||
assert_eq!(s.doc.sections().len(), 2);
|
||||
let texts = |d: &wordcraft_doc::Document| -> Vec<String> {
|
||||
d.body.iter().filter_map(|b| if let Block::Para(p) = &**b { Some(p.plain_text().to_string()) } else { None }).collect()
|
||||
};
|
||||
assert_eq!(texts(&s.doc), ["", "one", "two"]);
|
||||
// A backward selection in the middle of a paragraph keeps its text and direction too.
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("alpha beta\ngamma"));
|
||||
let p = |off| Pos { story: StoryRef::Body, path: wordcraft_doc::Path::top(0), off };
|
||||
s.sel = Selection { anchor: p(10), focus: p(6) };
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
assert_eq!(s.selected_text(), "beta");
|
||||
assert!(s.sel.anchor > s.sel.focus);
|
||||
assert_eq!(texts(&s.doc), ["alpha ", "beta", "gamma"]);
|
||||
|
||||
// The issue's document: "one", a continuous break, "two" — both on page 1 after a round trip.
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo"));
|
||||
s.run("select.text", &json!({"text": "one"})).unwrap();
|
||||
s.run("caret.end", &json!({})).unwrap();
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap();
|
||||
assert_eq!(back.sections().len(), 2);
|
||||
assert_eq!(back.last_section.start, SectionStart::Continuous);
|
||||
let mut s = Session::new(back);
|
||||
let l = s.layout();
|
||||
assert_eq!(l.pages.len(), 1);
|
||||
let mut lines: Vec<(String, f32)> = l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
crate::layout::Placed::Lines { path, y, .. } => {
|
||||
let Some(Block::Para(p)) = s.doc.body.get(path.last()).map(|b| &**b) else { return None };
|
||||
(!p.plain_text().is_empty()).then(|| (p.plain_text().to_string(), *y))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
lines.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let order: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
|
||||
assert_eq!(order, ["one", "two"]);
|
||||
}
|
||||
|
||||
/// More Columns: unequal widths with a line between, as one undo step.
|
||||
#[test]
|
||||
fn unequal_columns_with_a_line_between() {
|
||||
let mut s = doc();
|
||||
let tw = sect(&s).text_width();
|
||||
s.run("layout.columns", &json!({"widths": [[150.0, 24.0], [tw - 174.0, 0.0]], "separator": true})).unwrap();
|
||||
let c = sect(&s).columns;
|
||||
assert_eq!((c.count, c.separator, c.widths.len()), (2, true, 2));
|
||||
assert_eq!(c.widths.first(), Some(&(150.0, 24.0)));
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).columns, Columns::default());
|
||||
// Too wide, too narrow, too many, or nonsense: refused.
|
||||
for bad in [json!([[tw, 36.0], [100.0, 0.0]]), json!([[5.0, 0.0], [100.0, 0.0]]), json!(vec![[40.0, 0.0]; 13]), json!("x")] {
|
||||
assert!(s.run("layout.columns", &json!({"widths": bad})).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply to: the whole document, or from the caret on (a continuous section break first).
|
||||
#[test]
|
||||
fn columns_apply_to_the_document_or_from_the_caret_on() {
|
||||
let mut s = doc();
|
||||
s.run("layout.break", &json!({"kind": "nextPage"})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 3, "apply": "document"})).unwrap();
|
||||
assert!(s.doc.sections().iter().all(|(_, x)| x.columns.count == 3));
|
||||
let mut s = doc();
|
||||
s.run("caret.docEnd", &json!({})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 2, "apply": "forward"})).unwrap();
|
||||
let counts: Vec<u32> = s.doc.sections().iter().map(|(_, x)| x.columns.count).collect();
|
||||
assert_eq!(counts, vec![1, 2]);
|
||||
assert_eq!(s.doc.last_section.start, SectionStart::Continuous);
|
||||
assert!(s.run("layout.columns", &json!({"count": 2, "apply": "elsewhere"})).is_err());
|
||||
}
|
||||
|
||||
/// Each section's paragraphs (a section break's own paragraph shows as "") and column count.
|
||||
fn sections_of(s: &Session) -> Vec<(Vec<String>, u32)> {
|
||||
let mut from = 0;
|
||||
let mut out = Vec::new();
|
||||
for (end, x) in s.doc.sections() {
|
||||
let paras = (from..=end).map(|i| s.doc.para_at(&Pos::body(i, 0)).map(|p| p.text.clone()).unwrap_or_default()).collect();
|
||||
out.push((paras, x.columns.count));
|
||||
from = end + 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, from: (usize, usize), to: (usize, usize)) {
|
||||
s.sel = Selection { anchor: Pos::body(from.0, from.1), focus: Pos::body(to.0, to.1) };
|
||||
}
|
||||
|
||||
fn strs(v: &[&str]) -> Vec<String> {
|
||||
v.iter().map(|x| x.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Columns on selected text (#2024), as observed in Word: a continuous section break before
|
||||
/// and after the selection, the columns on the section between, the rest left alone.
|
||||
#[test]
|
||||
fn columns_apply_to_the_selected_text() {
|
||||
let five = || Session::new(wordcraft_doc::Document::from_text("p1\np2\nalpha beta gamma\np4\np5"));
|
||||
// A whole paragraph (a triple-click takes its mark): each break gets a paragraph of its own.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 0), (3, 0));
|
||||
let whole = s.selected_text();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", ""]), 1), (strs(&["alpha beta gamma", ""]), 2), (strs(&["p4", "p5"]), 1)]);
|
||||
assert!(s.doc.sections().iter().skip(1).all(|(_, x)| x.start == SectionStart::Continuous));
|
||||
assert_eq!(s.selected_text(), whole, "the paragraph stays selected");
|
||||
// The same paragraph again: its section changes, no new breaks.
|
||||
select(&mut s, (3, 0), (4, 0));
|
||||
s.run("layout.columns", &json!({"count": 3})).unwrap();
|
||||
assert_eq!(sections_of(&s).iter().map(|(_, n)| *n).collect::<Vec<_>>(), vec![1, 3, 1]);
|
||||
// Taking the section break along changes nothing more.
|
||||
select(&mut s, (3, 0), (5, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s).iter().map(|(_, n)| *n).collect::<Vec<_>>(), vec![1, 2, 1]);
|
||||
// One undo step takes the breaks and the columns away.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 0), (3, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", "p5"]), 1)]);
|
||||
|
||||
// Words inside a paragraph: the breaks split it there.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 10), (2, 6));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha "]), 1), (strs(&["beta"]), 2), (strs(&[" gamma", "p4", "p5"]), 1)]);
|
||||
assert_eq!(s.selected_text(), "beta");
|
||||
assert!(s.sel.anchor > s.sel.focus, "a backward selection stays backward");
|
||||
|
||||
// At the document's start no break goes before it, at its end none after.
|
||||
let mut s = five();
|
||||
select(&mut s, (0, 0), (1, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", ""]), 2), (strs(&["p2", "alpha beta gamma", "p4", "p5"]), 1)]);
|
||||
let mut s = five();
|
||||
select(&mut s, (4, 0), (4, 2));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", ""]), 1), (strs(&["p5"]), 2)]);
|
||||
// Select All: the whole document, no breaks.
|
||||
let mut s = five();
|
||||
s.run("select.all", &json!({})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", "p5"]), 2)]);
|
||||
|
||||
// Without a selection the caret's section changes; "selection" then needs one.
|
||||
let mut s = five();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s).len(), 1);
|
||||
assert!(s.run("layout.columns", &json!({"count": 2, "apply": "selection"})).is_err());
|
||||
}
|
||||
|
||||
/// #407 Line Numbers options: start, count by, distance and restart; options alone keep the
|
||||
/// restart mode, `none` turns numbering off.
|
||||
#[test]
|
||||
fn line_numbering_options() {
|
||||
use wordcraft_doc::section::LineNumberRestart;
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("One\nTwo"));
|
||||
s.run("layout.lineNumbers", &json!({"value": "restartSection", "start": 5, "countBy": 2, "distance": 18})).unwrap();
|
||||
let l = s.doc.last_section.line_numbers.clone().unwrap();
|
||||
assert_eq!((l.start, l.count_by, l.distance, l.restart), (5, 2, 18.0, LineNumberRestart::Section));
|
||||
s.run("layout.lineNumbers", &json!({"countBy": 1e9, "start": -3})).unwrap();
|
||||
let l = s.doc.last_section.line_numbers.clone().unwrap();
|
||||
assert_eq!((l.start, l.count_by, l.restart), (5, 100, LineNumberRestart::Section), "clamped, mode kept");
|
||||
s.run("layout.lineNumbers", &json!({"value": "none"})).unwrap();
|
||||
assert!(s.doc.last_section.line_numbers.is_none());
|
||||
assert!(s.run("layout.lineNumbers", &json!({"value": "sometimes"})).is_err());
|
||||
}
|
||||
|
||||
/// #407 Hyphenation Options: options don't toggle automatic hyphenation; Manual finds a
|
||||
/// word at a line start that would fit hyphenated on the line above, and an optional hyphen
|
||||
/// there breaks the word.
|
||||
#[test]
|
||||
fn hyphenation_options_and_manual_hyphenation() {
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text(
|
||||
&"a extraordinarily be uncharacteristic of responsibilities the considerations ".repeat(12),
|
||||
));
|
||||
let r = s.run("layout.hyphenation", &json!({"zone": 36, "caps": false, "limit": 3})).unwrap();
|
||||
assert_eq!(r, json!({"value": false, "zone": 36.0, "caps": false, "limit": 3}));
|
||||
s.run("layout.hyphenation", &json!({})).unwrap();
|
||||
assert!(s.doc.settings.auto_hyphenation, "nothing given toggles");
|
||||
s.run("layout.hyphenation", &json!({"value": false})).unwrap();
|
||||
// Accept every candidate (which words they are depends on the fonts).
|
||||
let mut found = 0;
|
||||
let mut from = json!(null);
|
||||
while found < 5 {
|
||||
let c = s.run("layout.manualHyphenation", &json!({"from": from})).unwrap();
|
||||
if c["done"] == true {
|
||||
break;
|
||||
}
|
||||
let word = c["word"].as_str().unwrap().to_string();
|
||||
let pts = c["points"].as_array().unwrap();
|
||||
assert!(!pts.is_empty() && pts.contains(&c["suggest"]), "{c}");
|
||||
let at = c["suggest"].as_u64().unwrap() as usize;
|
||||
let mut pos = c["pos"].clone();
|
||||
let off = pos["off"].as_u64().unwrap() as usize + word.char_indices().nth(at).unwrap().0;
|
||||
pos["off"] = json!(off);
|
||||
s.run("caret.set", &json!({"pos": pos})).unwrap();
|
||||
s.run("text.optionalHyphen", &json!({})).unwrap();
|
||||
pos["off"] = json!(off + word.len());
|
||||
from = pos;
|
||||
found += 1;
|
||||
}
|
||||
// Some line of thirty or so starts with a long word that fits hyphenated above, whatever the font.
|
||||
assert!(found > 0, "no candidates");
|
||||
assert_eq!(s.doc.plain_text(wordcraft_doc::StoryRef::Body).matches('\u{ad}').count(), found);
|
||||
assert!(s.run("layout.manualHyphenation", &json!({"from": {"block": 999, "off": 0}})).unwrap()["done"] == true);
|
||||
}
|
||||
|
||||
/// `layout.pageNumberFormat` sets the section's number format and start; a call without
|
||||
/// `start` keeps it (the ribbon's old plain call cleared it), and a null `start` numbers on
|
||||
/// from the section before — the Format Page Numbers dialog's "Continue from previous
|
||||
/// section" (#1113).
|
||||
#[test]
|
||||
fn page_number_format_keeps_or_clears_the_start() {
|
||||
use wordcraft_doc::section::NumFormat;
|
||||
let mut s = doc();
|
||||
s.run("layout.pageNumberFormat", &json!({"format": "upperRoman", "start": 3})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::UpperRoman, Some(3)));
|
||||
s.run("layout.pageNumberFormat", &json!({"format": "lowerLetter"})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::LowerLetter, Some(3)), "a format change keeps the start");
|
||||
s.run("layout.pageNumberFormat", &json!({"start": null})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::LowerLetter, None), "continue numbers on from the section before");
|
||||
// The result carries the section back (scripts read it instead of the dialog).
|
||||
let back = s.run("layout.pageNumberFormat", &json!({"format": "decimal", "start": 1})).unwrap();
|
||||
assert_eq!(back["pageNumFormat"], "decimal");
|
||||
assert_eq!(back["pageNumStart"], 1);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
use super::*;
|
||||
|
||||
fn doc() -> Session {
|
||||
Session::new(wordcraft_doc::Document::from_text("one\ntwo\nthree"))
|
||||
}
|
||||
|
||||
fn with_section(f: impl Fn(&mut Value)) -> Value {
|
||||
let mut v = serde_json::to_value(SectionProps::default()).unwrap();
|
||||
f(&mut v);
|
||||
json!({"section": v})
|
||||
}
|
||||
|
||||
/// `layout.pageSetup {section}` checks the values like the dedicated commands do.
|
||||
#[test]
|
||||
fn page_setup_section_is_validated() {
|
||||
let mut s = doc();
|
||||
let before = sect(&s);
|
||||
// Refused with an error, nothing changes: non-finite (overflowing), margins that leave no text room.
|
||||
for (what, p) in [
|
||||
("inf width", with_section(|v| v["pageW"] = json!(1e300))),
|
||||
("inf margin", with_section(|v| v["marginTop"] = json!(1e300))),
|
||||
("inf header", with_section(|v| v["header"] = json!(1e300))),
|
||||
("inf column space", with_section(|v| v["columns"]["space"] = json!(1e300))),
|
||||
("inf column width", with_section(|v| v["columns"]["widths"] = json!([[1e300, 0.0], [100.0, 0.0]]))),
|
||||
("inf line number distance", with_section(|v| v["lineNumbers"] = json!({"distance": 1e300}))),
|
||||
("inf grid pitch", with_section(|v| v["docGrid"] = json!({"type": "lines", "linePitch": 1e300, "charSpace": 0}))),
|
||||
("inf border space", with_section(|v| v["pageBorders"] = json!({"top": {"style": "single", "width": 1.0, "space": 1e300}}))),
|
||||
("inf border width", with_section(|v| v["pageBorders"] = json!({"top": {"style": "single", "width": 1e300, "space": 1.0}}))),
|
||||
("margins > page", with_section(|v| v["marginLeft"] = json!(5000.0))),
|
||||
("margins eat the height", with_section(|v| v["marginTop"] = json!(700.0))),
|
||||
("gutter eats the width", with_section(|v| v["gutter"] = json!(600.0))),
|
||||
("negative page", with_section(|v| v["pageW"] = json!(-5.0))),
|
||||
("zero page height", with_section(|v| v["pageH"] = json!(0.0))),
|
||||
] {
|
||||
assert!(s.run("layout.pageSetup", &p).is_err(), "{what}");
|
||||
assert_eq!(sect(&s), before, "{what}");
|
||||
}
|
||||
// Clamped.
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(100.0);
|
||||
v["pageH"] = json!(100.0);
|
||||
v["marginTop"] = json!(-10.0);
|
||||
v["marginBottom"] = json!(0.0);
|
||||
v["marginLeft"] = json!(0.0);
|
||||
v["marginRight"] = json!(0.0);
|
||||
v["header"] = json!(-3.0);
|
||||
v["footer"] = json!(1e6);
|
||||
v["columns"]["count"] = json!(1000);
|
||||
v["columns"]["space"] = json!(-4.0);
|
||||
v["lineNumbers"] = json!({"countBy": 0, "start": 0, "distance": 1e6, "restart": "page"});
|
||||
v["docGrid"] = json!({"type": "lines", "linePitch": 1e9, "charSpace": 2_000_000_000});
|
||||
v["pageNumStart"] = json!(4_000_000_000u64);
|
||||
v["footnotePr"] = json!({"numStart": 4_000_000_000u64});
|
||||
v["endnotePr"] = json!({"numStart": 0});
|
||||
v["pageBorders"] =
|
||||
json!({"top": {"style": "single", "width": 1e6, "space": -5.0}, "left": {"style": "single", "width": 0.0, "space": 1e6}});
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_w, x.page_h), (100.0, 100.0));
|
||||
assert_eq!(x.margin_top, 0.0);
|
||||
assert_eq!(x.header, 0.0);
|
||||
assert_eq!(x.footer, x.page_h, "a footer distance stays on the page");
|
||||
assert_eq!(x.columns.count, 12);
|
||||
assert_eq!(x.columns.space, 0.0);
|
||||
let l = x.line_numbers.unwrap();
|
||||
assert_eq!((l.count_by, l.start, l.distance), (1, 1, 1584.0));
|
||||
let g = x.doc_grid.unwrap();
|
||||
assert_eq!((g.line_pitch, g.char_space), (wordcraft_doc::section::MAX_GRID_PITCH, wordcraft_doc::section::MAX_CHAR_SPACE));
|
||||
assert_eq!(x.page_num_start, Some(100_000));
|
||||
assert_eq!(x.footnote_pr.num_start, Some(wordcraft_doc::section::MAX_NOTE_START));
|
||||
assert_eq!(x.endnote_pr.num_start, Some(1));
|
||||
let b = x.page_borders.unwrap();
|
||||
let (t, lf) = (b.top.unwrap(), b.left.unwrap());
|
||||
assert_eq!((t.width, t.space), (6.0, 0.0));
|
||||
assert_eq!((lf.width, lf.space), (0.25, 31.0));
|
||||
// Huge pages clamp; columns and widths agree; orientation follows the sides.
|
||||
let mut s = doc();
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(1e5);
|
||||
v["pageH"] = json!(792.0);
|
||||
v["landscape"] = json!(false);
|
||||
v["columns"]["count"] = json!(3);
|
||||
v["columns"]["widths"] = json!(vec![json!([1.0, 1e5]); 40]);
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!(x.page_w, 1584.0);
|
||||
assert!(x.landscape, "wider than tall is landscape");
|
||||
assert_eq!(x.columns.widths.len(), 12);
|
||||
assert_eq!(x.columns.count, 12, "the count follows the widths");
|
||||
assert!(x.columns.widths.iter().all(|(w, sp)| *w >= 18.0 && (0.0..=300.0).contains(sp)));
|
||||
// One width is no custom layout: equal columns.
|
||||
s.run("layout.pageSetup", &with_section(|v| v["columns"]["widths"] = json!([[200.0, 0.0]]))).unwrap();
|
||||
assert!(sect(&s).columns.widths.is_empty());
|
||||
}
|
||||
|
||||
/// A valid section still applies as given.
|
||||
#[test]
|
||||
fn page_setup_section_applies_a_valid_one() {
|
||||
let mut s = doc();
|
||||
s.run(
|
||||
"layout.pageSetup",
|
||||
&with_section(|v| {
|
||||
v["pageW"] = json!(595.0);
|
||||
v["pageH"] = json!(842.0);
|
||||
v["marginLeft"] = json!(50.0);
|
||||
v["columns"]["count"] = json!(2);
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_w, x.page_h, x.margin_left, x.columns.count), (595.0, 842.0, 50.0, 2));
|
||||
}
|
||||
|
||||
/// A page smaller than the usual 36 pt of text room is fine with no margins; any margin or
|
||||
/// gutter on it is refused.
|
||||
#[test]
|
||||
fn page_setup_section_allows_a_tiny_page_without_margins() {
|
||||
let tiny = |m: f32, g: f32| {
|
||||
with_section(|v| {
|
||||
v["pageW"] = json!(7.2);
|
||||
v["pageH"] = json!(20.0);
|
||||
for k in ["marginTop", "marginBottom", "marginLeft", "marginRight"] {
|
||||
v[k] = json!(m);
|
||||
}
|
||||
v["gutter"] = json!(g);
|
||||
})
|
||||
};
|
||||
let mut s = doc();
|
||||
let before = sect(&s);
|
||||
assert!(s.run("layout.pageSetup", &tiny(1.0, 0.0)).is_err(), "a margin on a tiny page");
|
||||
assert!(s.run("layout.pageSetup", &tiny(0.0, 1.0)).is_err(), "a gutter on a tiny page");
|
||||
assert_eq!(sect(&s), before);
|
||||
s.run("layout.pageSetup", &tiny(0.0, 0.0)).unwrap();
|
||||
assert_eq!((sect(&s).page_w, sect(&s).page_h), (7.2, 20.0));
|
||||
}
|
||||
|
||||
/// Layout › Text Direction turns the section, or the table cell the caret is in; one undo step.
|
||||
#[test]
|
||||
fn text_direction_turns_the_section_or_the_cell() {
|
||||
use wordcraft_doc::props::TextDirection;
|
||||
let mut s = doc();
|
||||
s.run("layout.textDirection", &json!({"value": "down"})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
assert_eq!(text_direction_at(&s), TextDirection::Down);
|
||||
// Without a value: the next one, like Word's button.
|
||||
s.run("layout.textDirection", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Up);
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
assert!(s.run("layout.textDirection", &json!({"value": "diagonal"})).is_err());
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
// In a table cell it turns the cell, not the section.
|
||||
s.run("insert.table", &json!({"rows": 1, "cols": 2})).unwrap();
|
||||
assert!(s.sel.focus.path.cell().is_some());
|
||||
s.run("layout.textDirection", &json!({"value": "btLr"})).unwrap();
|
||||
assert_eq!(text_direction_at(&s), TextDirection::Up);
|
||||
assert_eq!(sect(&s).text_direction, TextDirection::Down);
|
||||
}
|
||||
|
||||
/// #1206: each Margins and Columns preset is recognised after it's applied (Mirrored and
|
||||
/// Office 2003 share three margins; mirror margins tell them apart), and a value no preset
|
||||
/// gives is none of them.
|
||||
#[test]
|
||||
fn page_setup_presets_are_recognised() {
|
||||
let mut s = doc();
|
||||
for (id, _) in MARGIN_PRESETS {
|
||||
s.run("layout.margins", &json!({"preset": id})).unwrap();
|
||||
assert_eq!(margins_preset_at(&s), Some(id));
|
||||
}
|
||||
s.run("layout.margins", &json!({"left": 80.0})).unwrap();
|
||||
assert_eq!(margins_preset_at(&s), None);
|
||||
for (params, id) in [
|
||||
(json!({"count": 2}), "two"),
|
||||
(json!({"preset": "left"}), "left"),
|
||||
(json!({"count": 3}), "three"),
|
||||
(json!({"preset": "right"}), "right"),
|
||||
(json!({"count": 1}), "one"),
|
||||
] {
|
||||
s.run("layout.columns", ¶ms).unwrap();
|
||||
assert_eq!(columns_preset_at(&s), Some(id), "{params}");
|
||||
}
|
||||
s.run("layout.columns", &json!({"count": 4})).unwrap();
|
||||
assert_eq!(columns_preset_at(&s), None);
|
||||
}
|
||||
|
||||
/// #852: a continuous section break with text selected goes before the selection and keeps
|
||||
/// it, as in Word; it used to delete the text, so the first section came out empty. Saved and
|
||||
/// opened again, both sections' text lays out on page 1, the second below the first.
|
||||
#[test]
|
||||
fn section_break_keeps_the_selected_text() {
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo"));
|
||||
s.run("select.text", &json!({"text": "one"})).unwrap();
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
assert_eq!(s.selected_text(), "one");
|
||||
assert_eq!(s.doc.sections().len(), 2);
|
||||
let texts = |d: &wordcraft_doc::Document| -> Vec<String> {
|
||||
d.body.iter().filter_map(|b| if let Block::Para(p) = &**b { Some(p.plain_text().to_string()) } else { None }).collect()
|
||||
};
|
||||
assert_eq!(texts(&s.doc), ["", "one", "two"]);
|
||||
// A backward selection in the middle of a paragraph keeps its text and direction too.
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("alpha beta\ngamma"));
|
||||
let p = |off| Pos { story: StoryRef::Body, path: wordcraft_doc::Path::top(0), off };
|
||||
s.sel = Selection { anchor: p(10), focus: p(6) };
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
assert_eq!(s.selected_text(), "beta");
|
||||
assert!(s.sel.anchor > s.sel.focus);
|
||||
assert_eq!(texts(&s.doc), ["alpha ", "beta", "gamma"]);
|
||||
|
||||
// The issue's document: "one", a continuous break, "two" — both on page 1 after a round trip.
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo"));
|
||||
s.run("select.text", &json!({"text": "one"})).unwrap();
|
||||
s.run("caret.end", &json!({})).unwrap();
|
||||
s.run("layout.break", &json!({"kind": "continuous"})).unwrap();
|
||||
let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap();
|
||||
assert_eq!(back.sections().len(), 2);
|
||||
assert_eq!(back.last_section.start, SectionStart::Continuous);
|
||||
let mut s = Session::new(back);
|
||||
let l = s.layout();
|
||||
assert_eq!(l.pages.len(), 1);
|
||||
let mut lines: Vec<(String, f32)> = l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
crate::layout::Placed::Lines { path, y, .. } => {
|
||||
let Some(Block::Para(p)) = s.doc.body.get(path.last()).map(|b| &**b) else { return None };
|
||||
(!p.plain_text().is_empty()).then(|| (p.plain_text().to_string(), *y))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
lines.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let order: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
|
||||
assert_eq!(order, ["one", "two"]);
|
||||
}
|
||||
|
||||
/// More Columns: unequal widths with a line between, as one undo step.
|
||||
#[test]
|
||||
fn unequal_columns_with_a_line_between() {
|
||||
let mut s = doc();
|
||||
let tw = sect(&s).text_width();
|
||||
s.run("layout.columns", &json!({"widths": [[150.0, 24.0], [tw - 174.0, 0.0]], "separator": true})).unwrap();
|
||||
let c = sect(&s).columns;
|
||||
assert_eq!((c.count, c.separator, c.widths.len()), (2, true, 2));
|
||||
assert_eq!(c.widths.first(), Some(&(150.0, 24.0)));
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sect(&s).columns, Columns::default());
|
||||
// Too wide, too narrow, too many, or nonsense: refused.
|
||||
for bad in [json!([[tw, 36.0], [100.0, 0.0]]), json!([[5.0, 0.0], [100.0, 0.0]]), json!(vec![[40.0, 0.0]; 13]), json!("x")] {
|
||||
assert!(s.run("layout.columns", &json!({"widths": bad})).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply to: the whole document, or from the caret on (a continuous section break first).
|
||||
#[test]
|
||||
fn columns_apply_to_the_document_or_from_the_caret_on() {
|
||||
let mut s = doc();
|
||||
s.run("layout.break", &json!({"kind": "nextPage"})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 3, "apply": "document"})).unwrap();
|
||||
assert!(s.doc.sections().iter().all(|(_, x)| x.columns.count == 3));
|
||||
let mut s = doc();
|
||||
s.run("caret.docEnd", &json!({})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 2, "apply": "forward"})).unwrap();
|
||||
let counts: Vec<u32> = s.doc.sections().iter().map(|(_, x)| x.columns.count).collect();
|
||||
assert_eq!(counts, vec![1, 2]);
|
||||
assert_eq!(s.doc.last_section.start, SectionStart::Continuous);
|
||||
assert!(s.run("layout.columns", &json!({"count": 2, "apply": "elsewhere"})).is_err());
|
||||
}
|
||||
|
||||
/// Each section's paragraphs (a section break's own paragraph shows as "") and column count.
|
||||
fn sections_of(s: &Session) -> Vec<(Vec<String>, u32)> {
|
||||
let mut from = 0;
|
||||
let mut out = Vec::new();
|
||||
for (end, x) in s.doc.sections() {
|
||||
let paras = (from..=end).map(|i| s.doc.para_at(&Pos::body(i, 0)).map(|p| p.text.clone()).unwrap_or_default()).collect();
|
||||
out.push((paras, x.columns.count));
|
||||
from = end + 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, from: (usize, usize), to: (usize, usize)) {
|
||||
s.sel = Selection { anchor: Pos::body(from.0, from.1), focus: Pos::body(to.0, to.1) };
|
||||
}
|
||||
|
||||
fn strs(v: &[&str]) -> Vec<String> {
|
||||
v.iter().map(|x| x.to_string()).collect()
|
||||
}
|
||||
|
||||
/// Columns on selected text (#2024), as observed in Word: a continuous section break before
|
||||
/// and after the selection, the columns on the section between, the rest left alone.
|
||||
#[test]
|
||||
fn columns_apply_to_the_selected_text() {
|
||||
let five = || Session::new(wordcraft_doc::Document::from_text("p1\np2\nalpha beta gamma\np4\np5"));
|
||||
// A whole paragraph (a triple-click takes its mark): each break gets a paragraph of its own.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 0), (3, 0));
|
||||
let whole = s.selected_text();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", ""]), 1), (strs(&["alpha beta gamma", ""]), 2), (strs(&["p4", "p5"]), 1)]);
|
||||
assert!(s.doc.sections().iter().skip(1).all(|(_, x)| x.start == SectionStart::Continuous));
|
||||
assert_eq!(s.selected_text(), whole, "the paragraph stays selected");
|
||||
// The same paragraph again: its section changes, no new breaks.
|
||||
select(&mut s, (3, 0), (4, 0));
|
||||
s.run("layout.columns", &json!({"count": 3})).unwrap();
|
||||
assert_eq!(sections_of(&s).iter().map(|(_, n)| *n).collect::<Vec<_>>(), vec![1, 3, 1]);
|
||||
// Taking the section break along changes nothing more.
|
||||
select(&mut s, (3, 0), (5, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s).iter().map(|(_, n)| *n).collect::<Vec<_>>(), vec![1, 2, 1]);
|
||||
// One undo step takes the breaks and the columns away.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 0), (3, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", "p5"]), 1)]);
|
||||
|
||||
// Words inside a paragraph: the breaks split it there.
|
||||
let mut s = five();
|
||||
select(&mut s, (2, 10), (2, 6));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha "]), 1), (strs(&["beta"]), 2), (strs(&[" gamma", "p4", "p5"]), 1)]);
|
||||
assert_eq!(s.selected_text(), "beta");
|
||||
assert!(s.sel.anchor > s.sel.focus, "a backward selection stays backward");
|
||||
|
||||
// At the document's start no break goes before it, at its end none after.
|
||||
let mut s = five();
|
||||
select(&mut s, (0, 0), (1, 0));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", ""]), 2), (strs(&["p2", "alpha beta gamma", "p4", "p5"]), 1)]);
|
||||
let mut s = five();
|
||||
select(&mut s, (4, 0), (4, 2));
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", ""]), 1), (strs(&["p5"]), 2)]);
|
||||
// Select All: the whole document, no breaks.
|
||||
let mut s = five();
|
||||
s.run("select.all", &json!({})).unwrap();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s), vec![(strs(&["p1", "p2", "alpha beta gamma", "p4", "p5"]), 2)]);
|
||||
|
||||
// Without a selection the caret's section changes; "selection" then needs one.
|
||||
let mut s = five();
|
||||
s.run("layout.columns", &json!({"count": 2})).unwrap();
|
||||
assert_eq!(sections_of(&s).len(), 1);
|
||||
assert!(s.run("layout.columns", &json!({"count": 2, "apply": "selection"})).is_err());
|
||||
}
|
||||
|
||||
/// #407 Line Numbers options: start, count by, distance and restart; options alone keep the
|
||||
/// restart mode, `none` turns numbering off.
|
||||
#[test]
|
||||
fn line_numbering_options() {
|
||||
use wordcraft_doc::section::LineNumberRestart;
|
||||
let mut s = Session::new(wordcraft_doc::Document::from_text("One\nTwo"));
|
||||
s.run("layout.lineNumbers", &json!({"value": "restartSection", "start": 5, "countBy": 2, "distance": 18})).unwrap();
|
||||
let l = s.doc.last_section.line_numbers.clone().unwrap();
|
||||
assert_eq!((l.start, l.count_by, l.distance, l.restart), (5, 2, 18.0, LineNumberRestart::Section));
|
||||
s.run("layout.lineNumbers", &json!({"countBy": 1e9, "start": -3})).unwrap();
|
||||
let l = s.doc.last_section.line_numbers.clone().unwrap();
|
||||
assert_eq!((l.start, l.count_by, l.restart), (5, 100, LineNumberRestart::Section), "clamped, mode kept");
|
||||
s.run("layout.lineNumbers", &json!({"value": "none"})).unwrap();
|
||||
assert!(s.doc.last_section.line_numbers.is_none());
|
||||
assert!(s.run("layout.lineNumbers", &json!({"value": "sometimes"})).is_err());
|
||||
}
|
||||
|
||||
/// #407 Hyphenation Options: options don't toggle automatic hyphenation; Manual finds a
|
||||
/// word at a line start that would fit hyphenated on the line above, and an optional hyphen
|
||||
/// there breaks the word.
|
||||
#[test]
|
||||
fn hyphenation_options_and_manual_hyphenation() {
|
||||
let mut s =
|
||||
Session::new(wordcraft_doc::Document::from_text(&"a extraordinarily be uncharacteristic of responsibilities the considerations ".repeat(12)));
|
||||
let r = s.run("layout.hyphenation", &json!({"zone": 36, "caps": false, "limit": 3})).unwrap();
|
||||
assert_eq!(r, json!({"value": false, "zone": 36.0, "caps": false, "limit": 3}));
|
||||
s.run("layout.hyphenation", &json!({})).unwrap();
|
||||
assert!(s.doc.settings.auto_hyphenation, "nothing given toggles");
|
||||
s.run("layout.hyphenation", &json!({"value": false})).unwrap();
|
||||
// Accept every candidate (which words they are depends on the fonts).
|
||||
let mut found = 0;
|
||||
let mut from = json!(null);
|
||||
while found < 5 {
|
||||
let c = s.run("layout.manualHyphenation", &json!({"from": from})).unwrap();
|
||||
if c["done"] == true {
|
||||
break;
|
||||
}
|
||||
let word = c["word"].as_str().unwrap().to_string();
|
||||
let pts = c["points"].as_array().unwrap();
|
||||
assert!(!pts.is_empty() && pts.contains(&c["suggest"]), "{c}");
|
||||
let at = c["suggest"].as_u64().unwrap() as usize;
|
||||
let mut pos = c["pos"].clone();
|
||||
let off = pos["off"].as_u64().unwrap() as usize + word.char_indices().nth(at).unwrap().0;
|
||||
pos["off"] = json!(off);
|
||||
s.run("caret.set", &json!({"pos": pos})).unwrap();
|
||||
s.run("text.optionalHyphen", &json!({})).unwrap();
|
||||
pos["off"] = json!(off + word.len());
|
||||
from = pos;
|
||||
found += 1;
|
||||
}
|
||||
// Some line of thirty or so starts with a long word that fits hyphenated above, whatever the font.
|
||||
assert!(found > 0, "no candidates");
|
||||
assert_eq!(s.doc.plain_text(wordcraft_doc::StoryRef::Body).matches('\u{ad}').count(), found);
|
||||
assert!(s.run("layout.manualHyphenation", &json!({"from": {"block": 999, "off": 0}})).unwrap()["done"] == true);
|
||||
}
|
||||
|
||||
/// `layout.pageNumberFormat` sets the section's number format and start; a call without
|
||||
/// `start` keeps it (the ribbon's old plain call cleared it), and a null `start` numbers on
|
||||
/// from the section before — the Format Page Numbers dialog's "Continue from previous
|
||||
/// section" (#1113).
|
||||
#[test]
|
||||
fn page_number_format_keeps_or_clears_the_start() {
|
||||
use wordcraft_doc::section::NumFormat;
|
||||
let mut s = doc();
|
||||
s.run("layout.pageNumberFormat", &json!({"format": "upperRoman", "start": 3})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::UpperRoman, Some(3)));
|
||||
s.run("layout.pageNumberFormat", &json!({"format": "lowerLetter"})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::LowerLetter, Some(3)), "a format change keeps the start");
|
||||
s.run("layout.pageNumberFormat", &json!({"start": null})).unwrap();
|
||||
let x = sect(&s);
|
||||
assert_eq!((x.page_num_format, x.page_num_start), (NumFormat::LowerLetter, None), "continue numbers on from the section before");
|
||||
// The result carries the section back (scripts read it instead of the dialog).
|
||||
let back = s.run("layout.pageNumberFormat", &json!({"format": "decimal", "start": 1})).unwrap();
|
||||
assert_eq!(back["pageNumFormat"], "decimal");
|
||||
assert_eq!(back["pageNumStart"], 1);
|
||||
}
|
||||
Reference in New Issue
Block a user