Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor 57957ef917 Group scaled-group reroute test geometry into tuples to satisfy the parameter limit 2026-10-26 10:10:53 -04:00
Dylan M. Taylor 277f352d06 Sonar: fix clippy lints that fail the gate; drop unread ScaledGroupSpec field
Collapse nested ifs into match guards, simplify negated boolean
expressions to their equivalent forms, and remove the ScaledGroupSpec
group field, which was written but never read.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-26 09:11:20 -04:00
Dylan M. Taylor fbea1b2f8f Sonar: split rerouting test cases and the caret-code parser
- engine cmd/find_pattern.rs parse_replace: caret_code handles ^ codes and
  flush_literal flushes pending text (both were inline in one loop).
- engine cmd/connectors.rs and doc connector.rs: each reroute test's innermost
  case moves into a helper; the assertions are unchanged.
2026-10-26 08:41:41 -04:00
13 changed files with 136 additions and 138 deletions
+38 -37
View File
@@ -362,6 +362,43 @@ mod tests {
assert!(near(corner_radius(Some(10_000), -5.0, 100.0), 0.0));
}
/// One rerouting case: the free end settles and the attached end keeps its site.
fn assert_transformed_reroute(kind: ShapeKind, rot: f32, flip_h: bool, flip_v: bool, free: Option<ConnEnd>, free_start: bool) {
let attached = Some(ConnEnd { id: 7, site: 3 });
let (start, end) = if free_start { (free, attached) } else { (attached, free) };
let mut obj = shape(kind, 40.0, 20.0, ShapeExtra { start, end, ..Default::default() });
let InlineObject::Shape { float, .. } = &mut obj else { panic!() };
float.rot = rot;
float.flip_h = flip_h;
float.flip_v = flip_v;
// Frame (10, 30, 40, 20), centre (30, 40): mirror, then rotate
// clockwise in y-down group space. Do not use the routing transform
// to compute the expectation.
let sign = if free_start { -1.0 } else { 1.0 };
let dx = sign * if flip_h { -20.0 } else { 20.0 };
let dy = sign * if flip_v { -10.0 } else { 10.0 };
let (sin, cos) = rot.to_radians().sin_cos();
let expected = (30.0 + dx * cos - dy * sin, 40.0 + dx * sin + dy * cos);
let kids = vec![
GroupChild { x: 100.0, y: 200.0, obj: shape(ShapeKind::Rectangle, 40.0, 20.0, ShapeExtra { id: 7, ..Default::default() }) },
GroupChild { x: 10.0, y: 30.0, obj },
];
let mut group = group(kids);
assert!(reroute_group(&mut group));
let InlineObject::Group { children, .. } = &group else { panic!() };
let c = &children[1];
let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!() };
let (start, end) = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v);
let (free, attached) = if free_start { (start, end) } else { (end, start) };
assert!(
(free.0 - expected.0).abs() < 0.001 && (free.1 - expected.1).abs() < 0.001,
"{kind:?}, {rot}, {flip_h}, {flip_v}, {free_start}: {free:?} != {expected:?}"
);
assert_eq!(attached, (140.0, 210.0));
assert_eq!(float.rot, 0.0);
assert!(!reroute_group(&mut group), "rerouting must settle");
}
#[test]
fn rerouting_preserves_transformed_free_and_unresolved_ends() {
for kind in [ShapeKind::StraightConnector, ShapeKind::ElbowConnector, ShapeKind::CurvedConnector] {
@@ -369,43 +406,7 @@ mod tests {
for (flip_h, flip_v) in [(false, false), (true, false), (false, true), (true, true)] {
for free in [None, Some(ConnEnd { id: 404, site: 0 }), Some(ConnEnd { id: 7, site: 99 })] {
for free_start in [false, true] {
let attached = Some(ConnEnd { id: 7, site: 3 });
let (start, end) = if free_start { (free, attached) } else { (attached, free) };
let mut obj = shape(kind, 40.0, 20.0, ShapeExtra { start, end, ..Default::default() });
let InlineObject::Shape { float, .. } = &mut obj else { panic!() };
float.rot = rot;
float.flip_h = flip_h;
float.flip_v = flip_v;
// Frame (10, 30, 40, 20), centre (30, 40): mirror, then rotate
// clockwise in y-down group space. Do not use the routing transform
// to compute the expectation.
let sign = if free_start { -1.0 } else { 1.0 };
let dx = sign * if flip_h { -20.0 } else { 20.0 };
let dy = sign * if flip_v { -10.0 } else { 10.0 };
let (sin, cos) = rot.to_radians().sin_cos();
let expected = (30.0 + dx * cos - dy * sin, 40.0 + dx * sin + dy * cos);
let kids = vec![
GroupChild {
x: 100.0,
y: 200.0,
obj: shape(ShapeKind::Rectangle, 40.0, 20.0, ShapeExtra { id: 7, ..Default::default() }),
},
GroupChild { x: 10.0, y: 30.0, obj },
];
let mut group = group(kids);
assert!(reroute_group(&mut group));
let InlineObject::Group { children, .. } = &group else { panic!() };
let c = &children[1];
let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!() };
let (start, end) = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v);
let (free, attached) = if free_start { (start, end) } else { (end, start) };
assert!(
(free.0 - expected.0).abs() < 0.001 && (free.1 - expected.1).abs() < 0.001,
"{kind:?}, {rot}, {flip_h}, {flip_v}, {free_start}: {free:?} != {expected:?}"
);
assert_eq!(attached, (140.0, 210.0));
assert_eq!(float.rot, 0.0);
assert!(!reroute_group(&mut group), "rerouting must settle");
assert_transformed_reroute(kind, rot, flip_h, flip_v, free, free_start);
}
}
}
+1 -1
View File
@@ -570,7 +570,7 @@ impl P {
}
out.pop()
};
if !matches!(c, '^' | '_') && !(matches!(c, '┬' | '┴') && self.no_limits == 0) && !is_combining(c) {
if !(matches!(c, '^' | '_') || matches!(c, '┬' | '┴') && self.no_limits == 0 || is_combining(c)) {
break;
}
// The base is already deep: the operator is text (see `too_deep`).
+6 -12
View File
@@ -377,21 +377,15 @@ impl Props {
}
0x0104 => self.pib = Some(op),
0x0181 => self.fill_color = color(op),
0x01BF => {
// fFilled (bit 4) is valid when fUsefFilled (bit 20) is set.
if op & (1 << 20) != 0 {
self.filled = Some(op & (1 << 4) != 0);
}
}
// fFilled (bit 4) is valid when fUsefFilled (bit 20) is set.
0x01BF if op & (1 << 20) != 0 => self.filled = Some(op & (1 << 4) != 0),
0x01BF => {}
0x01C0 => self.line_color = color(op),
0x01CB => self.line_width = Some(op),
0x01D0 | 0x01D1 => self.arrowhead |= op != 0,
0x01FF => {
// fLine (bit 3) is valid when fUsefLine (bit 19) is set.
if op & (1 << 19) != 0 {
self.line = Some(op & (1 << 3) != 0);
}
}
// fLine (bit 3) is valid when fUsefLine (bit 19) is set.
0x01FF if op & (1 << 19) != 0 => self.line = Some(op & (1 << 3) != 0),
0x01FF => {}
0x0384..=0x0387 => self.wrap_dist[(pid - 0x0384) as usize] = Some(op),
0x038F => self.posh = Some(op),
0x0390 => self.posrelh = Some(op),
+3 -4
View File
@@ -732,11 +732,10 @@ impl Reader<'_> {
};
self.emit_text(sc, pb, c, props);
}
"w:cr" => {
if !sc.fields.iter().any(|f| f.phase == Phase::Instr) {
self.emit_text(sc, pb, "\n", props);
}
"w:cr" if !sc.fields.iter().any(|f| f.phase == Phase::Instr) => {
self.emit_text(sc, pb, "\n", props);
}
"w:cr" => {}
"w:noBreakHyphen" => self.emit_text(sc, pb, "\u{2011}", props),
"w:softHyphen" => self.emit_text(sc, pb, "\u{00AD}", props),
"w:sym" => {
+46 -47
View File
@@ -327,6 +327,51 @@ mod tests {
(turn((x, y)), turn((x + w, y + h)))
}
/// One scaled-group case: rerouting keeps the visual free end and the resolved sites.
fn assert_scaled_group_reroute(size: (f32, f32), kind: ShapeKind, spin: (f32, bool, bool), free: Option<ConnEnd>, free_start: bool) {
let (w, h) = size;
let (rot, flip_h, flip_v) = spin;
let attached = Some(ConnEnd { id: 7, site: 3 });
let (start, end) = if free_start { (free, attached) } else { (attached, free) };
let group = InlineObject::Group {
w,
h,
ch_w: 200.0,
ch_h: 300.0,
float: Float { rot: 25.0, flip_h: true, ..Default::default() },
canvas: None,
children: vec![
GroupChild { x: 100.0, y: 200.0, obj: shape(ShapeKind::Rectangle, ShapeExtra { id: 7, ..Default::default() }, Float::default()) },
GroupChild {
x: 10.0,
y: 30.0,
obj: shape(kind, ShapeExtra { start, end, ..Default::default() }, Float { rot, flip_h, flip_v, ..Default::default() }),
},
],
};
// Rendering scales each child rectangle before applying its own spin.
let before = visual_ends(&group);
let expected = if free_start { before.0 } else { before.1 };
let mut s = Session::new(wordcraft_doc::Document::new());
let pos = s.doc.start_of(StoryRef::Body);
s.doc.para_mut(pos.story, &pos.path).unwrap().insert_object(pos.off, group, &Default::default()).unwrap();
assert!(reroute(&mut s));
let group = s.doc.para_at(&pos).unwrap().object_at(pos.off).unwrap();
let after = visual_ends(group);
let actual = if free_start { after.0 } else { after.1 };
assert!(
(actual.0 - expected.0).abs() < 0.001 && (actual.1 - expected.1).abs() < 0.001,
"{w}x{h}, {kind:?}, {rot}, {flip_h}, {flip_v}, {free_start}: {actual:?} != {expected:?}"
);
let InlineObject::Group { children, .. } = group else { panic!("group") };
let c = &children[1];
let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!("connector") };
let routed = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v);
assert_eq!(if free_start { routed.1 } else { routed.0 }, (140.0, 210.0), "resolved sites stay in child coordinates");
assert_eq!(float.rot, 0.0);
assert!(!reroute(&mut s), "rerouting must settle");
}
#[test]
fn rerouting_scaled_group_preserves_visual_free_and_unresolved_ends() {
for (w, h) in [(200.0, 300.0), (400.0, 600.0), (400.0, 150.0)] {
@@ -335,53 +380,7 @@ mod tests {
for (flip_h, flip_v) in [(false, false), (true, false), (false, true), (true, true)] {
for free in [None, Some(ConnEnd { id: 404, site: 0 }), Some(ConnEnd { id: 7, site: 99 })] {
for free_start in [false, true] {
let attached = Some(ConnEnd { id: 7, site: 3 });
let (start, end) = if free_start { (free, attached) } else { (attached, free) };
let group = InlineObject::Group {
w,
h,
ch_w: 200.0,
ch_h: 300.0,
float: Float { rot: 25.0, flip_h: true, ..Default::default() },
canvas: None,
children: vec![
GroupChild {
x: 100.0,
y: 200.0,
obj: shape(ShapeKind::Rectangle, ShapeExtra { id: 7, ..Default::default() }, Float::default()),
},
GroupChild {
x: 10.0,
y: 30.0,
obj: shape(
kind,
ShapeExtra { start, end, ..Default::default() },
Float { rot, flip_h, flip_v, ..Default::default() },
),
},
],
};
// Rendering scales each child rectangle before applying its own spin.
let before = visual_ends(&group);
let expected = if free_start { before.0 } else { before.1 };
let mut s = Session::new(wordcraft_doc::Document::new());
let pos = s.doc.start_of(StoryRef::Body);
s.doc.para_mut(pos.story, &pos.path).unwrap().insert_object(pos.off, group, &Default::default()).unwrap();
assert!(reroute(&mut s));
let group = s.doc.para_at(&pos).unwrap().object_at(pos.off).unwrap();
let after = visual_ends(group);
let actual = if free_start { after.0 } else { after.1 };
assert!(
(actual.0 - expected.0).abs() < 0.001 && (actual.1 - expected.1).abs() < 0.001,
"{w}x{h}, {kind:?}, {rot}, {flip_h}, {flip_v}, {free_start}: {actual:?} != {expected:?}"
);
let InlineObject::Group { children, .. } = group else { panic!("group") };
let c = &children[1];
let InlineObject::Shape { w, h, float, .. } = &c.obj else { panic!("connector") };
let routed = ends(c.x, c.y, *w, *h, float.flip_h, float.flip_v);
assert_eq!(if free_start { routed.1 } else { routed.0 }, (140.0, 210.0), "resolved sites stay in child coordinates");
assert_eq!(float.rot, 0.0);
assert!(!reroute(&mut s), "rerouting must settle");
assert_scaled_group_reroute((w, h), kind, (rot, flip_h, flip_v), free, free_start);
}
}
}
+31 -25
View File
@@ -356,6 +356,31 @@ pub enum Piece {
/// Read Replace with text: `^` codes (unless `literal`), `\1`–`\9` with wildcards, and with a
/// regex also `$n`, `${n}`, `${name}`, `$name`, `$$`, `\n` (paragraph mark), `\t` and `\\`.
/// Pushes the pending literal text, if any.
fn flush_literal(cur: &mut String, out: &mut Vec<Piece>) {
if !cur.is_empty() {
out.push(Piece::Lit(std::mem::take(cur)));
}
}
/// The `^` code whose letter is `chars[i]` (`^&`, `^c`, a replace code or a numeric one): pushes
/// its piece and returns the characters it used, or `None` when it isn't a code.
fn caret_code(chars: &[char], i: usize, cur: &mut String, out: &mut Vec<Piece>) -> Option<usize> {
let &n = chars.get(i)?;
if n == '&' || n == 'c' {
flush_literal(cur, out);
out.push(if n == '&' { Piece::Found } else { Piece::Clip });
return Some(1);
}
if let Some(ch) = replace_code(n) {
cur.push(ch);
return Some(1);
}
let (ch, used) = numeric_code(chars, i)?;
cur.push(ch);
Some(used)
}
pub fn parse_replace(with: &str, o: &Opts) -> Vec<Piece> {
// With a regex, `special: false` turns off only the `^` codes; group references still expand.
if o.literal && !o.regex {
@@ -364,11 +389,6 @@ pub fn parse_replace(with: &str, o: &Opts) -> Vec<Piece> {
let chars: Vec<char> = with.chars().collect();
let mut out = Vec::new();
let mut cur = String::new();
let flush = |cur: &mut String, out: &mut Vec<Piece>| {
if !cur.is_empty() {
out.push(Piece::Lit(std::mem::take(cur)));
}
};
let mut i = 0;
while let Some(&c) = chars.get(i) {
i += 1;
@@ -379,7 +399,7 @@ pub fn parse_replace(with: &str, o: &Opts) -> Vec<Piece> {
i += used;
}
Some((Ref::Group(pc), used)) => {
flush(&mut cur, &mut out);
flush_literal(&mut cur, &mut out);
out.extend(pc);
i += used;
}
@@ -391,7 +411,7 @@ pub fn parse_replace(with: &str, o: &Opts) -> Vec<Piece> {
if c == '\\' && o.wildcards {
match chars.get(i) {
Some(d) if d.is_ascii_digit() && *d != '0' => {
flush(&mut cur, &mut out);
flush_literal(&mut cur, &mut out);
out.push(Piece::Group(d.to_digit(10).unwrap_or(0) as usize));
i += 1;
continue;
@@ -406,28 +426,14 @@ pub fn parse_replace(with: &str, o: &Opts) -> Vec<Piece> {
}
if c == '^'
&& !o.literal
&& let Some(&n) = chars.get(i)
&& let Some(used) = caret_code(&chars, i, &mut cur, &mut out)
{
if n == '&' || n == 'c' {
flush(&mut cur, &mut out);
out.push(if n == '&' { Piece::Found } else { Piece::Clip });
i += 1;
continue;
}
if let Some(ch) = replace_code(n) {
cur.push(ch);
i += 1;
continue;
}
if let Some((ch, used)) = numeric_code(&chars, i) {
cur.push(ch);
i += used;
continue;
}
i += used;
continue;
}
cur.push(c);
}
flush(&mut cur, &mut out);
flush_literal(&mut cur, &mut out);
out
}
+1 -1
View File
@@ -899,7 +899,7 @@ fn tab_indent_on(s: &Session) -> bool {
fn tab_indents(s: &mut Session, f: &Pos) -> bool {
let Some(para) = s.doc.para_at(f).filter(|p| !p.is_empty()) else { return false };
if f.off == 0 {
return !s.doc.styles.resolve_para(&para.props).numbering.is_some_and(|n| n.num != 0);
return s.doc.styles.resolve_para(&para.props).numbering.is_none_or(|n| n.num == 0);
}
let l = s.layout();
l.line_bounds(f, s.page_hint).is_some_and(|(a, _)| a == *f)
+1 -1
View File
@@ -4528,7 +4528,7 @@ fn place_table(ctx: &mut Ctx, pb: &mut PageBuilder, t: &wordcraft_doc::Table, bl
&& !pb.at_top()
&& !fresh
&& rest.is_none()
&& !ri.checked_sub(1).and_then(|prev| kept.get(prev)).is_some_and(|&(end, _)| end >= ri)
&& ri.checked_sub(1).and_then(|prev| kept.get(prev)).is_none_or(|&(end, _)| end < ri)
&& kept.get(ri).is_some_and(|&(end, height)| {
if end == ri {
return false;
+5 -6
View File
@@ -314,13 +314,12 @@ fn parse_aff(aff: &str) -> Result<Rules, String> {
match dir {
"SET" | "LANG" | "NAME" | "VERSION" | "HOME" | "WORDCHARS" | "MAXNGRAMSUGS" | "MAXDIFF" | "ONLYMAXDIFF" | "NOSPLITSUGS"
| "SUGSWITHDOTS" | "WARN" | "FORBIDWARN" | "MAXCPDSUGS" => {}
"FLAG" => {
// Per-character flag modes (`FLAG UTF-8`, as VERO's pt_BR dictionary uses) work
// with the one-character parsing here; only the multi-character encodings differ.
if !matches!(at(1), "UTF-8" | "DEFAULT" | "utf-8" | "default" | "UTF8" | "") {
return Err(format!("line {}: FLAG {} is not supported (one-character flags only)", n + 1, at(1)));
}
// Per-character flag modes (`FLAG UTF-8`, as VERO's pt_BR dictionary uses) work
// with the one-character parsing here; only the multi-character encodings differ.
"FLAG" if !matches!(at(1), "UTF-8" | "DEFAULT" | "utf-8" | "default" | "UTF8" | "") => {
return Err(format!("line {}: FLAG {} is not supported (one-character flags only)", n + 1, at(1)));
}
"FLAG" => {}
directive @ ("FORBIDDENWORD" | "NOSUGGEST") => {
let mut it = at(1).chars().filter(|&c| c != EMOJI_SELECTOR);
match (it.next(), it.next()) {
+1 -1
View File
@@ -952,7 +952,7 @@ fn alert(ui: &mut Ui, text: &Option<String>) {
fn split_cells_ui(app: &mut WordApp, ui: &mut Ui, f: &mut SplitCellsForm) -> bool {
// Several cells split one by one keep their rows.
let rows_on = !(f.cells > 1 && !f.merge_first);
let rows_on = f.cells <= 1 || f.merge_first;
egui::Grid::new("split_cells").num_columns(2).show(ui, |ui| {
ui.label(tl!("Number of columns:"));
ui.add(egui::DragValue::new(&mut f.columns).range(1..=63));
+1 -1
View File
@@ -773,7 +773,7 @@ pub fn logic(app: &mut WordApp, ctx: &egui::Context) {
}
// Alt off while an Alt key is still down (AltGr on Linux turns Level 3 Shift on, not
// Alt): the key's release ends the press, not this.
egui::Event::ModifiersChanged(modifiers) if !(app.ui.alt_key_down && !modifiers.alt) => {
egui::Event::ModifiersChanged(modifiers) if !app.ui.alt_key_down || modifiers.alt => {
alt_released |= alt_transition(app, modifiers.alt, *modifiers);
}
egui::Event::PointerButton { pressed: true, .. } => {
+1 -1
View File
@@ -272,7 +272,7 @@ pub fn pointer(app: &mut WordApp, ui: &Ui, resp: &egui::Response, pages: &[Rect]
let adding = ui.input(|i| i.modifiers.shift || i.modifiers.command);
// Ctrl on the object that is already selected starts a copy-drag instead.
let again = selected(app).is_some_and(|(p, _)| p == object.pos());
if pressed && adding && grab == Grab::Move && selected(app).is_some() && !(again && !ui.input(|i| i.modifiers.shift)) {
if pressed && adding && grab == Grab::Move && selected(app).is_some() && (ui.input(|i| i.modifiers.shift) || !again) {
let _ = app.run("select.addObject", json!({"pos": object.pos()}));
return true;
}
+1 -1
View File
@@ -37,7 +37,7 @@ pub fn run(args: &[&str]) -> Result<(), String> {
None => 100,
};
let pos: Vec<&str> =
args.iter().enumerate().filter(|(i, a)| !a.starts_with("--") && !(*i > 0 && args[i - 1] == "--dpi")).map(|(_, a)| *a).collect();
args.iter().enumerate().filter(|(i, a)| !(a.starts_with("--") || *i > 0 && args[i - 1] == "--dpi")).map(|(_, a)| *a).collect();
let [reference, test, out] = pos.as_slice() else {
return Err("usage: cargo xtask visual-diff REF.pdf TEST.pdf OUTDIR [--private] [--dpi N]".into());
};