Merge pull request #856 from marcoarnulfo/feat/autoformat-ordinals

This commit is contained in:
Brandon Thomas
2026-10-18 11:18:18 -04:00
6 changed files with 85 additions and 6 deletions
+1
View File
@@ -139,6 +139,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-16 | Export: PNG with a transparent background — the slide background is left out, only the slide's content is drawn (Export… dialog, File › Export, web download, `file.export`/`file.render` `transparentBackground`, `deckcraft-cli render --transparent`) (#784) |
| 2026-10-15 | CLI: .txt and .md export includes table cell text, which it used to skip silently (#814) |
| 2026-10-15 | A `.pptx` slide that can't be read is reported, not dropped in silence: `lostSlides` from `file.open`, a message and a repaired copy in the app, a warning from `deckcraft-cli` (#203) |
| 2026-10-16 | AutoFormat as you type: ordinals (1st, 22nd, 103rd) get superscript suffixes after a space or punctuation; Tools ▸ AutoCorrect and `text.autocorrect {ordinals}` turn it off |
| 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) |
| 2026-10-14 | connected MCP rejects unrelated, malformed, incomplete and oversized replies without replay; buffered unsolicited bytes force a fresh connection before sending (#718) |
| 2026-10-14 | Empty bulleted and numbered paragraphs show gray markers while editing, including on first click and after Enter. Markers use the list style and stay out of slide shows and exports. |
+46 -3
View File
@@ -31,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
"AutoCorrect Options",
["Tools"],
None,
"{on?, twoInitials?, sentenceCaps?, days?, capsLock?, fractions?: bool, add?: [from, to], remove?: from} → the options and the replacement list",
"{on?, twoInitials?, sentenceCaps?, days?, capsLock?, fractions?, ordinals?: bool, add?: [from, to], remove?: from} → the options and the replacement list",
always,
autocorrect_options
),
@@ -655,7 +655,12 @@ fn autocorrect(s: &mut Session, typing: bool) -> Result<()> {
} else {
correct_word(&s.prefs, &word, sentence_start, typing)
};
let Some(rep) = rep else { return Ok(()) };
let Some(rep) = rep else {
if typing && s.prefs.ac_ordinals && is_ordinal(&word) {
return superscript_suffix(s, &t, word_end);
}
return Ok(());
};
let delta = rep.chars().count() as isize - word.chars().count() as isize;
let st = s.doc_mut()?;
let mut d = (*st.doc).clone();
@@ -674,6 +679,43 @@ fn autocorrect(s: &mut Session, typing: bool) -> Result<()> {
Ok(())
}
/// `word` is a number with the ordinal suffix it takes (1st, 22nd, 103rd, 11th), as AutoFormat
/// superscripts it.
fn is_ordinal(word: &str) -> bool {
let digits = word.trim_end_matches(|c: char| c.is_ascii_lowercase());
let suffix = word.get(digits.len()..).unwrap_or_default();
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) || suffix.len() != 2 {
return false;
}
let d = digits.as_bytes();
let teen = d.len() >= 2 && d.get(d.len() - 2) == Some(&b'1');
let want = match d.last() {
_ if teen => "th",
Some(b'1') => "st",
Some(b'2') => "nd",
Some(b'3') => "rd",
_ => "th",
};
suffix == want
}
/// Superscript the two letters before `end` in the caret's paragraph (an ordinal's suffix).
fn superscript_suffix(s: &mut Session, t: &crate::TextSel, end: usize) -> Result<()> {
let (a, b) = ((t.caret.0, end.saturating_sub(2)), (t.caret.0, end));
let st = s.doc_mut()?;
if body_of(st, t).is_none_or(|body| ed::props_at(body, b).baseline.unwrap_or(0.0) > 0.0) {
return Ok(());
}
let mut d = (*st.doc).clone();
let sel = st.selection.clone();
if let Some(body) = body_mut(&mut d, &sel, t) {
ed::format(body, a, b, &|r| r.baseline = Some(0.3));
}
st.doc = Arc::new(d);
st.revision += 1;
Ok(())
}
fn autocorrect_options(s: &mut Session, p: &Value) -> Result<Value> {
let pr = &mut s.prefs;
for (key, slot) in [
@@ -683,6 +725,7 @@ fn autocorrect_options(s: &mut Session, p: &Value) -> Result<Value> {
("days", &mut pr.ac_days),
("capsLock", &mut pr.ac_caps_lock),
("fractions", &mut pr.ac_fractions),
("ordinals", &mut pr.ac_ordinals),
] {
if let Some(v) = p.get(key).and_then(Value::as_bool) {
*slot = v;
@@ -729,7 +772,7 @@ fn autocorrect_options(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({
"on": pr.autocorrect, "twoInitials": pr.ac_two_initials, "sentenceCaps": pr.ac_sentence_caps,
"days": pr.ac_days, "capsLock": pr.ac_caps_lock, "fractions": pr.ac_fractions,
"entries": list,
"ordinals": pr.ac_ordinals, "entries": list,
}))
}
+3 -1
View File
@@ -324,12 +324,13 @@ pub struct Prefs {
pub smart_quotes: bool,
/// AutoCorrect options (Tools ▸ AutoCorrect): fix TWo INitial CApitals, capitalize the first
/// letter of sentences and the names of days, fix accidental cAPS lOCK, and replace 1/2, 1/4
/// and 3/4 with fraction characters.
/// and 3/4 with fraction characters; AutoFormat superscripts ordinals (1st) as you type.
pub ac_two_initials: bool,
pub ac_sentence_caps: bool,
pub ac_days: bool,
pub ac_caps_lock: bool,
pub ac_fractions: bool,
pub ac_ordinals: bool,
/// Your own "replace … with …" entries; they win over the built-in list.
pub ac_entries: Vec<(String, String)>,
/// Built-in entries you deleted.
@@ -354,6 +355,7 @@ impl Default for Prefs {
ac_days: true,
ac_caps_lock: true,
ac_fractions: true,
ac_ordinals: true,
ac_entries: Vec::new(),
ac_removed: Vec::new(),
author: "Presenter".into(),
+31
View File
@@ -3167,6 +3167,37 @@ fn autocorrect_options_as_you_type() {
assert_eq!(s.execute("text.get", &json!({"id": title})).unwrap()["text"], "monday notes ");
}
#[test]
fn autocorrect_superscripts_ordinals_as_you_type() {
let mut s = session();
let title = s.doc().unwrap().current_slide().unwrap().shapes[0].id.0;
// The typed text with each superscript run in ^(…).
let typed = |s: &mut Session, text: &str| -> String {
s.execute("text.edit", &json!({"id": title})).unwrap();
s.execute("text.selectAll", &json!({})).ok();
s.execute("text.delete", &json!({"dir": "backward"})).ok();
for c in text.chars() {
s.execute("text.insert", &json!({"text": c.to_string()})).unwrap();
}
let st = s.doc().unwrap();
let sh = st.current_slide().unwrap().shapes.iter().find(|sh| sh.id.0 == title).unwrap();
sh.text.as_ref().unwrap().paragraphs[0]
.runs
.iter()
.map(|r| if r.props.baseline.unwrap_or(0.0) > 0.0 { format!("^({})", r.text) } else { r.text.clone() })
.collect()
};
assert_eq!(typed(&mut s, "1st, 22nd and 103rd. 4th 11th x"), "1^(st), 22^(nd) and 103^(rd). 4^(th) 11^(th) x");
// Not a word, a number without a suffix it takes, or a suffix not yet followed by a space.
assert_eq!(typed(&mut s, "first x1st 1th 2st 21st"), "First x1st 1th 2st 21st");
assert_eq!(s.execute("text.autocorrect", &json!({})).unwrap()["ordinals"], true);
s.execute("text.autocorrect", &json!({"ordinals": false})).unwrap();
assert_eq!(typed(&mut s, "2nd "), "2nd ");
// AutoCorrect off turns it off too.
s.execute("text.autocorrect", &json!({"ordinals": true, "on": false})).unwrap();
assert_eq!(typed(&mut s, "2nd "), "2nd ");
}
#[test]
fn theme_variants_apply_and_report_the_current_one() {
let mut s = session();
+1
View File
@@ -2108,6 +2108,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.checkbox(&mut pr.ac_days, "Capitalize names of days");
ui.checkbox(&mut pr.ac_caps_lock, "Correct accidental use of cAPS LOCK key");
ui.checkbox(&mut pr.ac_fractions, "Replace fractions (1/2) with fraction characters (½)");
ui.checkbox(&mut pr.ac_ordinals, "Ordinals (1st) with superscript");
ui.checkbox(&mut pr.autocorrect, "Replace text as you type");
ui.add_space(4.0);
let mut from = d.get("from", "");
+3 -2
View File
@@ -257,8 +257,8 @@ of the effect gallery. Detail in [animation-parity.md](animation-parity.md). 8
installed (always on Windows and the web) only a short list of common misspellings is found.
`review.thesaurus` looks up the selected word (checklist row partial). AutoCorrect has
PowerPoint's options (two initial capitals, sentence and day capitals, caps lock, fractions, an
editable replace list); no AutoFormat of ordinals or automatic bulleted lists, no undo of a
single correction.
editable replace list), and AutoFormat superscripts ordinals (1st) as you type; no automatic
bulleted lists, no undo of a single correction.
- **Estimate:** 1–2 h (bundle openly licensed dictionaries); 2–4 h (AutoFormat as you type).
### Equations
@@ -355,6 +355,7 @@ Detail in [localization-parity.md](localization-parity.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-15 | trivial | PPTX trigger sequences keep later On Click steps instead of converting them to After Previous; mixed starts, independent buttons and paragraph builds have synthetic round-trip regressions |
| 2026-10-16 | trivial | AutoFormat superscripts ordinals (1st, 22nd) as you type, with an AutoCorrect option to turn it off; automatic bulleted lists remain |
| 2026-10-14 | trivial | Fixed #718: remote MCP no longer accepts unrelated or buffered stale success; loopback regressions cover invalid envelopes, incomplete and oversized frames |
| 2026-10-14 | minor | Community PR wave folded: one entry each for the soak tests (engine and UI), the PPTX corpus (seven owned corpus files), edit points / text warp / 3-D, proofing (Hunspell spelling, thesaurus, AutoCorrect), the smaller missing rows, dialogs (41 vs ~60; Print and Insert Object left) and pen/trackpad input; ranked rows 3, 4 and 10 de-duplicated (comment threads #70 and the pane timeline are done); the fixed command-palette entry (#79) removed |
| 2026-10-13 | minor | EMF/WMF rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing. EMF+ records remain |