mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-29 12:57:09 -06:00
Sonar: split flood fill, CSV separator, noise collapse and numbered backspace
Extract helpers with unchanged behaviour to bring these functions under the cognitive-complexity limit (S3776): - engine cmd/objects.rs clear_background: border_pixels, run_span, push_open_runs. - engine cmd/tools.rs collapse_noise_matches: flatten_region (Kind is now module-level). - engine cmd/mailings.rs parse_csv: csv_separator, push_csv_row. - engine cmd/text.rs backspace_inner: remove_number.
This commit is contained in:
@@ -522,7 +522,8 @@ fn insert_field(s: &mut Session, instr: &str, placeholder: &str) -> CmdResult {
|
||||
}
|
||||
|
||||
/// Parse CSV (RFC 4180-ish: quotes, doubled quotes, commas/semicolons/tabs).
|
||||
pub fn parse_csv(text: &str) -> Vec<Vec<String>> {
|
||||
/// The field separator: whichever of `,` `;` and tab the first line (outside quotes) uses most.
|
||||
fn csv_separator(text: &str) -> char {
|
||||
let first = text.lines().next().unwrap_or("");
|
||||
let mut quoted = false;
|
||||
let mut unquoted = String::new();
|
||||
@@ -533,7 +534,20 @@ pub fn parse_csv(text: &str) -> Vec<Vec<String>> {
|
||||
unquoted.push(c);
|
||||
}
|
||||
}
|
||||
let sep = [',', ';', '\t'].into_iter().max_by_key(|c| unquoted.matches(*c).count()).unwrap_or(',');
|
||||
[',', ';', '\t'].into_iter().max_by_key(|c| unquoted.matches(*c).count()).unwrap_or(',')
|
||||
}
|
||||
|
||||
/// Ends the current row: kept when it has any non-empty field, otherwise dropped.
|
||||
fn push_csv_row(rows: &mut Vec<Vec<String>>, row: &mut Vec<String>) {
|
||||
if row.iter().any(|x| !x.is_empty()) {
|
||||
rows.push(std::mem::take(row));
|
||||
} else {
|
||||
row.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_csv(text: &str) -> Vec<Vec<String>> {
|
||||
let sep = csv_separator(text);
|
||||
let mut rows = Vec::new();
|
||||
let mut row = Vec::new();
|
||||
let mut cell = String::new();
|
||||
@@ -560,11 +574,7 @@ pub fn parse_csv(text: &str) -> Vec<Vec<String>> {
|
||||
chars.next();
|
||||
}
|
||||
row.push(std::mem::take(&mut cell));
|
||||
if row.iter().any(|x| !x.is_empty()) {
|
||||
rows.push(std::mem::take(&mut row));
|
||||
} else {
|
||||
row.clear();
|
||||
}
|
||||
push_csv_row(&mut rows, &mut row);
|
||||
} else {
|
||||
cell.push(c);
|
||||
}
|
||||
@@ -573,9 +583,7 @@ pub fn parse_csv(text: &str) -> Vec<Vec<String>> {
|
||||
}
|
||||
}
|
||||
row.push(cell);
|
||||
if row.iter().any(|x| !x.is_empty()) {
|
||||
rows.push(row);
|
||||
}
|
||||
push_csv_row(&mut rows, &mut row);
|
||||
rows
|
||||
}
|
||||
|
||||
|
||||
@@ -1396,27 +1396,13 @@ fn clear_background(rgba: &mut image::RgbaImage, tol: f32) -> usize {
|
||||
!seen.get(at(x, y)).copied().unwrap_or(true) && rgba.get_pixel_checked(x, y).is_some_and(|p| close(p.0))
|
||||
};
|
||||
let mut seen = vec![false; (w as usize) * (h as usize)];
|
||||
let mut stack: Vec<(u32, u32)> = Vec::new();
|
||||
for x in 0..w {
|
||||
stack.push((x, 0));
|
||||
stack.push((x, h - 1));
|
||||
}
|
||||
for y in 0..h {
|
||||
stack.push((0, y));
|
||||
stack.push((w - 1, y));
|
||||
}
|
||||
let mut stack = border_pixels(w, h);
|
||||
let mut most = stack.len();
|
||||
while let Some((x, y)) = stack.pop() {
|
||||
if !open(rgba, &seen, x, y) {
|
||||
continue;
|
||||
}
|
||||
let (mut x0, mut x1) = (x, x);
|
||||
while x0 > 0 && open(rgba, &seen, x0 - 1, y) {
|
||||
x0 -= 1;
|
||||
}
|
||||
while x1 + 1 < w && open(rgba, &seen, x1 + 1, y) {
|
||||
x1 += 1;
|
||||
}
|
||||
let (x0, x1) = run_span(x, y, w, |xi, yi| open(rgba, &seen, xi, yi));
|
||||
for xi in x0..=x1 {
|
||||
if let Some(s) = seen.get_mut(at(xi, y)) {
|
||||
*s = true;
|
||||
@@ -1426,23 +1412,54 @@ fn clear_background(rgba: &mut image::RgbaImage, tol: f32) -> usize {
|
||||
}
|
||||
}
|
||||
for ny in [y.checked_sub(1), y.checked_add(1).filter(|&n| n < h)].into_iter().flatten() {
|
||||
let mut xi = x0;
|
||||
while xi <= x1 {
|
||||
if open(rgba, &seen, xi, ny) {
|
||||
stack.push((xi, ny));
|
||||
while xi <= x1 && open(rgba, &seen, xi, ny) {
|
||||
xi += 1;
|
||||
}
|
||||
} else {
|
||||
xi += 1;
|
||||
}
|
||||
}
|
||||
push_open_runs(&mut stack, x0, x1, ny, |xi, yi| open(rgba, &seen, xi, yi));
|
||||
}
|
||||
most = most.max(stack.len());
|
||||
}
|
||||
most
|
||||
}
|
||||
|
||||
/// Every pixel on the image's edge, the flood-fill seeds.
|
||||
fn border_pixels(w: u32, h: u32) -> Vec<(u32, u32)> {
|
||||
let mut stack = Vec::new();
|
||||
for x in 0..w {
|
||||
stack.push((x, 0));
|
||||
stack.push((x, h - 1));
|
||||
}
|
||||
for y in 0..h {
|
||||
stack.push((0, y));
|
||||
stack.push((w - 1, y));
|
||||
}
|
||||
stack
|
||||
}
|
||||
|
||||
/// The horizontal run of open pixels around `(x, y)`, both ends included.
|
||||
fn run_span(x: u32, y: u32, w: u32, open: impl Fn(u32, u32) -> bool) -> (u32, u32) {
|
||||
let (mut x0, mut x1) = (x, x);
|
||||
while x0 > 0 && open(x0 - 1, y) {
|
||||
x0 -= 1;
|
||||
}
|
||||
while x1 + 1 < w && open(x1 + 1, y) {
|
||||
x1 += 1;
|
||||
}
|
||||
(x0, x1)
|
||||
}
|
||||
|
||||
/// Queues the first pixel of each open run in row `ny` between `x0` and `x1`.
|
||||
fn push_open_runs(stack: &mut Vec<(u32, u32)>, x0: u32, x1: u32, ny: u32, open: impl Fn(u32, u32) -> bool) {
|
||||
let mut xi = x0;
|
||||
while xi <= x1 {
|
||||
if open(xi, ny) {
|
||||
stack.push((xi, ny));
|
||||
while xi <= x1 && open(xi, ny) {
|
||||
xi += 1;
|
||||
}
|
||||
} else {
|
||||
xi += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(img: image::DynamicImage, border: u32, color: [u8; 4]) -> Result<image::DynamicImage, CmdError> {
|
||||
let (w, h) = grown(img.width(), img.height(), border).ok_or_else(too_large)?;
|
||||
let mut out = canvas(w, h, color)?;
|
||||
|
||||
@@ -916,6 +916,24 @@ fn backspace(s: &mut Session, v: &Value) -> CmdResult {
|
||||
/// numbering while keeping the text position, takes a List Paragraph out of the list, outdents,
|
||||
/// joins the previous paragraph or moves into a preceding table; the command wrapper restores a
|
||||
/// content-control placeholder.
|
||||
/// The first Backspace at a numbered paragraph's start removes the number but keeps the item's
|
||||
/// indent (Word): the text stays where it was, so the indent becomes the paragraph's own.
|
||||
fn remove_number(s: &mut Session, f: &Pos, n: NumRef) -> CmdResult {
|
||||
let text_at = s.doc.numbering.level(n.num, n.level).map(|l| l.indent);
|
||||
let p = s.doc.para_mut(f.story, &f.path)?;
|
||||
if p.props.style.as_deref() == Some("ListParagraph") {
|
||||
if p.props.indent_left.is_none() && p.props.indent_left_chars.is_none_or(|c| c == 0.0) {
|
||||
p.props.indent_left = text_at;
|
||||
}
|
||||
if p.props.indent_first.is_none() && p.props.indent_first_chars.is_none_or(|c| c == 0.0) {
|
||||
p.props.indent_first = Some(0.0);
|
||||
}
|
||||
}
|
||||
p.props.numbering = Some(NumRef { num: 0, level: 0 });
|
||||
p.touch();
|
||||
sel_result(s)
|
||||
}
|
||||
|
||||
fn backspace_inner(s: &mut Session, _: &Value) -> CmdResult {
|
||||
s.goal_x = None;
|
||||
s.pending = None;
|
||||
@@ -949,22 +967,8 @@ fn backspace_inner(s: &mut Session, _: &Value) -> CmdResult {
|
||||
return sel_result(s);
|
||||
}
|
||||
// At the start of a paragraph.
|
||||
if para.props.numbering.is_some_and(|n| n.num != 0) {
|
||||
// First Backspace removes the number but keeps the item's indent (Word): the text stays
|
||||
// where it was, so the indent becomes the paragraph's own (`num: 0` alone drops it).
|
||||
let text_at = para.props.numbering.and_then(|n| s.doc.numbering.level(n.num, n.level)).map(|l| l.indent);
|
||||
let p = s.doc.para_mut(f.story, &f.path)?;
|
||||
if p.props.style.as_deref() == Some("ListParagraph") {
|
||||
if p.props.indent_left.is_none() && p.props.indent_left_chars.is_none_or(|c| c == 0.0) {
|
||||
p.props.indent_left = text_at;
|
||||
}
|
||||
if p.props.indent_first.is_none() && p.props.indent_first_chars.is_none_or(|c| c == 0.0) {
|
||||
p.props.indent_first = Some(0.0);
|
||||
}
|
||||
}
|
||||
p.props.numbering = Some(NumRef { num: 0, level: 0 });
|
||||
p.touch();
|
||||
return sel_result(s);
|
||||
if let Some(n) = para.props.numbering.filter(|n| n.num != 0) {
|
||||
return remove_number(s, &f, n);
|
||||
}
|
||||
if para.props.style.as_deref() == Some("ListParagraph") {
|
||||
// The second one takes it out of the list's indent instead of joining paragraphs.
|
||||
|
||||
@@ -1554,12 +1554,13 @@ const NOISE_MATCH_LEN: usize = 1;
|
||||
/// stripped before running its table, so it's left untouched regardless of length; only a short
|
||||
/// match produced by the table's own backtracking, which can only ever be interior, is a
|
||||
/// candidate.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Kind {
|
||||
Keep,
|
||||
Fold,
|
||||
}
|
||||
|
||||
fn collapse_noise_matches(ops: &[Op]) -> Vec<Op> {
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Kind {
|
||||
Keep,
|
||||
Fold,
|
||||
}
|
||||
let mut runs: Vec<(Op, usize, Kind)> = Vec::new();
|
||||
let mut k = 0;
|
||||
while k < ops.len() {
|
||||
@@ -1586,12 +1587,7 @@ fn collapse_noise_matches(ops: &[Op]) -> Vec<Op> {
|
||||
}
|
||||
let region = &runs[start..i];
|
||||
if region.iter().any(|(_, _, kind)| *kind == Kind::Fold) {
|
||||
// Flatten the region: every original character first (in its own order), then every
|
||||
// revised character (in its own order) — one clean delete, one clean insert.
|
||||
let dels: usize = region.iter().map(|(op, len, kind)| if *kind == Kind::Fold || *op == Op::Del { *len } else { 0 }).sum();
|
||||
let inss: usize = region.iter().map(|(op, len, kind)| if *kind == Kind::Fold || *op == Op::Ins { *len } else { 0 }).sum();
|
||||
out.extend(std::iter::repeat_n(Op::Del, dels));
|
||||
out.extend(std::iter::repeat_n(Op::Ins, inss));
|
||||
flatten_region(&mut out, region);
|
||||
} else {
|
||||
for (op, len, _) in region {
|
||||
out.extend(std::iter::repeat_n(*op, *len));
|
||||
@@ -1601,6 +1597,15 @@ fn collapse_noise_matches(ops: &[Op]) -> Vec<Op> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Flattens a region with a folded run: every original character first (in its own order), then
|
||||
/// every revised character (in its own order) — one clean delete, one clean insert.
|
||||
fn flatten_region(out: &mut Vec<Op>, region: &[(Op, usize, Kind)]) {
|
||||
let dels: usize = region.iter().map(|(op, len, kind)| if *kind == Kind::Fold || *op == Op::Del { *len } else { 0 }).sum();
|
||||
let inss: usize = region.iter().map(|(op, len, kind)| if *kind == Kind::Fold || *op == Op::Ins { *len } else { 0 }).sum();
|
||||
out.extend(std::iter::repeat_n(Op::Del, dels));
|
||||
out.extend(std::iter::repeat_n(Op::Ins, inss));
|
||||
}
|
||||
|
||||
/// Words with the spaces after them ("The ", "cat "), or single characters.
|
||||
fn tokens(s: &str, characters: bool) -> Vec<String> {
|
||||
if characters {
|
||||
|
||||
Reference in New Issue
Block a user