mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 17:12:52 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d41ee15a2 | ||
|
|
99d8a31a84 |
@@ -700,29 +700,15 @@ pub struct Word {
|
||||
pub repeated: Option<usize>,
|
||||
}
|
||||
|
||||
/// Longest language tag a word is checked in. BCP 47 tags in documents are a few bytes; a longer
|
||||
/// value from a file is no language we have a dictionary for, and copying it into every word of
|
||||
/// a paragraph would multiply it.
|
||||
const MAX_LANG_LEN: usize = 64;
|
||||
|
||||
/// The words of `p`, each with its run's language (`lang` on the run, else `default`). Words
|
||||
/// with digits are skipped; apostrophes inside a word belong to it; hyphenated words are checked
|
||||
/// part by part. A language over [`MAX_LANG_LEN`] counts as none.
|
||||
/// part by part.
|
||||
pub fn paragraph_words(p: &deckcraft_model::Paragraph, default: &str) -> Vec<Word> {
|
||||
let mut chars: Vec<(char, usize)> = vec![];
|
||||
for (ri, r) in p.runs.iter().enumerate() {
|
||||
chars.extend(r.content().chars().map(|c| (c, ri)));
|
||||
}
|
||||
let default = if default.len() > MAX_LANG_LEN { "" } else { default };
|
||||
let lang_of = |ri: usize| {
|
||||
p.runs
|
||||
.get(ri)
|
||||
.and_then(|r| r.props.lang.as_deref())
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| if l.len() > MAX_LANG_LEN { "" } else { l })
|
||||
.unwrap_or(default)
|
||||
.to_string()
|
||||
};
|
||||
let lang_of = |ri: usize| p.runs.get(ri).and_then(|r| r.props.lang.clone()).filter(|l| !l.is_empty()).unwrap_or_else(|| default.to_string());
|
||||
// Combining marks (decomposed accents) belong to the letter before them.
|
||||
let inside = |c: char| c.is_alphanumeric() || deckcraft_model::words::is_combining_mark(c) || c == '\'' || c == '’' || c == '-';
|
||||
let mut out = vec![];
|
||||
@@ -805,18 +791,15 @@ pub fn misspelt(p: &deckcraft_model::Paragraph, default: &str, ignore: &[String]
|
||||
.chain(repeated(p, default))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.fold((vec![], HashMap::new()), |(mut v, mut at): (Vec<Word>, HashMap<usize, usize>), w| {
|
||||
.fold(vec![], |mut v: Vec<Word>, w| {
|
||||
// A repeated misspelt word is reported once, as repeated.
|
||||
match at.get(&w.start).and_then(|&i| v.get_mut(i)) {
|
||||
Some(x) => *x = w,
|
||||
None => {
|
||||
at.insert(w.start, v.len());
|
||||
v.push(w);
|
||||
}
|
||||
if let Some(i) = v.iter().position(|x| x.start == w.start) {
|
||||
v[i] = w;
|
||||
} else {
|
||||
v.push(w);
|
||||
}
|
||||
(v, at)
|
||||
v
|
||||
})
|
||||
.0
|
||||
}
|
||||
|
||||
/// Use `d` for `lang` from now on in this process (a dictionary the app loaded itself).
|
||||
@@ -970,23 +953,6 @@ SFX D Y 1\nSFX D 0 ed/S .\n";
|
||||
assert_eq!(w, vec!["check", "done"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_paragraph_with_a_huge_language_is_checked_in_linear_work() {
|
||||
use deckcraft_model::{Paragraph, Run, RunProps};
|
||||
// 100,000 copies of one word under a 1 MB language value: every word after the first is
|
||||
// a repeat, and no word carries a copy of the value.
|
||||
let lang = "x".repeat(1 << 20);
|
||||
let p =
|
||||
Paragraph { runs: vec![Run::with("the ".repeat(100_000), RunProps { lang: Some(lang), ..Default::default() })], ..Default::default() };
|
||||
let words = paragraph_words(&p, "en-US");
|
||||
assert_eq!(words.len(), 100_000);
|
||||
assert!(words.iter().all(|w| w.lang.is_empty()));
|
||||
let bad = misspelt(&p, "en-US", &[]);
|
||||
assert_eq!(bad.len(), 99_999);
|
||||
assert!(bad.iter().all(|w| w.repeated.is_some()));
|
||||
assert!(paragraph_words(&Paragraph::new("word"), &"y".repeat(100)).iter().all(|w| w.lang.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_and_numeric_flags_and_latin1() {
|
||||
let d = Dictionary::parse(b"FLAG long\nSFX Aa Y 1\nSFX Aa 0 s .\n", b"1\ncat/Aa\n");
|
||||
|
||||
+29
-12
@@ -260,12 +260,15 @@ pub(crate) struct Tab {
|
||||
align: TabAlign,
|
||||
}
|
||||
|
||||
/// The paragraph's tab stops in position order (a stop at no finite position is left out), so
|
||||
/// [`tab_advance`] finds the next one by binary search instead of a scan per tab.
|
||||
pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
|
||||
props
|
||||
let mut tabs: Vec<Tab> = props
|
||||
.tabs
|
||||
.as_ref()
|
||||
.map(|t| {
|
||||
t.iter()
|
||||
.filter(|s| s.pos.is_finite())
|
||||
.map(|s| Tab {
|
||||
pos: s.pos,
|
||||
align: match s.align.as_str() {
|
||||
@@ -277,7 +280,9 @@ pub(crate) fn tabs_of(props: &ParaProps) -> Vec<Tab> {
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
tabs.sort_by(|a, b| a.pos.total_cmp(&b.pos));
|
||||
tabs
|
||||
}
|
||||
|
||||
/// Width of the text after the tab at cell `i` up to the next tab or line break, and the width of
|
||||
@@ -301,18 +306,30 @@ pub(crate) fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) {
|
||||
/// (`seg`: its width, and its width before the decimal point) as the stop aligns it but never
|
||||
/// moving back, as PowerPoint does; past the last stop, to the next default stop.
|
||||
pub(crate) fn tab_advance(x: f64, stops: &[Tab], default_tab: f64, seg: (f64, f64)) -> f64 {
|
||||
for s in stops {
|
||||
if s.pos > x + 0.5 {
|
||||
let to = s.pos - x;
|
||||
return match s.align {
|
||||
TabAlign::Left => to,
|
||||
TabAlign::Right => (to - seg.0).max(0.0),
|
||||
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
|
||||
TabAlign::Decimal => (to - seg.1).max(0.0),
|
||||
};
|
||||
}
|
||||
// `stops` is in position order (`tabs_of`): the next stop is the first past x.
|
||||
// A NaN `x` (a NaN spacing upstream) skips the lookup and takes the finite default-stop path.
|
||||
if let Some(s) = stops.get(stops.partition_point(|s| s.pos <= x + 0.5)).filter(|_| !x.is_nan()) {
|
||||
let to = s.pos - x;
|
||||
return match s.align {
|
||||
TabAlign::Left => to,
|
||||
TabAlign::Right => (to - seg.0).max(0.0),
|
||||
TabAlign::Center => (to - seg.0 / 2.0).max(0.0),
|
||||
TabAlign::Decimal => (to - seg.1).max(0.0),
|
||||
};
|
||||
}
|
||||
let d = if default_tab > 1.0 { default_tab } else { 72.0 };
|
||||
let next = ((x / d).floor() + 1.0) * d;
|
||||
(next - x).max(1.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tab_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_nan_position_takes_the_finite_default_stop() {
|
||||
let stops = [Tab { pos: 100.0, align: TabAlign::Left }];
|
||||
assert!(tab_advance(f64::NAN, &stops, 72.0, (0.0, 0.0)).is_finite());
|
||||
assert_eq!(tab_advance(10.0, &stops, 72.0, (0.0, 0.0)), 90.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1416,6 +1416,16 @@ fn tab_stops_align_the_text_after_them() {
|
||||
assert!((l.lines[0].edges[4].0 - x0(&l) - 72.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_tabs_over_many_stops_lay_out_by_position() {
|
||||
// 20,000 stops given out of order, and 2,000 tabs: each tab finds the next stop by position.
|
||||
let mut stops: Vec<(f64, &str)> = (1..=20_000).rev().map(|k| (k as f64 * 0.05 + 30.0, "l")).collect();
|
||||
stops.push((f64::NAN, "l"));
|
||||
let text = "a\t".repeat(2_000);
|
||||
let l = tabbed(&text, &stops, 1.0e6);
|
||||
assert!((l.lines[0].edges[2].0 - l.inner.x0 - 30.05).abs() < 0.01, "{:?}", &l.lines[0].edges[..3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tab_past_the_end_moves_to_the_next_line() {
|
||||
// PowerPoint: "abcdefghij\tk\tl\tm…" in a narrow box puts the tab that doesn't fit on the next
|
||||
|
||||
Reference in New Issue
Block a user