mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-01 14:37:05 -06:00
Text: a word doesn't break after ?, !, } or + (URLs wrap as in PowerPoint) (#924)
* Text: a word doesn't break after ?, !, } or + (URLs wrap as in PowerPoint) Unicode line breaking allows a break after ?, !, } and + inside a word, so a long URL broke after its '?' and after each '+' of a query and left the rest of the line empty. Measured in PowerPoint 2016 for Windows with latinLnBrk off (its default), none of them is a break: the word fills the line and breaks where it no longer fits, as PowerPoint already does after / and \ (break_opportunities). The same synthetic slide now wraps line for line as PowerPoint's PDF does, a Yandex search URL included. * Text tests: the first line doesn't end at the separator * Text tests: check the line is full, not that it avoids ending at a separator * Keep the Unicode break after ?, !, / outside Latin words The PowerPoint no-break rule only holds inside a word: before an ideograph (日本!語) or punctuation the Unicode break stays, as CodeRabbit pointed out. * Only letters and digits continue a word for the no-break rule AL and AI also hold symbols such as & and ×, so a!&b kept no break after !. is_word_char now also requires c.is_alphanumeric() (CodeRabbit). * Test that Latin before CJK and a separator before a space still break --------- Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
This commit is contained in:
@@ -82,9 +82,18 @@ fn is_cj_nonstarter(c: char) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// A character that continues a Latin-style word: a letter or digit that Unicode does not break
|
||||
/// between, as opposed to CJK ideographs, kana, spaces, punctuation and symbols such as `&` or `×`
|
||||
/// (which share the AL and AI line-break classes with letters).
|
||||
fn is_word_char(c: char) -> bool {
|
||||
use unicode_linebreak::BreakClass::{Alphabetic, Ambiguous, HebrewLetter, Numeric};
|
||||
matches!(unicode_linebreak::break_property(c as u32), Alphabetic | Ambiguous | HebrewLetter | Numeric) && c.is_alphanumeric()
|
||||
}
|
||||
|
||||
/// Break opportunities: allowed[i] = a line may start at char i. Unicode line breaking, then
|
||||
/// PowerPoint's differences: without `latinLnBrk` a word doesn't break after `/` or `\`, and
|
||||
/// Korean breaks only between words.
|
||||
/// PowerPoint's differences: without `latinLnBrk` a word doesn't break after `/`, `\`, `?`, `!`, `}`
|
||||
/// or `+` inside a word (measured in PowerPoint 2016 for Windows; before an ideograph or a space
|
||||
/// the Unicode break stays), and Korean breaks only between words.
|
||||
pub(crate) fn break_opportunities(text: &str, latin_break: bool) -> (Vec<bool>, Vec<bool>) {
|
||||
let n = text.chars().count();
|
||||
let mut allowed = vec![false; n + 1];
|
||||
@@ -113,7 +122,7 @@ pub(crate) fn break_opportunities(text: &str, latin_break: bool) -> (Vec<bool>,
|
||||
let keep = if (is_hangul(prev) && is_hangul(next)) || prev == '\u{200B}' || next == '\u{2014}' || is_cj_nonstarter(next) {
|
||||
false
|
||||
} else {
|
||||
latin_break || !matches!(prev, '/' | '\\') || next.is_whitespace()
|
||||
latin_break || !matches!(prev, '/' | '\\' | '?' | '!' | '}' | '+') || !is_word_char(next)
|
||||
};
|
||||
if !keep
|
||||
&& !mandatory.get(i + 1).copied().unwrap_or(false)
|
||||
|
||||
@@ -857,19 +857,37 @@ fn one_line(text: &str) -> f64 {
|
||||
#[test]
|
||||
fn a_word_does_not_break_after_a_slash_or_backslash() {
|
||||
// PowerPoint (latinLnBrk off, its default) keeps "alpha/beta/gamma" whole and only breaks it
|
||||
// where it no longer fits; Unicode line breaking would allow a break after each slash.
|
||||
for sep in ["/", "\\"] {
|
||||
// where it no longer fits; Unicode line breaking would allow a break after each slash. The
|
||||
// same holds after ?, !, } and + (a URL's query, measured in PowerPoint 2016 for Windows).
|
||||
for sep in ["/", "\\", "?", "!", "}", "+"] {
|
||||
let text = ["alpha", "beta", "gamma", "delta", "epsilon"].join(sep);
|
||||
let w = one_line(&text) * 0.6;
|
||||
let lines = wrapped(&text, w, |_| {});
|
||||
assert!(lines.len() >= 2, "{lines:?}");
|
||||
assert!(lines[0].chars().count() > "alpha/beta".len(), "filled to the edge, not cut at a separator: {lines:?}");
|
||||
// The line breaks where the next character no longer fits, not at an earlier separator
|
||||
// (it may still end with one when that is where the width runs out).
|
||||
let next: String = lines[0].chars().chain(lines[1].chars().take(1)).collect();
|
||||
assert!(one_line(&next) > w, "no break right after {sep:?} while more fits: {lines:?}");
|
||||
}
|
||||
// latinLnBrk on: Unicode's break after a slash is allowed again.
|
||||
let text = "alpha/beta/gamma/delta/epsilon";
|
||||
let lines = wrapped(text, one_line(text) * 0.6, |p| p.latin_line_break = Some(true));
|
||||
assert!(lines[0].ends_with('/'), "{lines:?}");
|
||||
// A slash followed by a space still breaks, and hyphens still break.
|
||||
// Outside a Latin word the Unicode break stays: "日本!" fits, "語" moves to the next line.
|
||||
let lines = wrapped("日本!語", one_line("日本!") + 0.5, |_| {});
|
||||
assert_eq!(lines[0], "日本!", "{lines:?}");
|
||||
// Nor before a symbol that only shares the letters' line-break class.
|
||||
let (allowed, _) = break_opportunities("ab!&cd", false);
|
||||
assert!(allowed[3], "break after ! before &: {allowed:?}");
|
||||
// Latin text before CJK still breaks after the mark: "ab?" fits, "日本" moves down.
|
||||
let lines = wrapped("ab?日本", one_line("ab?") + 0.5, |_| {});
|
||||
assert_eq!(lines[0], "ab?", "{lines:?}");
|
||||
// A separator followed by a space still breaks, and hyphens still break.
|
||||
for sep in ["/", "?", "!"] {
|
||||
let lines = wrapped(&format!("alpha{sep} beta"), one_line(&format!("alpha{sep} b")) + 0.5, |_| {});
|
||||
assert_eq!(lines.iter().map(|l| l.trim_end()).collect::<Vec<_>>(), [format!("alpha{sep}"), "beta".into()], "{lines:?}");
|
||||
}
|
||||
let lines = wrapped("alpha-beta-gamma-delta-epsilon", one_line("alpha-beta-gamma-") + 1.0, |_| {});
|
||||
assert_eq!(lines[0], "alpha-beta-gamma-");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DeckCraft vs Microsoft PowerPoint: parity assessment
|
||||
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (Animations shortfall: preview from the selected effect is in; previous: major, community PR wave 2026-10-14: breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) after `cargo xtask parity`; area table and What works today folded to one row or bullet each; 320 engine commands; ready ~55% and stage alpha unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (no break after `?`, `!`, `}`, `+` inside a word; previous: trivial, Animations shortfall: preview from the selected effect is in; before that: major, community PR wave 2026-10-14: breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) after `cargo xtask parity`; area table and What works today folded to one row or bullet each; 320 engine commands; ready ~55% and stage alpha unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?".
|
||||
[ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall;
|
||||
@@ -289,6 +289,7 @@ scored done (16 open issues).
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-22 | trivial | Animations shortfall: preview from the selected effect (Play From) is in. Percentages unchanged |
|
||||
| 2026-10-17 | trivial | A word no longer breaks after `?`, `!`, `}` or `+` (a URL's query wraps at the edge as in PowerPoint 2016 for Windows), as it already didn't after `/` and `\`. Headline percentages unchanged |
|
||||
| 2026-10-16 | trivial | Arrange shortfall: Align to Slide and Align Selected Objects are in the Align menu. Percentages unchanged |
|
||||
| 2026-10-14 | minor | Community PR wave folded: one headline breadth row (84% over 191 features after `cargo xtask parity`; duplicate checklist rows removed), one row per feature area with the latest breadth and shortfalls, one Themes / Slide show / Views / Review / Automation bullet each (320 engine commands). SmartArt ready ~35% (Text Pane, 6e40335) moves the area mean to 62.8%; dimension and headline percentages unchanged |
|
||||
| 2026-10-13 | minor | Shapes shortfall "text warp" narrowed to adjust handles: the Transform gallery applies a preset |
|
||||
|
||||
Reference in New Issue
Block a user