diff --git a/crates/engine/src/cmd/find.rs b/crates/engine/src/cmd/find.rs index a40e8da51..a0afcfbb6 100644 --- a/crates/engine/src/cmd/find.rs +++ b/crates/engine/src/cmd/find.rs @@ -323,18 +323,38 @@ fn replacement(p: &Value) -> String { if bool_or(p, "grep", false) { translate_replacement(c) } else { metachars(c) } } +/// Repair byte validity of a text selection once against the final story, without remapping valid offsets. +fn repair_selection(t: &mut designcraft_doc::TextSel, st: &designcraft_doc::Story) { + let range = t.range(); + let start = floor_char_boundary(&st.text, range.start); + let mut end = range.end.min(st.len()); + if t.is_caret() { + end = start; + } else if !st.text.is_char_boundary(end) { + let floor = floor_char_boundary(&st.text, end); + end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8()); + } + (t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) }; +} + +/// The character overrides a Change applies to every replacement, from its `attrs` object. +fn char_overrides(attrs: Option<&Value>) -> Result { + let mut over = designcraft_doc::CharAttrs::default(); + if let Some(o) = attrs.and_then(Value::as_object) { + for (k, v) in o { + over.set_json(k, v).map_err(|e| bad("find.change", e))?; + } + } + Ok(over) +} + fn change(s: &mut Session, p: &Value) -> Result { let re = build(p)?; let grep = bool_or(p, "grep", false); let rep = replacement(p); let first_only = bool_or(p, "first", false); let attrs = p.get("attrs").cloned(); - let mut over = designcraft_doc::CharAttrs::default(); - if let Some(o) = attrs.as_ref().and_then(Value::as_object) { - for (k, v) in o { - over.set_json(k, v).map_err(|e| bad("find.change", e))?; - } - } + let over = char_overrides(attrs.as_ref())?; super::text::validate_char_fill(&s.doc()?.doc, &over, "find.change")?; let targets = scope(s, p)?.targets; s.edit(|d, sel| { @@ -361,17 +381,7 @@ fn change(s: &mut Session, p: &Value) -> Result { count += 1; } if let Some(t) = sel.text.as_mut().filter(|t| t.story == sid && t.cell == cell) { - // Repair byte validity once against the final story, without remapping valid offsets. - let range = t.range(); - let start = floor_char_boundary(&st.text, range.start); - let mut end = range.end.min(st.len()); - if t.is_caret() { - end = start; - } else if !st.text.is_char_boundary(end) { - let floor = floor_char_boundary(&st.text, end); - end = st.text.get(floor..).and_then(|rest| rest.chars().next()).map_or(floor, |ch| floor + ch.len_utf8()); - } - (t.anchor, t.focus) = if t.anchor <= t.focus { (start, end) } else { (end, start) }; + repair_selection(t, st); } // `first` is the first match of the whole search, not of each story. if first_only && count > 0 { diff --git a/crates/engine/src/cmd/fonts.rs b/crates/engine/src/cmd/fonts.rs index b1577400c..ee80eb914 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -81,36 +81,41 @@ fn list(d: &Document) -> Vec { used_fonts(d).into_iter().map(|font| json!(font)).collect() } -pub(super) fn used_fonts(d: &Document) -> Vec { - let db = designcraft_fonts::FontDb::global().scoped(d.font_scope); - let mut used: BTreeMap<(String, String), (usize, std::collections::BTreeSet)> = BTreeMap::new(); - for_each_story(d, &mut |st| { - let ranges = st.para_ranges(); - for (pi, r) in ranges.iter().enumerate() { - let (_, base) = d.styles.resolve_para(&st.paras[pi]); - for (rr, fmt) in st.runs() { - let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count(); - if n == 0 && !(r.is_empty() && rr.contains(&r.start)) { - continue; - } - let p = d.styles.resolve_char(&base, fmt); - if let Some(f) = d.styles.composite_font(&p.font_family) { - let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or(""); - for c in text.chars() { - if let Some(e) = f.entry(c) { - let entry = used.entry((e.family.clone(), e.style.clone())).or_default(); - entry.0 += 1; - entry.1.insert(c); - } +type FontTally = BTreeMap<(String, String), (usize, std::collections::BTreeSet)>; + +/// Count the characters of every run of one story against the font each resolves to. +fn tally_story(d: &Document, st: &Story, used: &mut FontTally) { + let ranges = st.para_ranges(); + for (pi, r) in ranges.iter().enumerate() { + let (_, base) = d.styles.resolve_para(&st.paras[pi]); + for (rr, fmt) in st.runs() { + let n = st.text[rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))].chars().count(); + if n == 0 && !(r.is_empty() && rr.contains(&r.start)) { + continue; + } + let p = d.styles.resolve_char(&base, fmt); + if let Some(f) = d.styles.composite_font(&p.font_family) { + let text = st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or(""); + for c in text.chars() { + if let Some(e) = f.entry(c) { + let entry = used.entry((e.family.clone(), e.style.clone())).or_default(); + entry.0 += 1; + entry.1.insert(c); } - } else { - let entry = used.entry((p.font_family, p.font_style)).or_default(); - entry.0 += n; - entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars()); } + } else { + let entry = used.entry((p.font_family, p.font_style)).or_default(); + entry.0 += n; + entry.1.extend(st.text.get(rr.start.max(r.start)..rr.end.min(r.end).max(rr.start.max(r.start))).unwrap_or("").chars()); } } - }); + } +} + +pub(super) fn used_fonts(d: &Document) -> Vec { + let db = designcraft_fonts::FontDb::global().scoped(d.font_scope); + let mut used = FontTally::new(); + for_each_story(d, &mut |st| tally_story(d, st, &mut used)); let mut out: Vec = used .into_iter() .map(|((family, style), (n, characters))| {