mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
chore: resolve SonarCloud maintainability issues (#4083)
* chore: resolve SonarCloud maintainability issues * fix: restore compiling refactors, revert behaviour changes, run cargo fmt - fontdb::norm: keep the closure for filter (char::is_alphanumeric takes self, not &self, so it does not fit Iterator::filter). - story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change). - Revert the viewport meta change and the return 0 added to workspace_version/sha256, which changed behaviour or masked failures. - cargo fmt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Import Document in odt page_parts after dropping glob re-export * fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures * fix(engine): pass case helpers by value (clippy needless borrows) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
co-authored by
Claude Opus 5.5
Dylan Taylor
parent
c524027f34
commit
47edcd2ff2
@@ -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() {}
|
||||
}
|
||||
|
||||
+26
-24
@@ -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<String> = self.0.iter().map(|i| i.to_string()).collect();
|
||||
let s: Vec<String> = 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()));
|
||||
|
||||
@@ -47,7 +47,7 @@ fn tokenize(s: &str) -> Vec<Tok> {
|
||||
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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn measure(s: &str, per_pt: f32) -> Option<f32> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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("</w:sdtContent></w:sdt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:p>");
|
||||
w.raw(&run);
|
||||
w.raw("</w:p>");
|
||||
} else {
|
||||
w.raw("<w:p/>");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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("</w:sdtContent></w:sdt>");
|
||||
}
|
||||
}
|
||||
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:p>");
|
||||
w.raw(&run);
|
||||
w.raw("</w:p>");
|
||||
} else {
|
||||
w.raw("<w:p/>");
|
||||
}
|
||||
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(&mut 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);
|
||||
|
||||
@@ -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::<Vec<_>>().join(", ");
|
||||
let list = pages.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
|
||||
out.push(if kind.starts_with("INDEX") { (format!("{e}, {list}"), true) } else { (format!("{e}\t{list}"), false) });
|
||||
}
|
||||
out
|
||||
|
||||
@@ -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<String> = 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 ContentControl, v: &Value) -> Option<String> {
|
||||
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<String> = 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 };
|
||||
|
||||
@@ -601,6 +601,42 @@ 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 +670,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,28 +766,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).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -884,3 +872,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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<String>());
|
||||
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<String>,
|
||||
/// 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::*;
|
||||
@@ -1308,70 +1375,3 @@ mod tests {
|
||||
assert_eq!(date_picture("DATE"), None);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::<String>());
|
||||
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<String>,
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -228,6 +228,50 @@ enum Tok {
|
||||
Cmp(&'static str),
|
||||
}
|
||||
|
||||
fn parse_num_token(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Result<Tok, CmdError> {
|
||||
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::<f64>().map_err(|_| syntax(format!("`{lit}` isn't a number")))?;
|
||||
Ok(Tok::Num(x))
|
||||
}
|
||||
|
||||
fn parse_name_token(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> 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<std::str::Chars<'_>>) -> 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<Vec<Tok>, CmdError> {
|
||||
let mut out = Vec::new();
|
||||
let mut chars = s.chars().peekable();
|
||||
@@ -235,46 +279,14 @@ fn tokenize(s: &str) -> Result<Vec<Tok>, 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::<f64>().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,7 +480,7 @@ 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()) {
|
||||
return None;
|
||||
|
||||
@@ -195,8 +195,7 @@ impl AutoCorrectPrefs {
|
||||
.filter(|(a, _)| !self.removed.iter().any(|r| r == a) && !self.entries.iter().any(|(e, _)| e == a))
|
||||
.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 == word || (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 +248,7 @@ impl AutoCorrectPrefs {
|
||||
let mut v: Vec<String> = 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();
|
||||
|
||||
@@ -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::<String>().cloned()).unwrap_or_default();
|
||||
let msg = p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||
Err(CmdError::Failed(format!("{id} panicked: {msg}")))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<FontFace>) -> FaceRef {
|
||||
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = RwLock::new(Vec::new());
|
||||
static LEAKED: RwLock<Vec<Option<&FontFace>>> = 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(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect()
|
||||
}
|
||||
|
||||
/// Weight implied by a style name.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ fn list_marker(t: &str) -> Option<Marker> {
|
||||
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<String> {
|
||||
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 == ")"
|
||||
|
||||
@@ -88,10 +88,10 @@ fn run(r: &MRun, o: &mut String, eq_arr: bool) {
|
||||
let cs: Vec<char> = 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;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//! reader (#863).
|
||||
use std::collections::HashSet;
|
||||
|
||||
use wordcraft_doc::{Part, PartKind};
|
||||
use wordcraft_doc::{Document, Part, PartKind};
|
||||
|
||||
use super::*;
|
||||
use super::{Body, Ev, FBlock, Pages, SectionProps, Styles, events, get, model};
|
||||
|
||||
/// Most XML events kept for all headers and footers together; a slot past it is left out.
|
||||
const MAX_EVENTS: usize = 1 << 20;
|
||||
|
||||
@@ -2638,7 +2638,7 @@ fn parse_full(bytes: &[u8]) -> Result<Parsed, String> {
|
||||
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<Parsed, String> {
|
||||
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 {
|
||||
|
||||
@@ -788,7 +788,7 @@ fn iso_datetime(v: &str) -> Option<String> {
|
||||
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;
|
||||
|
||||
@@ -276,7 +276,7 @@ fn e_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
|
||||
}
|
||||
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()]));
|
||||
|
||||
@@ -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<HashSet<&'static str>> = OnceLock::new();
|
||||
static E: OnceLock<HashSet<&str>> = 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<Issue> {
|
||||
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,
|
||||
|
||||
@@ -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 (`кто-то`, `где-нибудь`, `давай-ка`, `всё-таки`,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -1250,7 +1250,7 @@ impl Dialog {
|
||||
filter: RecipientFilter::from_session(m),
|
||||
}
|
||||
} else {
|
||||
let fields: Vec<String> = NEW_LIST_FIELDS.iter().map(|f| f.to_string()).collect();
|
||||
let fields: Vec<String> = 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() }
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ pub struct CaptionForm {
|
||||
|
||||
impl CaptionForm {
|
||||
pub fn read(app: &WordApp) -> CaptionForm {
|
||||
let mut labels: Vec<String> = BUILT_IN_LABELS.iter().map(|l| l.to_string()).collect();
|
||||
let mut labels: Vec<String> = 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<String> = BUILT_IN_LABELS.iter().map(|l| l.to_string()).collect();
|
||||
let mut labels: Vec<String> = 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);
|
||||
|
||||
+4
-1
@@ -42,6 +42,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 +52,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 +67,6 @@ 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
|
||||
}
|
||||
|
||||
+5
-1
@@ -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; }
|
||||
render() {
|
||||
local src="$1" size="$2" dst="$3"
|
||||
resvg -w "$size" -h "$size" "$src" "$dst" </dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
render "$SVG" 1024 "$DIR/wordcraft-1024.png"
|
||||
# Runtime window/Dock icon on macOS (embedded by apps/wordcraft); other platforms use hicolor 256.
|
||||
|
||||
@@ -75,7 +75,7 @@ if command -v appstreamcli >/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"
|
||||
|
||||
@@ -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 <verb> <args...>; 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
|
||||
|
||||
Reference in New Issue
Block a user