diff --git a/apps/wordcraft-web/index.html b/apps/wordcraft-web/index.html
index ca6ad4693..b7528461a 100644
--- a/apps/wordcraft-web/index.html
+++ b/apps/wordcraft-web/index.html
@@ -2,7 +2,7 @@
-
+
WordCraft
diff --git a/crates/doc/src/eq.rs b/crates/doc/src/eq.rs
index 732532f04..6280896c7 100644
--- a/crates/doc/src/eq.rs
+++ b/crates/doc/src/eq.rs
@@ -117,7 +117,7 @@ fn arg_text(s: &str) -> String {
}
// A switch: its letters, then an optional (signed) number.
Some(_) => {
- while it.next_if(|n| n.is_ascii_alphabetic()).is_some() {}
+ while it.next_if(char::is_ascii_alphabetic).is_some() {}
while it.next_if(|n| *n == ' ').is_some() {}
while it.next_if(|n| n.is_ascii_digit() || *n == '-' || *n == '.').is_some() {}
}
diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs
index d4c548a2c..91e33a993 100644
--- a/crates/doc/src/lib.rs
+++ b/crates/doc/src/lib.rs
@@ -229,7 +229,7 @@ impl Path {
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let s: Vec = self.0.iter().map(|i| i.to_string()).collect();
+ let s: Vec = self.0.iter().map(ToString::to_string).collect();
write!(f, "{}", s.join("/"))
}
}
@@ -801,34 +801,36 @@ impl Document {
d
}
- /// Every story must hold at least one paragraph, and every cell too.
- pub fn ensure_nonempty(&mut self) {
- fn fix(bl: &mut Blocks) {
- if !bl.iter().any(|b| matches!(**b, Block::Para(_))) || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
- bl.push(para_block(Paragraph::new()));
- }
- for b in bl.iter_mut() {
- if let Block::Table(t) = &**b {
- let needs = t
- .rows
- .iter()
- .any(|r| r.cells.iter().any(|c| c.blocks.is_empty() || matches!(c.blocks.last().map(|b| &**b), Some(Block::Table(_)))));
- if needs && let Block::Table(t) = Arc::make_mut(b) {
- for r in &mut t.rows {
- for c in &mut r.cells {
- fix_cell(&mut c.blocks);
- }
+ fn fix_cell_blocks(bl: &mut Blocks) {
+ if bl.is_empty() || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
+ bl.push(para_block(Paragraph::new()));
+ }
+ }
+
+ fn fix_story_blocks(bl: &mut Blocks) {
+ if !bl.iter().any(|b| matches!(**b, Block::Para(_))) || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
+ bl.push(para_block(Paragraph::new()));
+ }
+ for b in bl.iter_mut() {
+ if let Block::Table(t) = &**b {
+ let needs = t
+ .rows
+ .iter()
+ .any(|r| r.cells.iter().any(|c| c.blocks.is_empty() || matches!(c.blocks.last().map(|b| &**b), Some(Block::Table(_)))));
+ if needs && let Block::Table(t) = Arc::make_mut(b) {
+ for r in &mut t.rows {
+ for c in &mut r.cells {
+ Self::fix_cell_blocks(&mut c.blocks);
}
}
}
}
}
- fn fix_cell(bl: &mut Blocks) {
- if bl.is_empty() || matches!(bl.last().map(|b| &**b), Some(Block::Table(_))) {
- bl.push(para_block(Paragraph::new()));
- }
- }
- fix(&mut self.body);
+ }
+
+ /// Every story must hold at least one paragraph, and every cell too.
+ pub fn ensure_nonempty(&mut self) {
+ Self::fix_story_blocks(&mut self.body);
for p in self.parts.values_mut() {
if p.blocks.is_empty() {
p.blocks.push(para_block(Paragraph::new()));
diff --git a/crates/doc/src/math_latex.rs b/crates/doc/src/math_latex.rs
index 48c60b781..932aab182 100644
--- a/crates/doc/src/math_latex.rs
+++ b/crates/doc/src/math_latex.rs
@@ -47,7 +47,7 @@ fn tokenize(s: &str) -> Vec {
match c {
'\\' => {
let start = i;
- while cs.get(i).is_some_and(|c| c.is_ascii_alphabetic()) {
+ while cs.get(i).is_some_and(char::is_ascii_alphabetic) {
i += 1;
}
if i == start {
diff --git a/crates/doc/src/math_linear.rs b/crates/doc/src/math_linear.rs
index 5c3318732..1820c248d 100644
--- a/crates/doc/src/math_linear.rs
+++ b/crates/doc/src/math_linear.rs
@@ -339,7 +339,7 @@ impl P {
/// A prefix structure after its character.
fn prefix(&mut self, c: char, stops: &[char]) -> MNode {
match c {
- '√' | '∛' | '∜' => {
+ '√'..='∜' => {
let mut deg = match c {
'∛' => vec![text('3')],
'∜' => vec![text('4')],
@@ -410,7 +410,7 @@ impl P {
fn word(&mut self, stops: &[char]) -> Arg {
let start = self.i;
let mut end = start;
- while self.c.get(end).is_some_and(|c| c.is_ascii_alphabetic()) {
+ while self.c.get(end).is_some_and(char::is_ascii_alphabetic) {
end += 1;
}
let word: String = self.c.get(start..end).unwrap_or(&[]).iter().collect();
@@ -628,7 +628,7 @@ impl P {
// Word (#1609): the whole run of letters and digits up to a space or operator, so
// `V_dd` is V with a subscript `dd`, exactly like `V_(dd)`.
let start = self.i;
- while self.peek().is_some_and(|k| k.is_alphanumeric()) {
+ while self.peek().is_some_and(char::is_alphanumeric) {
self.i += 1;
}
let t: String = self.c.get(start..self.i).unwrap_or(&[]).iter().collect();
diff --git a/crates/docx/src/units.rs b/crates/docx/src/units.rs
index 7cbbfad8d..69b52eef6 100644
--- a/crates/docx/src/units.rs
+++ b/crates/docx/src/units.rs
@@ -31,7 +31,7 @@ pub fn measure(s: &str, per_pt: f32) -> Option {
Some(&s) if s == b'+' || s == b'-' => &rest[1..],
_ => rest,
};
- if after_sign.first().is_some_and(|c| c.is_ascii_digit()) {
+ if after_sign.first().is_some_and(u8::is_ascii_digit) {
in_exp = true;
continue;
}
diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs
index 7f3ddc450..292d2bd54 100644
--- a/crates/docx/src/write/story.rs
+++ b/crates/docx/src/write/story.rs
@@ -29,6 +29,43 @@ enum Piece<'a> {
}
impl Writer<'_> {
+ fn update_toc_before_block(&mut self, i: usize, toc: Option<(usize, usize)>, toc_field_open: bool, w: &mut W) {
+ if let Some((start, end)) = toc {
+ if i == start {
+ w.raw(TOC_SDT_OPEN);
+ self.toc_hold_end = true;
+ }
+ self.toc_begin_here = toc_field_open && i == start + 1;
+ self.toc_end_here = toc_field_open && i == end;
+ }
+ }
+
+ fn update_toc_after_block(&mut self, i: usize, toc: Option<(usize, usize)>, toc_field_open: &mut bool, w: &mut W) {
+ if let Some((start, end)) = toc {
+ if i == start {
+ // `field` holds the heading's TOC field back for the first entry.
+ self.toc_hold_end = false;
+ *toc_field_open = self.toc_field.is_some();
+ }
+ if i == end {
+ w.raw("");
+ }
+ }
+ }
+
+ fn write_trailing_para(&mut self, w: &mut W, depth: usize) {
+ // A header with no paragraph of its own before: the watermark goes in this one.
+ if depth == 0
+ && let Some(run) = self.pending_watermark.take()
+ {
+ w.raw("");
+ w.raw(&run);
+ w.raw("");
+ } else {
+ w.raw("");
+ }
+ }
+
/// Write a block list. `top` = body level (sections allowed).
pub fn blocks(&mut self, w: &mut W, bl: &Blocks, rels: &mut PartRels, top: bool, depth: usize) {
if depth > MAX_DEPTH {
@@ -43,14 +80,7 @@ impl Writer<'_> {
for c in plan.opens.get(i).into_iter().flatten() {
super::sdt::open(w, c);
}
- if let Some((start, end)) = toc {
- if i == start {
- w.raw(TOC_SDT_OPEN);
- self.toc_hold_end = true;
- }
- self.toc_begin_here = toc_field_open && i == start + 1;
- self.toc_end_here = toc_field_open && i == end;
- }
+ self.update_toc_before_block(i, toc, toc_field_open, w);
match &**b {
Block::Para(p) => {
self.inline_controls = plan.inline_in(i);
@@ -58,16 +88,7 @@ impl Writer<'_> {
}
Block::Table(t) => self.table(w, t, rels, depth),
}
- if let Some((start, end)) = toc {
- if i == start {
- // `field` holds the heading's TOC field back for the first entry.
- self.toc_hold_end = false;
- toc_field_open = self.toc_field.is_some();
- }
- if i == end {
- w.raw("");
- }
- }
+ self.update_toc_after_block(i, toc, &mut toc_field_open, w);
for _ in 0..plan.closes.get(i).copied().unwrap_or(0) {
super::sdt::close(w);
}
@@ -76,16 +97,7 @@ impl Writer<'_> {
self.move_ranges(w, &[]);
}
if bl.last().is_none_or(|b| matches!(**b, Block::Table(_))) {
- // A header with no paragraph of its own before: the watermark goes in this one.
- if depth == 0
- && let Some(run) = self.pending_watermark.take()
- {
- w.raw("");
- w.raw(&run);
- w.raw("");
- } else {
- w.raw("");
- }
+ self.write_trailing_para(w, depth);
}
}
@@ -158,6 +170,23 @@ impl Writer<'_> {
self.para_inner(w, p, rels, top, depth, false);
}
+ fn write_mark_rpr(&self, w: &mut W, mark: &CharProps, mark_revs: &[(u32, &str)]) {
+ w.open("w:rPr", &[]);
+ for (idx, tag) in mark_revs {
+ let (id, author, date) = self.rev_attrs(*idx);
+ if date.is_empty() {
+ w.empty(tag, &[("w:id", &id), ("w:author", &author)]);
+ } else {
+ w.empty(tag, &[("w:id", &id), ("w:author", &author), ("w:date", &date)]);
+ }
+ }
+ rpr_inner(w, mark);
+ if let Some(ch) = mark.fmt_change.as_deref() {
+ self.rpr_change(w, ch);
+ }
+ w.close("w:rPr");
+ }
+
fn para_inner(&mut self, w: &mut W, p: &Paragraph, rels: &mut PartRels, top: bool, depth: usize, framed: bool) {
let para_id = self.take_para_id(p);
match ¶_id {
@@ -185,20 +214,7 @@ impl Writer<'_> {
});
ppr_inner(w, &pp, framed, num_change.as_ref());
if has_mark {
- w.open("w:rPr", &[]);
- for (idx, tag) in mark_revs {
- let (id, author, date) = self.rev_attrs(idx);
- if date.is_empty() {
- w.empty(tag, &[("w:id", &id), ("w:author", &author)]);
- } else {
- w.empty(tag, &[("w:id", &id), ("w:author", &author), ("w:date", &date)]);
- }
- }
- rpr_inner(w, &p.mark);
- if let Some(ch) = p.mark.fmt_change.as_deref() {
- self.rpr_change(w, ch);
- }
- w.close("w:rPr");
+ self.write_mark_rpr(w, &p.mark, &mark_revs);
}
if let Some(s) = section {
self.sectpr(w, s, rels);
diff --git a/crates/engine/src/cmd/citations.rs b/crates/engine/src/cmd/citations.rs
index 6c2cadd41..b3950cf56 100644
--- a/crates/engine/src/cmd/citations.rs
+++ b/crates/engine/src/cmd/citations.rs
@@ -1090,7 +1090,7 @@ fn entries_for(s: &mut Session, kind: &str) -> Vec<(String, bool)> {
out.push((l, false));
}
}
- let list = pages.iter().map(|p| p.to_string()).collect::>().join(", ");
+ let list = pages.iter().map(ToString::to_string).collect::>().join(", ");
out.push(if kind.starts_with("INDEX") { (format!("{e}, {list}"), true) } else { (format!("{e}\t{list}"), false) });
}
out
diff --git a/crates/engine/src/cmd/controls.rs b/crates/engine/src/cmd/controls.rs
index 6ad46d76d..10ff46aef 100644
--- a/crates/engine/src/cmd/controls.rs
+++ b/crates/engine/src/cmd/controls.rs
@@ -579,27 +579,8 @@ fn info(s: &mut Session, v: &Value) -> CmdResult {
Ok(describe(s, &r))
}
-fn properties(s: &mut Session, v: &Value) -> CmdResult {
- let r = target(s, v)?;
- let mut c = r.control.clone();
- let mut content: Option = None;
- if let Some(t) = short(v, "title") {
- c.title = t;
- }
- if let Some(t) = short(v, "tag") {
- c.tag = t;
- }
- let (no_delete, no_edit) = (p::bool(v, "lockDelete").unwrap_or(!c.lock.can_delete()), p::bool(v, "lockEdit").unwrap_or(!c.lock.can_edit()));
- c.lock = ControlLock::from_flags(no_delete, no_edit);
- if let Some(t) = p::bool(v, "temporary") {
- c.temporary = t;
- }
- if let Some(t) = short(v, "placeholder").filter(|t| !t.trim().is_empty()) {
- if c.showing_placeholder && t != c.placeholder_shown() {
- content = Some(t.clone());
- }
- c.placeholder_text = t;
- }
+fn apply_kind_properties(c: &mut StructuredControl, v: &Value) -> Option {
+ let mut content = None;
match &mut c.kind {
ControlKind::Text { multi_line } => {
if let Some(m) = p::bool(v, "multiLine") {
@@ -633,6 +614,33 @@ fn properties(s: &mut Session, v: &Value) -> CmdResult {
}
_ => {}
}
+ content
+}
+
+fn properties(s: &mut Session, v: &Value) -> CmdResult {
+ let r = target(s, v)?;
+ let mut c = r.control.clone();
+ let mut content: Option = None;
+ if let Some(t) = short(v, "title") {
+ c.title = t;
+ }
+ if let Some(t) = short(v, "tag") {
+ c.tag = t;
+ }
+ let (no_delete, no_edit) = (p::bool(v, "lockDelete").unwrap_or(!c.lock.can_delete()), p::bool(v, "lockEdit").unwrap_or(!c.lock.can_edit()));
+ c.lock = ControlLock::from_flags(no_delete, no_edit);
+ if let Some(t) = p::bool(v, "temporary") {
+ c.temporary = t;
+ }
+ if let Some(t) = short(v, "placeholder").filter(|t| !t.trim().is_empty()) {
+ if c.showing_placeholder && t != c.placeholder_shown() {
+ content = Some(t.clone());
+ }
+ c.placeholder_text = t;
+ }
+ if let Some(kc) = apply_kind_properties(&mut c, v) {
+ content = Some(kc);
+ }
if let Some(text) = content {
let base = content_base(s, &r);
let props = if c.showing_placeholder { placeholder_props(s, &base) } else { base };
diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs
index 0cf413dd9..8c9b4a2d7 100644
--- a/crates/engine/src/cmd/format.rs
+++ b/crates/engine/src/cmd/format.rs
@@ -601,6 +601,48 @@ fn convert(src: &str, mode: &str, para_start: bool) -> String {
convert_chars(src, mode, para_start, &turkic).concat()
}
+fn convert_title_char(
+ c: char,
+ i: usize,
+ up: &mut bool,
+ upper: impl Fn(char) -> String,
+ lower: impl Fn(usize, char) -> String,
+) -> String {
+ let n = if *up && c.is_alphabetic() {
+ *up = false;
+ upper(c)
+ } else {
+ lower(i, c)
+ };
+ if c.is_whitespace() {
+ *up = true;
+ }
+ n
+}
+
+fn convert_sentence_char(
+ c: char,
+ i: usize,
+ up: &mut bool,
+ chars: &[char],
+ upper: impl Fn(char) -> String,
+ lower: impl Fn(usize, char) -> String,
+) -> String {
+ let n = if *up && c.is_alphabetic() {
+ *up = false;
+ upper(c)
+ } else {
+ lower(i, c)
+ };
+ let is_decimal = c == '.'
+ && i.checked_sub(1).and_then(|j| chars.get(j)).is_some_and(char::is_ascii_digit)
+ && chars.get(i + 1).is_some_and(char::is_ascii_digit);
+ if matches!(c, '!' | '?') || (c == '.' && !is_decimal) {
+ *up = true;
+ }
+ n
+}
+
/// The case-converted form of each character of `src`, in order (one entry per char, so a
/// change of length such as ß → SS or İ → i̇ can be applied run by run). `turkic` is one flag
/// per char: true when that character's run is Turkish, Azerbaijani or Crimean Tatar.
@@ -634,34 +676,8 @@ fn convert_chars(src: &str, mode: &str, para_start: bool, turkic: &[bool]) -> Ve
upper(c)
}
}
- "title" => {
- let n = if up && c.is_alphabetic() {
- up = false;
- upper(c)
- } else {
- lower(i, c)
- };
- if c.is_whitespace() {
- up = true;
- }
- n
- }
- _ => {
- // Sentence case.
- let n = if up && c.is_alphabetic() {
- up = false;
- upper(c)
- } else {
- lower(i, c)
- };
- let is_decimal = c == '.'
- && i.checked_sub(1).and_then(|j| chars.get(j)).is_some_and(|p| p.is_ascii_digit())
- && chars.get(i + 1).is_some_and(|n| n.is_ascii_digit());
- if matches!(c, '!' | '?') || (c == '.' && !is_decimal) {
- up = true;
- }
- n
- }
+ "title" => convert_title_char(c, i, &mut up, &upper, &lower),
+ _ => convert_sentence_char(c, i, &mut up, &chars, &upper, &lower),
};
out.push(new);
}
@@ -756,27 +772,6 @@ pub fn state(s: &Session) -> Value {
})
}
-#[cfg(test)]
-mod tests {
- use super::convert;
-
- #[test]
- fn case_conversions() {
- assert_eq!(convert("hello world. again", "sentence", true), "Hello world. Again");
- assert_eq!(convert("hello WORLD", "title", true), "Hello World");
- assert_eq!(convert("Hello", "toggle", true), "hELLO");
- assert_eq!(convert("straße", "upper", true), "STRASSE");
- assert_eq!(convert("hello world", "title", false), "Hello World");
- assert_eq!(convert("ΣΟΦΟΣ ΟΔΟΣ", "lower", true), "σοφος οδος");
- }
-
- #[test]
- fn sentence_case_ignores_decimal_points() {
- // #3485: A period between two digits is a decimal point, not a sentence boundary.
- assert_eq!(convert("one 3.14 apples and two 2.5 pears.", "sentence", true), "One 3.14 apples and two 2.5 pears.");
- assert_eq!(convert("pi is 3.14. next sentence.", "sentence", true), "Pi is 3.14. Next sentence.");
- }
-}
/// What Enclose Characters works on: the selected text (one full-width or up to two half-width
/// characters), or enclosed characters already there (selected, or beside the caret).
@@ -884,3 +879,25 @@ fn enclose(s: &mut Session, v: &Value) -> CmdResult {
s.sel = crate::Selection { anchor: at, focus: end };
sel_result(s)
}
+
+#[cfg(test)]
+mod tests {
+ use super::convert;
+
+ #[test]
+ fn case_conversions() {
+ assert_eq!(convert("hello world. again", "sentence", true), "Hello world. Again");
+ assert_eq!(convert("hello WORLD", "title", true), "Hello World");
+ assert_eq!(convert("Hello", "toggle", true), "hELLO");
+ assert_eq!(convert("straße", "upper", true), "STRASSE");
+ assert_eq!(convert("hello world", "title", false), "Hello World");
+ assert_eq!(convert("ΣΟΦΟΣ ΟΔΟΣ", "lower", true), "σοφος οδος");
+ }
+
+ #[test]
+ fn sentence_case_ignores_decimal_points() {
+ // #3485: A period between two digits is a decimal point, not a sentence boundary.
+ assert_eq!(convert("one 3.14 apples and two 2.5 pears.", "sentence", true), "One 3.14 apples and two 2.5 pears.");
+ assert_eq!(convert("pi is 3.14. next sentence.", "sentence", true), "Pi is 3.14. Next sentence.");
+ }
+}
diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs
index 2493d07f7..1a88ef96a 100644
--- a/crates/engine/src/cmd/insert.rs
+++ b/crates/engine/src/cmd/insert.rs
@@ -979,6 +979,73 @@ fn cover_page(s: &mut Session, v: &Value) -> CmdResult {
sel_result(s)
}
+/// Why Insert › Drop Cap is off at the caret, as in Word: in a table cell, in an empty paragraph
+/// and in one that starts with a space.
+fn drop_cap_off(s: &Session) -> Option<&'static str> {
+ if s.sel.focus.path.cell().is_some() {
+ return Some("drop caps can't go in a table");
+ }
+ let first = s.doc.para_at(&s.sel.focus).and_then(|p| p.text.chars().next());
+ match first {
+ None => Some("the paragraph is empty"),
+ Some(c) if c.is_whitespace() => Some("the paragraph starts with a space"),
+ Some(_) => None,
+ }
+}
+
+/// Insert › Drop Cap and Drop Cap Options: none, or the letter dropped into the text or hung in
+/// the margin, `lines` tall, `distance` points from the text, in `font` (unset: the text's).
+/// None takes all of it away, the letter's font with it.
+fn drop_cap(s: &mut Session, v: &Value) -> CmdResult {
+ let lines = v.get("lines").and_then(Value::as_f64).filter(|l| l.is_finite());
+ let none = p::str(v, "position") == Some("none") || lines == Some(0.0);
+ let margin = p::str(v, "position") == Some("margin");
+ let lines = lines.map_or(3.0, |l| l.round().clamp(1.0, 10.0)) as u8;
+ let distance = p::f32(v, "distance").map_or(0.0, |d| d.clamp(0.0, wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE));
+ // A family name, not a paragraph of text.
+ let font = p::str(v, "font").map(str::trim).filter(|f| !f.is_empty()).map(|f| f.chars().take(64).collect::());
+ super::para::fmt(s, &|p| {
+ p.drop_cap = (!none).then_some(lines);
+ p.drop_cap_margin = (!none && margin).then_some(true);
+ p.drop_cap_distance = (!none && distance > 0.0).then_some(distance);
+ p.drop_cap_font = font.clone().filter(|_| !none);
+ })
+}
+
+/// Insert › Drop Cap at the caret: the menu's highlight and the Drop Cap dialog's values.
+#[derive(Clone, Debug, PartialEq)]
+pub struct DropCapState {
+ /// `none`, `dropped` or `margin`.
+ pub position: &'static str,
+ /// Lines to drop (3 when there is no drop cap).
+ pub lines: u8,
+ /// Distance from the text, points.
+ pub distance: f32,
+ /// The letter's own font, if it has one.
+ pub font: Option,
+ /// The font of the paragraph's first character.
+ pub text_font: String,
+}
+
+pub fn drop_cap_state(s: &Session) -> DropCapState {
+ let para = s.doc.para_at(&s.sel.focus);
+ let props = para.map(|p| p.props.clone()).unwrap_or_default();
+ let rp = s.doc.styles.resolve_para(&props);
+ let first = para.map(|p| p.props_at(0).clone()).unwrap_or_default();
+ let text_font = s.doc.styles.resolve_char_in(props.style.as_deref(), None, &first).font;
+ DropCapState {
+ position: match (rp.drop_cap, rp.drop_cap_margin) {
+ (0, _) => "none",
+ (_, true) => "margin",
+ _ => "dropped",
+ },
+ lines: if rp.drop_cap == 0 { 3 } else { rp.drop_cap },
+ distance: if rp.drop_cap == 0 { 0.0 } else { rp.drop_cap_distance },
+ font: rp.drop_cap_font.filter(|_| rp.drop_cap > 0),
+ text_font,
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1309,69 +1376,3 @@ mod tests {
}
}
-/// Why Insert › Drop Cap is off at the caret, as in Word: in a table cell, in an empty paragraph
-/// and in one that starts with a space.
-fn drop_cap_off(s: &Session) -> Option<&'static str> {
- if s.sel.focus.path.cell().is_some() {
- return Some("drop caps can't go in a table");
- }
- let first = s.doc.para_at(&s.sel.focus).and_then(|p| p.text.chars().next());
- match first {
- None => Some("the paragraph is empty"),
- Some(c) if c.is_whitespace() => Some("the paragraph starts with a space"),
- Some(_) => None,
- }
-}
-
-/// Insert › Drop Cap and Drop Cap Options: none, or the letter dropped into the text or hung in
-/// the margin, `lines` tall, `distance` points from the text, in `font` (unset: the text's).
-/// None takes all of it away, the letter's font with it.
-fn drop_cap(s: &mut Session, v: &Value) -> CmdResult {
- let lines = v.get("lines").and_then(Value::as_f64).filter(|l| l.is_finite());
- let none = p::str(v, "position") == Some("none") || lines == Some(0.0);
- let margin = p::str(v, "position") == Some("margin");
- let lines = lines.map_or(3.0, |l| l.round().clamp(1.0, 10.0)) as u8;
- let distance = p::f32(v, "distance").map_or(0.0, |d| d.clamp(0.0, wordcraft_doc::resolve::MAX_DROP_CAP_DISTANCE));
- // A family name, not a paragraph of text.
- let font = p::str(v, "font").map(str::trim).filter(|f| !f.is_empty()).map(|f| f.chars().take(64).collect::());
- super::para::fmt(s, &|p| {
- p.drop_cap = (!none).then_some(lines);
- p.drop_cap_margin = (!none && margin).then_some(true);
- p.drop_cap_distance = (!none && distance > 0.0).then_some(distance);
- p.drop_cap_font = font.clone().filter(|_| !none);
- })
-}
-
-/// Insert › Drop Cap at the caret: the menu's highlight and the Drop Cap dialog's values.
-#[derive(Clone, Debug, PartialEq)]
-pub struct DropCapState {
- /// `none`, `dropped` or `margin`.
- pub position: &'static str,
- /// Lines to drop (3 when there is no drop cap).
- pub lines: u8,
- /// Distance from the text, points.
- pub distance: f32,
- /// The letter's own font, if it has one.
- pub font: Option,
- /// The font of the paragraph's first character.
- pub text_font: String,
-}
-
-pub fn drop_cap_state(s: &Session) -> DropCapState {
- let para = s.doc.para_at(&s.sel.focus);
- let props = para.map(|p| p.props.clone()).unwrap_or_default();
- let rp = s.doc.styles.resolve_para(&props);
- let first = para.map(|p| p.props_at(0).clone()).unwrap_or_default();
- let text_font = s.doc.styles.resolve_char_in(props.style.as_deref(), None, &first).font;
- DropCapState {
- position: match (rp.drop_cap, rp.drop_cap_margin) {
- (0, _) => "none",
- (_, true) => "margin",
- _ => "dropped",
- },
- lines: if rp.drop_cap == 0 { 3 } else { rp.drop_cap },
- distance: if rp.drop_cap == 0 { 0.0 } else { rp.drop_cap_distance },
- font: rp.drop_cap_font.filter(|_| rp.drop_cap > 0),
- text_font,
- }
-}
diff --git a/crates/engine/src/cmd/mailings.rs b/crates/engine/src/cmd/mailings.rs
index 2d6e807d0..ac5d8719e 100644
--- a/crates/engine/src/cmd/mailings.rs
+++ b/crates/engine/src/cmd/mailings.rs
@@ -837,7 +837,7 @@ fn condition<'a>(s: &Session, t: &'a str, row: usize) -> (bool, Vec<&'a str>) {
let inner = t.split_once('{').and_then(|(_, r)| r.split('}').next()).unwrap_or("");
let field = mergefield_name(inner).unwrap_or_default();
let rest = t.split_once('}').map(|(_, r)| r).unwrap_or("");
- let op: String = rest.trim_start().chars().take_while(|c| matches!(c, '=' | '<' | '>')).collect();
+ let op: String = rest.trim_start().chars().take_while(|c| matches!(c, '<'..='>')).collect();
let mut quoted = rest.split('"').skip(1).step_by(2);
let cmp = quoted.next().unwrap_or("");
let v = value(s, row, &field);
diff --git a/crates/engine/src/cmd/table_formula.rs b/crates/engine/src/cmd/table_formula.rs
index 4550b4ece..378a87fa6 100644
--- a/crates/engine/src/cmd/table_formula.rs
+++ b/crates/engine/src/cmd/table_formula.rs
@@ -228,6 +228,50 @@ enum Tok {
Cmp(&'static str),
}
+fn parse_num_token(chars: &mut std::iter::Peekable>) -> Result {
+ let mut lit = String::new();
+ while let Some(&d) = chars.peek() {
+ if d.is_ascii_digit() || d == '.' {
+ lit.push(d);
+ chars.next();
+ } else {
+ break;
+ }
+ }
+ let x = lit.parse::().map_err(|_| syntax(format!("`{lit}` isn't a number")))?;
+ Ok(Tok::Num(x))
+}
+
+fn parse_name_token(chars: &mut std::iter::Peekable>) -> Tok {
+ let mut name = String::new();
+ while let Some(&d) = chars.peek() {
+ if d.is_ascii_alphanumeric() {
+ name.push(d.to_ascii_uppercase());
+ chars.next();
+ } else {
+ break;
+ }
+ }
+ Tok::Name(name)
+}
+
+fn parse_cmp_token(ch: char, chars: &mut std::iter::Peekable>) -> Tok {
+ chars.next();
+ let next = chars.peek().copied();
+ let (op, longer) = match (ch, next) {
+ ('<', Some('=')) => ("<=", true),
+ ('<', Some('>')) => ("<>", true),
+ ('>', Some('=')) => (">=", true),
+ ('<', _) => ("<", false),
+ ('>', _) => (">", false),
+ _ => ("=", false),
+ };
+ if longer {
+ chars.next();
+ }
+ Tok::Cmp(op)
+}
+
fn tokenize(s: &str) -> Result, CmdError> {
let mut out = Vec::new();
let mut chars = s.chars().peekable();
@@ -235,46 +279,14 @@ fn tokenize(s: &str) -> Result, CmdError> {
if ch.is_whitespace() {
chars.next();
} else if ch.is_ascii_digit() || ch == '.' {
- let mut lit = String::new();
- while let Some(&d) = chars.peek() {
- if d.is_ascii_digit() || d == '.' {
- lit.push(d);
- chars.next();
- } else {
- break;
- }
- }
- let x = lit.parse::().map_err(|_| syntax(format!("`{lit}` isn't a number")))?;
- out.push(Tok::Num(x));
+ out.push(parse_num_token(&mut chars)?);
} else if ch.is_ascii_alphabetic() {
- let mut name = String::new();
- while let Some(&d) = chars.peek() {
- if d.is_ascii_alphanumeric() {
- name.push(d.to_ascii_uppercase());
- chars.next();
- } else {
- break;
- }
- }
- out.push(Tok::Name(name));
+ out.push(parse_name_token(&mut chars));
} else if matches!(ch, '+' | '-' | '*' | '/' | '^' | '(' | ')' | ',' | ':') {
out.push(Tok::Op(ch));
chars.next();
- } else if matches!(ch, '<' | '>' | '=') {
- chars.next();
- let next = chars.peek().copied();
- let (op, longer) = match (ch, next) {
- ('<', Some('=')) => ("<=", true),
- ('<', Some('>')) => ("<>", true),
- ('>', Some('=')) => (">=", true),
- ('<', _) => ("<", false),
- ('>', _) => (">", false),
- _ => ("=", false),
- };
- if longer {
- chars.next();
- }
- out.push(Tok::Cmp(op));
+ } else if matches!(ch, '<'..='>') {
+ out.push(parse_cmp_token(ch, &mut chars));
} else {
return Err(syntax(format!("`{ch}` isn't part of a formula")));
}
@@ -468,9 +480,9 @@ impl Parser {
/// `B4` → (row 3, column 1): the row counts from 1 and the column from A.
fn cell_ref(name: &str) -> Option<(usize, usize)> {
- let letters: String = name.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
+ let letters: String = name.chars().take_while(char::is_ascii_alphabetic).collect();
let digits = name.get(letters.len()..)?;
- if letters.is_empty() || digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
+ if letters.is_empty() || digits.is_empty() || !digits.chars().all(char::is_ascii_digit) {
return None;
}
let col = letters.bytes().try_fold(0usize, |acc, b| acc.checked_mul(26)?.checked_add((b.to_ascii_uppercase() - b'A') as usize + 1))? - 1;
diff --git a/crates/engine/src/cmd/tools.rs b/crates/engine/src/cmd/tools.rs
index 3403cae1f..35d2c1ea5 100644
--- a/crates/engine/src/cmd/tools.rs
+++ b/crates/engine/src/cmd/tools.rs
@@ -196,7 +196,7 @@ impl AutoCorrectPrefs {
.filter(|(a, _)| self.fractions || !FRACTIONS.contains(a))
.find(|(a, _)| {
*a == word
- || (a.chars().all(|c| c.is_lowercase()) && word.to_lowercase() == *a && word.chars().next().is_some_and(char::is_uppercase))
+ || (a.chars().all(char::is_lowercase) && word.to_lowercase() == *a && word.chars().next().is_some_and(char::is_uppercase))
})
.map(|(a, b)| {
if *a != word && b.chars().next().is_some_and(char::is_lowercase) {
@@ -249,7 +249,7 @@ impl AutoCorrectPrefs {
let mut v: Vec = NOT_SENTENCE_END
.iter()
.filter(|a| !self.exceptions_removed.iter().any(|r| r.eq_ignore_ascii_case(a)))
- .map(|a| a.to_string())
+ .map(ToString::to_string)
.chain(self.exceptions.iter().map(|a| a.to_lowercase()))
.collect();
v.sort();
diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs
index fb9999c74..071cb48f5 100644
--- a/crates/engine/src/session.rs
+++ b/crates/engine/src/session.rs
@@ -1519,7 +1519,7 @@ impl Session {
let mut result = match result {
Ok(r) => r,
Err(p) => {
- let msg = p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::().cloned()).unwrap_or_default();
+ let msg = p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::().cloned()).unwrap_or_default();
Err(CmdError::Failed(format!("{id} panicked: {msg}")))
}
};
diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs
index 03ebb9a0f..f5564359d 100644
--- a/crates/fonts/src/fontdb.rs
+++ b/crates/fonts/src/fontdb.rs
@@ -99,7 +99,7 @@ pub struct FaceRef(&'static FontFace);
impl FaceRef {
/// The handle for a loaded face (memoised per face).
pub fn of(face: &Arc) -> FaceRef {
- static LEAKED: RwLock>> = RwLock::new(Vec::new());
+ static LEAKED: RwLock>> = RwLock::new(Vec::new());
let id = face.id as usize;
if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) {
return FaceRef(f);
@@ -933,7 +933,7 @@ pub fn base_style(style: &str) -> &str {
}
fn norm(s: &str) -> String {
- s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect()
+ s.chars().filter(char::is_alphanumeric).flat_map(char::to_lowercase).collect()
}
/// Weight implied by a style name.
diff --git a/crates/formats/src/html.rs b/crates/formats/src/html.rs
index 8ec9f31cb..fa6ec5799 100644
--- a/crates/formats/src/html.rs
+++ b/crates/formats/src/html.rs
@@ -366,9 +366,8 @@ impl Lexer<'_> {
self.pos += after_tag;
self.skip_past(">");
return;
- } else {
- self.pos += k + 2;
}
+ self.pos += k + 2;
}
}
@@ -408,7 +407,7 @@ impl Lexer<'_> {
self.skip_past(">");
return Some(Tok::End(name));
}
- if !b.get(1).is_some_and(|c| c.is_ascii_alphabetic()) {
+ if !b.get(1).is_some_and(u8::is_ascii_alphabetic) {
self.pos += 1;
return Some(Tok::Text("<".into()));
}
@@ -419,7 +418,7 @@ impl Lexer<'_> {
let mut attrs = Vec::new();
let mut self_close = false;
loop {
- while b.get(i).is_some_and(|c| c.is_ascii_whitespace()) {
+ while b.get(i).is_some_and(u8::is_ascii_whitespace) {
i += 1;
}
match b.get(i) {
@@ -448,13 +447,13 @@ impl Lexer<'_> {
i += 1;
continue;
}
- while b.get(i).is_some_and(|c| c.is_ascii_whitespace()) {
+ while b.get(i).is_some_and(u8::is_ascii_whitespace) {
i += 1;
}
let mut val = String::new();
if b.get(i) == Some(&b'=') {
i += 1;
- while b.get(i).is_some_and(|c| c.is_ascii_whitespace()) {
+ while b.get(i).is_some_and(u8::is_ascii_whitespace) {
i += 1;
}
match b.get(i) {
diff --git a/crates/formats/src/latex.rs b/crates/formats/src/latex.rs
index bc0dd3133..ecb171c08 100644
--- a/crates/formats/src/latex.rs
+++ b/crates/formats/src/latex.rs
@@ -778,13 +778,13 @@ fn linear_to_latex_at(linear: &str, depth: usize) -> String {
o.push('\\');
o.push_str(&name);
i += 1 + name.len();
- if chars.get(i).is_some_and(|n| n.is_ascii_alphabetic()) {
+ if chars.get(i).is_some_and(char::is_ascii_alphabetic) {
o.push(' ');
}
}
continue;
}
- '%' | '#' | '&' | '$' => {
+ '#'..='&' => {
o.push('\\');
o.push(c);
}
@@ -796,7 +796,7 @@ fn linear_to_latex_at(linear: &str, depth: usize) -> String {
o.push('\\');
o.push_str(name);
// A control word needs a space only before a letter.
- if chars.get(i + 1).is_some_and(|n| n.is_ascii_alphabetic()) {
+ if chars.get(i + 1).is_some_and(char::is_ascii_alphabetic) {
o.push(' ');
}
}
diff --git a/crates/formats/src/markdown.rs b/crates/formats/src/markdown.rs
index 000b18aac..bf1987701 100644
--- a/crates/formats/src/markdown.rs
+++ b/crates/formats/src/markdown.rs
@@ -268,7 +268,7 @@ fn list_marker(t: &str) -> Option {
let (m, len) = if matches!(first, '-' | '+' | '*') {
(Marker { ordered: false, ch: first, len: 1, start: 1 }, 1)
} else {
- let digits = t.chars().take_while(|c| c.is_ascii_digit()).count();
+ let digits = t.chars().take_while(char::is_ascii_digit).count();
if !(1..=9).contains(&digits) {
return None;
}
@@ -607,7 +607,7 @@ fn parse_list(lines: &[String], mut i: usize, depth: usize, level: u8, out: &mut
i += 1;
}
// Blank lines at the end of an item belong between items.
- while item.len() > 1 && item.last().is_some_and(|x| x.is_empty()) {
+ while item.len() > 1 && item.last().is_some_and(String::is_empty) {
item.pop();
}
let li = ListInfo { ordered: first.ordered, level: level.min(8), start: first.start };
@@ -833,7 +833,7 @@ fn unescape(s: &str) -> String {
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\'
- && let Some(n) = chars.peek().copied().filter(|n| n.is_ascii_punctuation())
+ && let Some(n) = chars.peek().copied().filter(char::is_ascii_punctuation)
{
out.push(n);
chars.next();
@@ -852,7 +852,7 @@ fn tag_attr(tag: &str, name: &str) -> Option {
let at = from + k;
let before = lower.get(..at)?.chars().next_back();
let after = lower.get(at + name.len()..)?.trim_start();
- if before.is_some_and(|c| c.is_whitespace()) && after.starts_with('=') {
+ if before.is_some_and(char::is_whitespace) && after.starts_with('=') {
let v = after.get(1..)?.trim_start();
let q = v.chars().next()?;
return Some(if q == '"' || q == '\'' {
@@ -1257,7 +1257,7 @@ fn escape_line_start(s: String) -> String {
|| (t.starts_with('-') && t.trim_end_matches([' ', '\t']).chars().all(|c| c == '-'))
|| thematic_break(t)
|| {
- let d = t.chars().take_while(|c| c.is_ascii_digit()).count();
+ let d = t.chars().take_while(char::is_ascii_digit).count();
d > 0
&& t.get(d..).is_some_and(|r| {
r.starts_with(". ") || r.starts_with(") ") || r.starts_with(".\t") || r.starts_with(")\t") || r == "." || r == ")"
diff --git a/crates/formats/src/mathml.rs b/crates/formats/src/mathml.rs
index 43232e995..af362c9fe 100644
--- a/crates/formats/src/mathml.rs
+++ b/crates/formats/src/mathml.rs
@@ -88,10 +88,10 @@ fn run(r: &MRun, o: &mut String, eq_arr: bool) {
let cs: Vec = r.text.chars().collect();
let mut i = 0;
while let Some(&c) = cs.get(i) {
- if c.is_ascii_digit() || (c == '.' && cs.get(i + 1).is_some_and(|d| d.is_ascii_digit())) {
+ if c.is_ascii_digit() || (c == '.' && cs.get(i + 1).is_some_and(char::is_ascii_digit)) {
let mut num = String::new();
while let Some(&d) =
- cs.get(i).filter(|d| d.is_ascii_digit() || **d == '.' || **d == ',' && cs.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
+ cs.get(i).filter(|d| d.is_ascii_digit() || **d == '.' || **d == ',' && cs.get(i + 1).is_some_and(char::is_ascii_digit))
{
num.push(d);
i += 1;
diff --git a/crates/formats/src/odt/page_parts.rs b/crates/formats/src/odt/page_parts.rs
index 0a142590e..967be4094 100644
--- a/crates/formats/src/odt/page_parts.rs
+++ b/crates/formats/src/odt/page_parts.rs
@@ -4,7 +4,7 @@ use std::collections::HashSet;
use wordcraft_doc::{Part, PartKind};
-use super::*;
+use super::{events, get, model, Body, Ev, FBlock, Pages, SectionProps, Styles};
/// Most XML events kept for all headers and footers together; a slot past it is left out.
const MAX_EVENTS: usize = 1 << 20;
diff --git a/crates/formats/src/rtf.rs b/crates/formats/src/rtf.rs
index 45479cca6..eda8bb65c 100644
--- a/crates/formats/src/rtf.rs
+++ b/crates/formats/src/rtf.rs
@@ -2638,7 +2638,7 @@ fn parse_full(bytes: &[u8]) -> Result {
let Some(&n) = bytes.get(i + 1) else { break };
if n.is_ascii_alphabetic() {
let mut j = i + 1;
- while bytes.get(j).is_some_and(|b| b.is_ascii_alphabetic()) && j - i <= 32 {
+ while bytes.get(j).is_some_and(u8::is_ascii_alphabetic) && j - i <= 32 {
j += 1;
}
let word = std::str::from_utf8(bytes.get(i + 1..j).unwrap_or(b"")).unwrap_or("");
@@ -2648,7 +2648,7 @@ fn parse_full(bytes: &[u8]) -> Result {
k += 1;
}
let d0 = k;
- while bytes.get(k).is_some_and(|b| b.is_ascii_digit()) && k - d0 < 10 {
+ while bytes.get(k).is_some_and(u8::is_ascii_digit) && k - d0 < 10 {
k += 1;
}
let param = if k > d0 {
diff --git a/crates/formats/src/sheet.rs b/crates/formats/src/sheet.rs
index 1689b500b..49cc1259f 100644
--- a/crates/formats/src/sheet.rs
+++ b/crates/formats/src/sheet.rs
@@ -788,7 +788,7 @@ fn iso_datetime(v: &str) -> Option {
let y: i64 = it.next()?.parse().ok()?;
let m: u32 = it.next()?.parse().ok()?;
let d_part = it.next()?;
- let d_digits: String = d_part.chars().take_while(|c| c.is_ascii_digit()).collect();
+ let d_digits: String = d_part.chars().take_while(char::is_ascii_digit).collect();
let d: u32 = d_digits.parse().ok()?;
if !(1..=12).contains(&m) || !(1..=31).contains(&d) {
return None;
diff --git a/crates/proof/src/it/grammar.rs b/crates/proof/src/it/grammar.rs
index 235012bdd..463029369 100644
--- a/crates/proof/src/it/grammar.rs
+++ b/crates/proof/src/it/grammar.rs
@@ -276,7 +276,7 @@ fn e_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec) {
}
let mut after = text.get(*b..).unwrap_or("").chars();
let Some(ap) = after.next().filter(|&c| is_apostrophe(c)) else { continue };
- let ends = after.next().is_none_or(|c| c.is_whitespace());
+ let ends = after.next().is_none_or(char::is_whitespace);
let quoted = text.get(..*a).and_then(|t| t.chars().next_back()).is_some_and(|c| is_apostrophe(c) || c == '‘');
if ends && !quoted {
v.push(issue(*a, *b + ap.len_utf8(), "«È» si scrive con l'accento, non con l'apostrofo".into(), vec!["È".into()]));
diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs
index 0ab16529d..38f274c75 100644
--- a/crates/proof/src/lib.rs
+++ b/crates/proof/src/lib.rs
@@ -320,7 +320,7 @@ const COMMON: &str = "a an the and or but if of to in on at by for with from as
const EXTRA_EN: &str = include_str!("../../../assets/spelling/en-extra.txt");
fn extra_words() -> &'static HashSet<&'static str> {
- static E: OnceLock> = OnceLock::new();
+ static E: OnceLock> = OnceLock::new();
E.get_or_init(|| EXTRA_EN.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')).collect())
}
@@ -889,7 +889,7 @@ fn grammar_en(text: &str, capital_first: bool) -> Vec {
if gap == " "
&& (w0 == "an" || w0 == "An")
&& w1.chars().next().is_some_and(|c| c.is_alphabetic() && !"aeiouAEIOUhH".contains(c))
- && !w1.chars().all(|c| c.is_uppercase())
+ && !w1.chars().all(char::is_uppercase)
{
v.push(Issue {
start: *a0,
diff --git a/crates/proof/src/ru/mod.rs b/crates/proof/src/ru/mod.rs
index c29bbc7c5..50faac380 100644
--- a/crates/proof/src/ru/mod.rs
+++ b/crates/proof/src/ru/mod.rs
@@ -109,7 +109,7 @@ pub fn has_dictionary() -> bool {
/// Is `c` a letter of the Cyrillic script (the only one the Russian dictionary covers)?
pub(crate) fn is_cyrillic(c: char) -> bool {
- matches!(c, '\u{0400}'..='\u{04FF}' | '\u{0500}'..='\u{052F}')
+ matches!(c, '\u{0400}'..='\u{052F}')
}
/// Particles joined to a word with a hyphen (`кто-то`, `где-нибудь`, `давай-ка`, `всё-таки`,
diff --git a/crates/render/src/cache_tests.rs b/crates/render/src/cache_tests.rs
index b34ba52c4..3282f2f0e 100644
--- a/crates/render/src/cache_tests.rs
+++ b/crates/render/src/cache_tests.rs
@@ -1,6 +1,13 @@
//! Decoded-media cache: hits need the same allocation, and the byte budget bounds what is kept.
-use super::*;
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use wordcraft_doc::Document;
+use wordcraft_doc::props::Rgb;
+use wordcraft_layout::display::{Draw, Stroke};
+
+use super::{CACHE_BYTES, MediaCache, RenderOptions, render_items, wave_path};
#[test]
fn media_cache_only_hits_the_same_allocation() {
diff --git a/crates/render/src/tests_color_glyphs.rs b/crates/render/src/tests_color_glyphs.rs
index c1d38a7a8..163a5af15 100644
--- a/crates/render/src/tests_color_glyphs.rs
+++ b/crates/render/src/tests_color_glyphs.rs
@@ -1,9 +1,12 @@
//! Colour glyphs (#1269): emoji from colour fonts (bitmap strikes, COLR) are drawn in colour.
-use super::*;
-use wordcraft_layout::display::page_display;
+use wordcraft_doc::Document;
+use wordcraft_fonts::FontDb;
+use wordcraft_layout::display::{DisplayOptions, Draw, page_display};
use wordcraft_layout::{LayoutCache, LayoutOptions, layout};
+use super::{RenderOptions, Rendered, render_page};
+
/// Colour emoji families the system fallback may pick.
const COLOR_EMOJI: &[&str] = &["Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji"];
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
index 0709aca18..2135b5003 100644
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -1250,7 +1250,7 @@ impl Dialog {
filter: RecipientFilter::from_session(m),
}
} else {
- let fields: Vec = NEW_LIST_FIELDS.iter().map(|f| f.to_string()).collect();
+ let fields: Vec = NEW_LIST_FIELDS.iter().map(ToString::to_string).collect();
let rows = vec![vec![String::new(); fields.len()]];
Dialog::RecipientList { fields, rows, message: String::new(), filter: RecipientFilter::default() }
}
diff --git a/crates/ui-egui/src/dialogs_refs.rs b/crates/ui-egui/src/dialogs_refs.rs
index a6f49b5bf..0dfccb4e3 100644
--- a/crates/ui-egui/src/dialogs_refs.rs
+++ b/crates/ui-egui/src/dialogs_refs.rs
@@ -242,7 +242,7 @@ pub struct CaptionForm {
impl CaptionForm {
pub fn read(app: &WordApp) -> CaptionForm {
- let mut labels: Vec = BUILT_IN_LABELS.iter().map(|l| l.to_string()).collect();
+ let mut labels: Vec = BUILT_IN_LABELS.iter().map(ToString::to_string).collect();
for l in &app.ui.caption_labels {
if !labels.contains(l) {
labels.push(l.clone());
@@ -937,7 +937,7 @@ pub struct FiguresForm {
impl FiguresForm {
pub fn read(app: &WordApp) -> FiguresForm {
- let mut labels: Vec = BUILT_IN_LABELS.iter().map(|l| l.to_string()).collect();
+ let mut labels: Vec = BUILT_IN_LABELS.iter().map(ToString::to_string).collect();
for l in app.ui.caption_labels.iter().cloned().chain(wordcraft_engine::cmd::citations::caption_labels(&app.session)) {
if !labels.contains(&l) {
labels.push(l);
diff --git a/packaging/env.sh b/packaging/env.sh
index 87f0c0395..dcf7e4e39 100644
--- a/packaging/env.sh
+++ b/packaging/env.sh
@@ -19,6 +19,7 @@ workspace_version() {
/^\[/ { in_pkg = ($0 == "[workspace.package]") ; next }
in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit }
' "$ROOT/Cargo.toml"
+ return 0
}
VERSION="${WORDCRAFT_VERSION:-$(workspace_version)}"
@@ -42,6 +43,7 @@ export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}"
# Emit a GitHub Actions warning (plain stderr outside Actions).
warn() {
if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi
+ return 0
}
# Copy licence and readme files that exist into a package directory.
@@ -51,6 +53,7 @@ copy_docs() {
if [[ -f "$ROOT/$f" ]]; then cp "$ROOT/$f" "$dest/"; fi
done
copy_font_licences "$dest"
+ return 0
}
# Builds made with craft-fonts (CRAFT_FONTS_DIR, set by the release workflow) embed its fonts, so
@@ -65,5 +68,7 @@ copy_font_licences() {
# Portable SHA-256 of a file (prints just the hash).
sha256() {
- if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi
+ local file="$1"
+ if command -v sha256sum >/dev/null; then sha256sum "$file" | cut -d' ' -f1; else shasum -a 256 "$file" | cut -d' ' -f1; fi
+ return 0
}
diff --git a/packaging/icons.sh b/packaging/icons.sh
index 368f7c5f1..c71136d8b 100755
--- a/packaging/icons.sh
+++ b/packaging/icons.sh
@@ -21,7 +21,11 @@ MAC="$TMP/mac.svg"
sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC"
grep -q 'viewBox="-62.136' "$MAC" || { echo "error: expected viewBox=\"0 0 512 512\" in $SVG" >&2; exit 1; }
-render() { resvg -w "$2" -h "$2" "$1" "$3" /dev/null; then
appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml"
fi
-has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
+has() { local item="$1"; case " $FORMATS " in *" $item "*) return 0 ;; *) return 1 ;; esac; }
# ---- .tar.gz ------------------------------------------------------------------------------------
if has tar; then
@@ -119,13 +119,15 @@ if has appimage; then
fi
# Downloads $1 to $3 once and checks it against SHA-256 $2, the cached copy again on every run.
fetch_pinned() {
- if [[ ! -x "$3" ]]; then
- curl --proto '=https' -fsSL -o "$3.download" "$1"
- printf '%s %s\n' "$2" "$3.download" | sha256sum -c -
- chmod 755 "$3.download"
- mv "$3.download" "$3"
+ local url="$1" expected_sha="$2" target="$3"
+ if [[ ! -x "$target" ]]; then
+ curl --proto '=https' -fsSL -o "$target.download" "$url"
+ printf '%s %s\n' "$expected_sha" "$target.download" | sha256sum -c -
+ chmod 755 "$target.download"
+ mv "$target.download" "$target"
fi
- printf '%s %s\n' "$2" "$3" | sha256sum -c -
+ printf '%s %s\n' "$expected_sha" "$target" | sha256sum -c -
+ return 0
}
APPDIR="$WORK/WordCraft.AppDir"
cp -R "$STAGE" "$APPDIR"
diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh
index e9f59e228..6e603bfec 100755
--- a/packaging/macos/package.sh
+++ b/packaging/macos/package.sh
@@ -143,18 +143,20 @@ rm -f "$DMG"
# Retries an hdiutil verb that attaches a device: on CI runners it fails now and then with
# "Resource busy". Usage: hdiutil_retry ; five tries with growing pauses.
hdiutil_retry() {
+ local verb="$1"
local attempt
for attempt in 1 2 3 4 5; do
if hdiutil "$@"; then
return 0
fi
if [[ "$attempt" = 5 ]]; then
- echo "error: hdiutil $1 failed 5 times" >&2
+ echo "error: hdiutil $verb failed 5 times" >&2
return 1
fi
- warn "hdiutil $1 failed (attempt $attempt of 5); retrying in $((attempt * 10))s"
+ warn "hdiutil $verb failed (attempt $attempt of 5); retrying in $((attempt * 10))s"
sleep $((attempt * 10))
done
+ return 1
}
# `hdiutil create -srcfolder` copies the staged files as they are. The previous
# `makehybrid -hfs` + `convert` gave every file and folder in the image a com.apple.FinderInfo