diff --git a/ROADMAP.md b/ROADMAP.md index fc8a63689..232233225 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (direct run-level native InkML content parts survive DOCX saves; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (sample-text formulas on Enter, #1067; previously trivial: direct run-level native InkML content parts survive DOCX saves; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -135,6 +135,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| +| 2026-10-12 | Sample-text formulas on Enter (#1067): `=rand()` and `=lorem()`, optional paragraph/sentence counts, 5/3 defaults, original English and one-step undo | | 2026-10-12 | File › Export › Word 2003 XML Document (#1081): documents save as WordprocessingML 2003 (`format: "wordml2003"` on the save commands, `wordcraft-cli convert --format wordml2003`); shapes, text boxes, charts and SmartArt don't survive, and it hasn't been opened in Word yet | | 2026-10-12 | Direct run-level native InkML (`w:contentPart`) now survives DOCX edits and repeated saves with its relationship, content type and bytes intact. Native InkML rendering/editing and ink nested in groups or canvases remain missing | | 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word | @@ -275,6 +276,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Progress log: sample-text formulas on Enter (#1067) | | 2026-10-12 | trivial | Progress log: direct run-level native InkML content parts survive DOCX edits and repeated saves | | 2026-10-12 | trivial | Progress log: Accept All and Reject All keep the right paragraph marks (#864) | | 2026-10-11 | trivial | Progress log: Ctrl, Alt, left-edge and selected-cells table border drags (#711) | diff --git a/contributors/people.toml b/contributors/people.toml index a10cfcc5c..5a54f37ea 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -17,6 +17,9 @@ # To remove your names, delete your entry (or set fields empty) in a PR. The next # contributors.py run, and the next build, drop them. +[people.lokinmodar] +sync_github_name = true + [people.echelon] real_name = "Brandon Thomas" display_name = "Brandon Thomas" diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index c962cc9f1..23b79c97f 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -385,12 +385,70 @@ fn ins_char(s: &mut Session, c: char) -> CmdResult { sel_result(s) } +/// Exact sample-text formulas; bounded counts keep typed input from allocating without limit. +fn sample_text(s: &Session) -> Option { + let at = &s.sel.focus; + let para = s.doc.para_at(at)?; + if !s.sel.is_collapsed() || at.off != para.len() { + return None; + } + let (args, sentences) = if let Some(args) = para.text.strip_prefix("=rand(") { + ( + args, + [ + "A blank page leaves room for a new idea.", + "The morning light falls across a notebook on the desk.", + "Small changes in spacing can give a story a different rhythm.", + ], + ) + } else { + ( + para.text.strip_prefix("=lorem(")?, + [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + ], + ) + }; + let args = args.strip_suffix(')')?; + if args.contains(['(', ')']) { + return None; + } + let count = |arg: &str| arg.parse::().ok().filter(|n| (1..=200).contains(n)); + let counts = if args.is_empty() { + Some((5, 3)) + } else if let Some((p, s)) = args.split_once(',') { + count(p).zip(count(s)) + } else { + count(args).map(|p| (p, 3)) + }; + let (paragraphs, lines) = counts.unwrap_or((5, 3)); + let mut text = String::new(); + for _ in 0..paragraphs { + for sentence in sentences.iter().cycle().take(lines) { + if !text.is_empty() && !text.ends_with('\r') { + text.push(' '); + } + text.push_str(sentence); + } + text.push('\r'); + } + Some(text) +} + fn new_paragraph(s: &mut Session, _: &Value) -> CmdResult { // Enter in a one-line plain text control (or a check box or list) does nothing, as in Word. if super::controls::refuses_paragraph(s) { return sel_result(s); } super::controls::prepare_edit(s)?; + if let Some(text) = sample_text(s) { + let end = s.sel.focus.clone(); + s.sel = Selection { anchor: Pos { off: 0, ..end.clone() }, focus: end }; + type_text(s, &text)?; + return sel_result(s); + } let mut at = delete_selection(s)?; let auto = border_line(s, &at).map(EnterAutoFormat::Border).or_else(|| table_line(s, &at).map(EnterAutoFormat::Table)); if let Some(auto) = auto { diff --git a/crates/engine/src/tests_typing.rs b/crates/engine/src/tests_typing.rs index 6b2c97be8..70e75be47 100644 --- a/crates/engine/src/tests_typing.rs +++ b/crates/engine/src/tests_typing.rs @@ -49,6 +49,84 @@ fn typed(keys: &str) -> Session { s } +#[test] +fn sample_text_formulas_expand_on_enter() { + for name in ["rand", "lorem"] { + for (args, paragraphs, sentences) in [("", 5, 3), ("2", 2, 3), ("2,4", 2, 4), ("1,1", 1, 1), ("200,1", 200, 1), ("1,200", 1, 200)] { + let mut s = typed(&format!("={name}({args})")); + let depth = s.undo_depth(); + typ(&mut s, "\n"); + let generated: Vec<_> = paras(&s).into_iter().map(|p| p.0).collect(); + assert_eq!(generated.len(), paragraphs + 1, "{name}({args})"); + for text in generated.iter().take(paragraphs) { + assert_eq!(text.matches('.').count(), sentences); + assert!(text.starts_with(if name == "lorem" { "Lorem ipsum" } else { "A blank page" }), "{name}({args}): {text:?}"); + } + assert_eq!(generated.last().unwrap(), ""); + assert_eq!(s.sel.focus, Pos::body(paragraphs, 0)); + assert_eq!(s.undo_depth(), depth + 1); + let expanded = s.doc.clone(); + run(&mut s, "edit.undo", json!({})); + assert_eq!(texts(&s), [format!("={name}({args})")]); + run(&mut s, "edit.redo", json!({})); + assert_eq!(s.doc, expanded); + } + } +} + +#[test] +fn sample_text_invalid_parameters_use_defaults() { + for name in ["rand", "lorem"] { + for args in ["0", "-1", "201", "abc", "1.5", "99999999999999999999999999999", "2,0", "0,4", "2,201", "2,no", "2,", ",4", "1,2,3"] { + let s = typed(&format!("={name}({args})\n")); + let texts = texts(&s); + assert_eq!(texts.len(), 6, "{name}({args})"); + assert!(texts.iter().take(5).all(|text| text.matches('.').count() == 3)); + } + } +} + +#[test] +fn sample_text_requires_an_exact_paragraph_and_caret_at_end() { + for formula in [" =rand()", "=rand() ", "prefix =rand()", "=lorem() suffix", "=other()", "=rand(", "=rand(())"] { + assert_eq!(texts(&typed(&format!("{formula}\n"))), [formula, ""]); + } + let mut s = typed("=rand()"); + run(&mut s, "caret.home", json!({})); + typ(&mut s, "\n"); + assert_eq!(texts(&s), ["", "=rand()"]); + let mut s = typed("=rand()suffix"); + s.sel = crate::Selection { anchor: Pos::body(0, 7), focus: Pos::body(0, 13) }; + typ(&mut s, "\n"); + assert_eq!(texts(&s), ["=rand()", ""]); +} + +#[test] +fn sample_text_preserves_typing_and_paragraph_formatting() { + let mut s = s(); + run(&mut s, "format.bold", json!({})); + run(&mut s, "para.alignCenter", json!({})); + typ(&mut s, "=rand(2,1)\n"); + for p in s.doc.body.iter().filter_map(|b| b.as_para()).take(2) { + assert_eq!(p.props.align, Some(wordcraft_doc::props::Align::Center)); + assert_eq!(p.props_at(0).bold, Some(true)); + } +} + +#[test] +fn sample_text_replacement_tracks_changes() { + let mut s = typed("=lorem(2,1)"); + run(&mut s, "review.trackChanges", json!({})); + typ(&mut s, "\n"); + assert!(s.doc.body.iter().filter_map(|b| b.as_para()).flat_map(|p| &p.runs).any(|r| r.props.ins.is_some())); + run(&mut s, "review.rejectAll", json!({})); + assert_eq!(texts(&s), ["=lorem(2,1)"]); + run(&mut s, "edit.undo", json!({})); + run(&mut s, "review.acceptAll", json!({})); + assert_eq!(texts(&s).len(), 3); + assert!(!s.doc.plain_text(StoryRef::Body).contains("=lorem")); +} + #[test] fn enter_twice_after_a_list_item_ends_the_list() { let s = typed("* item\n\ntext"); diff --git a/docs/gaps.md b/docs/gaps.md index 5f3b62096..90eed0751 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where WordCraft falls short of Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (radar, stock and other undrawn chart types show a placeholder instead of blank space, #998; previously trivial: saving Word 2003 XML, #1081; previously trivial: direct run-level native InkML content parts now survive DOCX saves; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (sample-text formulas on Enter, #1067; previously trivial: radar, stock and other undrawn chart types show a placeholder instead of blank space, #998; previously trivial: saving Word 2003 XML, #1081; previously trivial: direct run-level native InkML content parts now survive DOCX saves; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -51,6 +51,10 @@ full number from ~60% to ~75%. Everything else is depth on the way to full parit ## By kind +Sample-text formulas on Enter (#1067) are implemented: `=rand()` and `=lorem()`, +optional paragraph/sentence counts and one-step undo. See [typing-parity.md](typing-parity.md) +for the accepted parameter range and validation limits; readiness numbers are unchanged. + The same gaps grouped the way the parity documents are, for agents working in one area. - **Feature gaps:** #2 (preserve), #5, #10, #12, #13, #15, #17, #21, #25, #27, #28, #29; Clipboard @@ -73,6 +77,7 @@ The same gaps grouped the way the parity documents are, for agents working in on | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Sample-text formulas on Enter (#1067) implemented; numerical limits not compared in Word | | 2026-10-12 | minor | Italian proofing landed (it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar); gap #8 narrowed to the remaining languages | | 2026-10-12 | trivial | #2 narrowed: direct run-level `w:contentPart` InkML survives DOCX edits and repeated saves; nested group/canvas ink and native rendering remain | | 2026-10-11 | trivial | Table column and row markers on the rulers landed (#700): the ruler's column markers are off the UI/UX gaps | diff --git a/docs/typing-parity.md b/docs/typing-parity.md index 07b3e77bf..c4be6aebe 100644 --- a/docs/typing-parity.md +++ b/docs/typing-parity.md @@ -1,6 +1,6 @@ # Typing parity with Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** trivial (Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** trivial (sample-text formulas on Enter, #1067; previously trivial: Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 What Word does when you type, observed black-box (same keystrokes typed into Word and WordCraft, results read back paragraph by paragraph) and pinned by tests in @@ -8,6 +8,22 @@ WordCraft, results read back paragraph by paragraph) and pinned by tests in tests together. Line breaking and pagination are in [`layout-parity.md`](layout-parity.md); the overall picture is in [`target-app-parity.md`](target-app-parity.md). +## Sample text (#1067) + +Enter at the end of a paragraph containing exactly `=rand()` or `=lorem()` replaces +the formula with five paragraphs of three sentences and leaves the caret in a following +empty paragraph. `=rand(p)` / `=lorem(p)` set the paragraph count; `(p,s)` sets both +counts. WordCraft accepts integers from 1 to 200 for each count; if either parameter +is invalid or outside that range, both revert to 5/3. Other text, a selection or Enter +inside the formula follows ordinary Enter behaviour. The English text is original to +WordCraft; lorem ipsum uses traditional pseudo-Latin. This implements the issue's +requested defaults; the exact numerical limits have not been compared in Word. + +The replacement and its paragraph breaks are one undo step. Typing and paragraph +formatting use the existing editing primitives. Tests: `sample_text_formulas_expand_on_enter`, +`sample_text_invalid_parameters_use_defaults`, `sample_text_requires_an_exact_paragraph_and_caret_at_end`, +`sample_text_preserves_typing_and_paragraph_formatting`, `sample_text_replacement_tracks_changes`. + ## Lists | Keys | Word's result | Test | @@ -168,6 +184,7 @@ Word. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Sample-text formulas on Enter (#1067), exact matching, bounded parameters, original English and one-step undo | | 2026-10-12 | trivial | Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864) | | 2026-10-12 | minor | AutoCorrect: Correct TWo INitial CApitals, accidental cAPS LOCK, capitalize first letter of table cells (#544) | | 2026-10-11 | minor | Text direction: right-to-left letters typed into an empty paragraph make it right-to-left; existing paragraphs keep theirs (#713) |