diff --git a/crates/automation/src/comments.rs b/crates/automation/src/comments.rs index 0f41bdbb6..80961e768 100644 --- a/crates/automation/src/comments.rs +++ b/crates/automation/src/comments.rs @@ -305,7 +305,9 @@ impl Automation { let author = a.opt_str("author")?.unwrap_or(DEFAULT_AUTHOR).to_string(); // Replace Text: the struck-out text and a grouped caret holding `contents`. if kind == "replace" { - let Shape::TextMarkup { quads, .. } = shape else { unreachable!("replace marks text") }; + let Shape::TextMarkup { quads, .. } = shape else { + return Err(ToolError::InvalidArgs("replace marks text: give it text to replace".into())); + }; if contents.trim().is_empty() { return Err(ToolError::InvalidArgs("replace needs `contents`: the replacement text".into())); } diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index 4f14c2c47..02616822b 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -196,7 +196,8 @@ impl Automation { "read_only": f.read_only(), "required": f.has(field_flags::REQUIRED), }); - let o = v.as_object_mut().expect("object"); + let mut more = serde_json::Map::new(); + let o = &mut more; if let Some(t) = &f.tooltip { o.insert("tooltip".into(), json!(t)); } @@ -236,6 +237,9 @@ impl Automation { } _ => {} } + if let Value::Object(m) = &mut v { + m.extend(more); + } v }) .collect(); diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 6c36079a3..99c14b947 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -9,6 +9,8 @@ //! with the origin at the top-left of the displayed page. //! - An optional root directory confines every path a tool reads or writes. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + mod a11y; mod comments; mod content; @@ -1204,12 +1206,15 @@ impl Automation { fn renderer(&mut self, id: DocId) -> Result<&mut PageRenderer> { let doc = self.session.get(id).ok_or_else(|| failed("no such document"))?; - let stale = self.renderers.get(&id).is_none_or(|(b, _)| !Arc::ptr_eq(b, &doc.bytes)); - if stale { + let fresh = || { let config = RenderConfig { password: doc.password.as_deref().map(Arc::from), ..Default::default() }; - self.renderers.insert(id, (doc.bytes.clone(), PageRenderer::new(doc.bytes.clone(), config))); + (doc.bytes.clone(), PageRenderer::new(doc.bytes.clone(), config)) + }; + let entry = self.renderers.entry(id).or_insert_with(fresh); + if !Arc::ptr_eq(&entry.0, &doc.bytes) { + *entry = fresh(); } - Ok(&mut self.renderers.get_mut(&id).expect("inserted above").1) + Ok(&mut entry.1) } fn page_render(&mut self, a: &Args) -> Result { @@ -1245,13 +1250,16 @@ impl Automation { if !Arc::ptr_eq(&entry.0, &bytes) || entry.1.len() != n { *entry = (bytes.clone(), vec![None; n]); } - let mut missing: Vec = pages.iter().copied().filter(|p| entry.1[*p].is_none()).collect(); + let mut missing: Vec = pages.iter().copied().filter(|p| entry.1.get(*p).is_some_and(Option::is_none)).collect(); missing.sort_unstable(); missing.dedup(); for (p, text) in extract_parallel(&bytes, password, &missing) { - entry.1[p] = Some(Arc::new(text.map_err(|e| failed(format!("page {}: {e}", p + 1)))?)); + let text = Arc::new(text.map_err(|e| failed(format!("page {}: {e}", p + 1)))?); + if let Some(slot) = entry.1.get_mut(p) { + *slot = Some(text); + } } - Ok(pages.iter().map(|p| entry.1[*p].clone().expect("extracted above")).collect()) + pages.iter().map(|p| entry.1.get(*p).cloned().flatten().ok_or_else(|| failed(format!("page {} has no text", p + 1)))).collect() } fn text_extract(&mut self, a: &Args) -> Result { diff --git a/crates/automation/src/redact.rs b/crates/automation/src/redact.rs index b0a9ffd6c..6c68ade66 100644 --- a/crates/automation/src/redact.rs +++ b/crates/automation/src/redact.rs @@ -73,7 +73,10 @@ impl Automation { Edit::AddAnnotation(NewAnnotation { page: *page, shape, style: style.clone(), contents: String::new(), author: author.clone() }) }) .collect(); - let edit = if edits.len() == 1 { edits.into_iter().next().expect("one") } else { Edit::Batch { label: "Mark for redaction".into(), edits } }; + let edit = match <[Edit; 1]>::try_from(edits) { + Ok([one]) => one, + Err(edits) => Edit::Batch { label: "Mark for redaction".into(), edits }, + }; let mut out = self.apply(a, edit)?; out["marked"] = json!(count); out["pages_marked"] = json!(marks.iter().map(|m| m.0 + 1).collect::>()); diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 55caa101e..a8eba2d21 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -48,11 +48,15 @@ fn color() -> Value { /// Properties that pick one comment: its `id` (from comment_list), or `page` + `index`. fn comment_ref(mut extra: Value) -> Value { - let props = extra.as_object_mut().expect("an object"); - props.insert("doc".into(), doc()); - props.insert("id".into(), json!({ "type": "string", "description": "The comment's id (from comment_list)." })); - props.insert("page".into(), json!({ "type": "integer", "minimum": 1, "description": "With `index`, instead of `id`." })); - props.insert("index".into(), json!({ "type": "integer", "minimum": 1, "description": "1-based position on the page, as comment_list reports." })); + if let Some(props) = extra.as_object_mut() { + props.insert("doc".into(), doc()); + props.insert("id".into(), json!({ "type": "string", "description": "The comment's id (from comment_list)." })); + props.insert("page".into(), json!({ "type": "integer", "minimum": 1, "description": "With `index`, instead of `id`." })); + props.insert( + "index".into(), + json!({ "type": "integer", "minimum": 1, "description": "1-based position on the page, as comment_list reports." }), + ); + } extra }