Expand sample-text formulas on Enter (#1067) (#1101)

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Dante Souza e Souza
2026-10-14 01:55:30 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 1e48e6710f
commit 196f828f3f
6 changed files with 166 additions and 3 deletions
+3 -1
View File
@@ -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) |
+3
View File
@@ -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"
+58
View File
@@ -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<String> {
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::<usize>().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 {
+78
View File
@@ -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");
+6 -1
View File
@@ -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 |
+18 -1
View File
@@ -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) |