mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 13:27:27 -06:00
* #1116: keep a paragraph's space after clear of the footnote separator On a page with footnotes (already there or brought by the line), a paragraph's last line only fits if its space after fits above the notes area too; otherwise it goes to the next page with its note (and, with widow control, one more line). Contextual spacing and automatic spacing between list items drop the requirement; pages without notes are unchanged (space after may still run past the bottom margin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #1116: space after vs footnote separator uses the spacing code's tests, only on pages with notes Contextual spacing drops a paragraph's space after only when that paragraph asks for it, and the next paragraph counts as a list item only when it gets a label (Numbering::shows_label), as in the spacing between paragraphs. The kept space after is worked out only for a last line on a page with notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Dmitrii Khlapov
Brandon Thomas
parent
2dcde62220
commit
85a3a53a8e
+2
-1
@@ -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-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: dragged shapes wrap in their new column, #884; 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-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: footnote pagination, #1116; previously trivial: 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.
|
||||
|
||||
@@ -144,6 +144,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-13 | The caret is black or white, whichever shows against what lies under it — the paper, a page colour, a dark page, shading or a highlight — whatever the UI theme, as Word's inverted caret does; before, it stayed black on dark page colours and highlights and could vanish (#299) |
|
||||
| 2026-10-13 | Clicking the ribbon's tab names, empty space, gallery tiles or View check boxes, or the status bar's view buttons and language menu, no longer makes the caret vanish: typing goes on in the document as in Word, without another click on the page (#1093) |
|
||||
| 2026-10-13 | Dragging a Square-wrapped shape or picture on a multi-column page wraps the text of the column it lands in around it, and its old place closes up (#884) |
|
||||
| 2026-10-13 | Pagination with footnotes follows Word (#1116): a paragraph ending on a page with footnotes keeps its space after above the separator, otherwise its last lines move to the next page with the note; pages without notes are unchanged |
|
||||
| 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 |
|
||||
| 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them |
|
||||
| 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started |
|
||||
|
||||
@@ -208,6 +208,11 @@ impl Numbering {
|
||||
let n = self.num(num)?;
|
||||
self.abstracts.iter().find(|a| a.id == n.abstract_id)
|
||||
}
|
||||
/// Whether a paragraph in list `num` at `level` gets a label ([`Counters::next_label`] gives
|
||||
/// one), without counting it.
|
||||
pub fn shows_label(&self, num: u32, level: u8) -> bool {
|
||||
self.abstract_of(num).is_some() && self.level(num, level.min(8)).is_some_and(|d| d.format != NumFormat::None)
|
||||
}
|
||||
pub fn level(&self, num: u32, level: u8) -> Option<&Level> {
|
||||
if let Some((_, l)) = self.num(num)?.level_overrides.iter().find(|(i, _)| *i == level) {
|
||||
return Some(l);
|
||||
|
||||
@@ -660,6 +660,14 @@ impl Ctx<'_> {
|
||||
|
||||
/// The list label of `p`, advancing the list counters: call it once per paragraph, in
|
||||
/// document order (only for paragraphs that will show).
|
||||
/// Whether `p` gets a list label ([`Ctx::next_label`] is `Some`), without counting it.
|
||||
fn has_label(&self, p: &Paragraph) -> bool {
|
||||
p.props
|
||||
.numbering
|
||||
.or_else(|| self.doc.styles.resolve_para(&p.props).numbering)
|
||||
.filter(|n| n.num != 0)
|
||||
.is_some_and(|n| self.doc.numbering.shows_label(n.num, n.level))
|
||||
}
|
||||
fn next_label(&mut self, p: &Paragraph) -> Option<(String, Level)> {
|
||||
let n = p.props.numbering.or_else(|| self.doc.styles.resolve_para(&p.props).numbering).filter(|n| n.num != 0)?;
|
||||
self.counters.next_label(&self.doc.numbering, n.num, n.level)
|
||||
@@ -2777,6 +2785,8 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
anchor_y = y0 - applied_before;
|
||||
}
|
||||
let mut n = pl.lines.len();
|
||||
// Worked out only for a last line on a page with notes.
|
||||
let mut after_on_page: Option<f32> = None;
|
||||
let mut l0 = 0;
|
||||
while l0 < n {
|
||||
// How many lines fit?
|
||||
@@ -2815,6 +2825,17 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
- if pb.notes.is_empty() && !new_notes.is_empty() { pb.seps.sep_h(false) } else { 0.0 }
|
||||
- extra;
|
||||
let bottom = pb.y + lead + (l.top + l.height - first.top);
|
||||
// On a page with footnotes the paragraph's last line keeps its space after clear of
|
||||
// the separator (Word): if the space doesn't fit, the line goes to the next page (and,
|
||||
// with widow control, one more with it). Without notes the space after may run past
|
||||
// the bottom margin as usual.
|
||||
let has_notes = !pb.notes.is_empty() || !new_notes.is_empty() || !line_notes.is_empty();
|
||||
let tail = if has_notes && l1 + 1 == n {
|
||||
*after_on_page.get_or_insert_with(|| space_after_kept(ctx, block, &pl.rp, label.is_some()))
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let over = bottom + tail > limit + 0.01;
|
||||
// Notes too long for the rest of the page continue on the next one: the line still
|
||||
// fits if the notes before its last one, and that one's first line, fit below it.
|
||||
// The notes then take the rest of the page.
|
||||
@@ -2824,7 +2845,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
let sep = pb.seps.sep_h(pb.notes.first().is_some_and(|n| n.cont));
|
||||
pb.body_floor().max(bottom) + sep + before + last.first_piece() + pb.seps.notice_h() <= pb.orig_bottom + 0.01
|
||||
});
|
||||
if bottom > limit + 0.01 && l1 > l0 && !split {
|
||||
if over && l1 > l0 && !split {
|
||||
break;
|
||||
}
|
||||
// A line holding nothing but a page or column break stays at the bottom of the page
|
||||
@@ -2834,7 +2855,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
&& p.text
|
||||
.get(l.start..l.stop)
|
||||
.is_some_and(|t| t.chars().all(|c| matches!(c, wordcraft_doc::para::PAGE_BREAK | wordcraft_doc::para::COLUMN_BREAK)));
|
||||
if bottom > limit + 0.01 && l1 == l0 && !pb.at_top() && !split && !only_break {
|
||||
if over && l1 == l0 && !pb.at_top() && !split && !only_break {
|
||||
break;
|
||||
}
|
||||
new_notes.extend(line_notes);
|
||||
@@ -3063,6 +3084,28 @@ fn mirror_shift(rp: &wordcraft_doc::resolve::ResolvedPara, number: u32, web: boo
|
||||
right - left
|
||||
}
|
||||
|
||||
/// The space after body paragraph `block` (resolved as `rp`) that is really left below it: none
|
||||
/// when contextual spacing or automatic spacing between list items drops it before the next
|
||||
/// paragraph. Same tests as the spacing between paragraphs in `place_para_in_flow`: contextual
|
||||
/// spacing removes this paragraph's space after only when this one asks for it, and the next
|
||||
/// counts as a list item when it gets a label.
|
||||
fn space_after_kept(ctx: &Ctx, block: usize, rp: &wordcraft_doc::resolve::ResolvedPara, listed: bool) -> f32 {
|
||||
let after = rp.space_after.max(0.0);
|
||||
if after <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
if let Some(Block::Para(next)) = ctx.doc.body.get(block + 1).map(|b| &**b) {
|
||||
let nrp = ctx.doc.styles.resolve_para(&next.props);
|
||||
if nrp.style == rp.style && rp.contextual_spacing {
|
||||
return 0.0;
|
||||
}
|
||||
if listed && rp.after_auto && ctx.has_label(next) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
after
|
||||
}
|
||||
|
||||
/// Height of the first line of the block after `block` (for keep-with-next).
|
||||
/// The room the paragraphs after `block` need on its page for keep with next: the next one's
|
||||
/// first line, or all of it and what it keeps with in turn when it is kept with the next too
|
||||
|
||||
@@ -4990,3 +4990,132 @@ fn automatic_spacing_drops_between_list_items() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// #1116: a document of a top paragraph with `before` points of space before, filler lines, a
|
||||
/// paragraph of several lines (block `t`) with 6 pt after and, when `note`, a footnote on its last line.
|
||||
fn space_after_doc(before: f32, note: bool) -> (Document, usize, Option<u32>) {
|
||||
let text = format!("Top.\n{}{}\nAfter.", "Body text line.\n".repeat(14), "Words in a paragraph that wraps. ".repeat(14));
|
||||
let mut d = Document::from_text(&text);
|
||||
let t = 15;
|
||||
if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) {
|
||||
p.props.space_before = Some(before);
|
||||
}
|
||||
if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(t).map(std::sync::Arc::make_mut) {
|
||||
p.props.space_after = Some(6.0);
|
||||
p.props.widow_control = Some(true);
|
||||
}
|
||||
let len = d.para(StoryRef::Body, &Path(vec![t as u32])).map(|p| p.text.len()).unwrap();
|
||||
let id = note.then(|| footnote(&mut d, &Pos::body(t, len), "The note."));
|
||||
(d, t, id)
|
||||
}
|
||||
|
||||
/// The pieces of body paragraph `t` as (page, l0, l1, bottom), and its line count.
|
||||
fn para_pieces(l: &DocLayout, t: usize) -> (Vec<(usize, usize, usize, f32)>, usize) {
|
||||
let mut v = Vec::new();
|
||||
let mut n = 0;
|
||||
for (pi, p) in l.pages.iter().enumerate() {
|
||||
for it in &p.items {
|
||||
if let Placed::Lines { story: StoryRef::Body, path, para, l0, l1, y, .. } = it
|
||||
&& path.0 == [t as u32]
|
||||
{
|
||||
n = para.lines.len();
|
||||
v.push((pi, *l0, *l1, item_bottom(*y, para, *l0, *l1).unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
(v, n)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_after_stays_clear_of_the_footnote_separator() {
|
||||
// #1116, measured in Word: on a page with footnotes a paragraph ending there keeps its space
|
||||
// after above the separator, or its last lines (two, with widow control) go to the next page
|
||||
// with the note.
|
||||
let mut moved = 0;
|
||||
for before in 0..=300 {
|
||||
let (d, t, id) = space_after_doc(before as f32, true);
|
||||
let l = lay(&d);
|
||||
let (pieces, n) = para_pieces(&l, t);
|
||||
assert!(n >= 4, "a paragraph of several lines: {n}");
|
||||
let &(page, _, l1, bottom) = pieces.last().unwrap();
|
||||
assert_eq!(l1, n);
|
||||
let note = note_pieces(&l, id.unwrap());
|
||||
assert_eq!(note.first().map(|p| p.0), Some(page), "before {before}: the note is on its reference's page");
|
||||
// The separator's area (default separator: NOTE_SEP high, its rule half way down) sits
|
||||
// right above the note.
|
||||
let sep_top = note[0].3 - NOTE_SEP;
|
||||
assert!(sep_top - bottom >= 6.0 - 0.01, "before {before}: last line ends at {bottom}, the separator starts at {sep_top}");
|
||||
if pieces.len() > 1 {
|
||||
assert_eq!(pieces.len(), 2, "{pieces:?}");
|
||||
assert!(pieces[0].2 - pieces[0].1 >= 2 && pieces[1].2 - pieces[1].1 >= 2, "widow/orphan control: {pieces:?}");
|
||||
moved += 1;
|
||||
}
|
||||
}
|
||||
assert!(moved > 0, "the sweep crosses the page bottom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_after_may_overflow_the_bottom_margin_without_notes() {
|
||||
// Without footnotes nothing changes: the last line only has to fit, its space after may run
|
||||
// past the bottom margin (Word).
|
||||
let mut tight = 0;
|
||||
for before in 0..=300 {
|
||||
let (d, t, _) = space_after_doc(before as f32, false);
|
||||
let l = lay(&d);
|
||||
let bottom_margin = d.last_section.page_h - d.last_section.margin_bottom;
|
||||
let (pieces, _) = para_pieces(&l, t);
|
||||
let &(_, _, _, bottom) = pieces.last().unwrap();
|
||||
assert!(bottom <= bottom_margin + 0.01, "before {before}: {bottom}");
|
||||
if bottom > bottom_margin - 6.0 {
|
||||
tight += 1;
|
||||
}
|
||||
}
|
||||
assert!(tight > 0, "some step leaves less than the space after below the last line");
|
||||
}
|
||||
|
||||
/// #1116: the sweep of `space_after_stays_clear_of_the_footnote_separator` with paragraphs `t`
|
||||
/// and `t + 1` changed by `tweak`: the last line of `t` stays `need` points clear of the separator.
|
||||
fn space_after_clear_with(tweak: impl Fn(&mut Document, usize), need: f32) {
|
||||
for before in 0..=300 {
|
||||
let (mut d, t, id) = space_after_doc(before as f32, true);
|
||||
tweak(&mut d, t);
|
||||
let l = lay(&d);
|
||||
let (pieces, _) = para_pieces(&l, t);
|
||||
let &(page, _, _, bottom) = pieces.last().unwrap();
|
||||
let note = note_pieces(&l, id.unwrap());
|
||||
assert_eq!(note.first().map(|p| p.0), Some(page), "before {before}: the note is on its reference's page");
|
||||
let sep_top = note[0].3 - NOTE_SEP;
|
||||
assert!(sep_top - bottom >= need - 0.01, "before {before}: last line ends at {bottom}, the separator starts at {sep_top}");
|
||||
}
|
||||
}
|
||||
|
||||
fn para_props(d: &mut Document, i: usize) -> &mut ParaProps {
|
||||
match d.body.get_mut(i).map(std::sync::Arc::make_mut) {
|
||||
Some(wordcraft_doc::Block::Para(p)) => &mut p.props,
|
||||
_ => panic!("no paragraph {i}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_after_stays_clear_when_only_the_next_paragraph_has_contextual_spacing() {
|
||||
// Contextual spacing on the next paragraph alone drops its space before, not this one's space
|
||||
// after (as the spacing between them): the space after still stays clear of the separator.
|
||||
space_after_clear_with(|d, t| para_props(d, t + 1).contextual_spacing = Some(true), 6.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() {
|
||||
// Automatic spacing drops only between two list items with labels: before a paragraph whose
|
||||
// list doesn't exist (no label) a list item's automatic space after stays clear.
|
||||
use wordcraft_doc::props::NumRef;
|
||||
space_after_clear_with(
|
||||
|d, t| {
|
||||
let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered);
|
||||
let p = para_props(d, t);
|
||||
p.numbering = Some(NumRef { num, level: 0 });
|
||||
p.space_after_auto = Some(true);
|
||||
para_props(d, t + 1).numbering = Some(NumRef { num: 9999, level: 0 });
|
||||
},
|
||||
wordcraft_doc::resolve::AUTO_SPACING,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (a dragged floating object anchors in the column it lands in, #884; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -37,7 +37,7 @@ font can't match Word's metrics.
|
||||
| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852) | 1–2 |
|
||||
| List labels: number formats (decimal, letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `layout::tests::chinese_counting_list_labels_and_their_font_runs`; a label mixing faces (digit + CJK comma) is drawn run by run (#534). Hebrew, Arabic, Hindi, Thai, Vietnamese and Russian formats show decimal | — |
|
||||
| Body top below a tall header, footer pushes body up | ✅ | #138; a footer taller than the bottom margin moves the body bottom up (#453), and a header or footer linked to the previous section takes its room too (#454) | — |
|
||||
| Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues); a long endnote continues on the next column or page at a line boundary (#456), and the caret finds note text on its continuation page (#457); per-section restart partial | 1–3 |
|
||||
| Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues); on a page with footnotes a paragraph ending there keeps its space after above the separator, or its last lines go to the next page with the note (#1116); a long endnote continues on the next column or page at a line boundary (#456), and the caret finds note text on its continuation page (#457); per-section restart partial | 1–3 |
|
||||
| Body top below a tall header, footer pushes body up | ✅ | #138 | — |
|
||||
| Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues; endnotes use the document's separator but don't continue onto a page of their own); each section's note options: footnotes below the text, endnotes at the end of each section, start at, numbering restarted each section or each page (counted from where the notes land, at most three passes) (#385) | 1–2 |
|
||||
| Tables: row heights (at least/exact), rows split across pages, header rows repeat, Can't Split | ✅ | 2026-10-06, #138 | — |
|
||||
@@ -80,6 +80,7 @@ font can't match Word's metrics.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | trivial | A shape or picture dragged into another column anchors there, so the text in that column wraps around it (#884) |
|
||||
| 2026-10-13 | trivial | On a page with footnotes, a paragraph that ends there keeps its space after above the footnote separator; if it doesn't fit, its last line (and, with widow control, one more) goes to the next page with the note. Pages without notes are unchanged (#1116) |
|
||||
| 2026-10-12 | trivial | Spaces, tabs and ideographic spaces don't count toward a line's height, as in Word (a line of only them takes the paragraph mark's) |
|
||||
| 2026-10-12 | trivial | A list label's font raises the first line's ascent but never its descent (Word keeps the text's descent below the baseline) |
|
||||
| 2026-10-12 | trivial | Hidden end-of-cell mark (`w:hideMark`): a cell's empty last paragraph takes no room, spacing and all; a row of only such empty cells is just its borders |
|
||||
|
||||
Reference in New Issue
Block a user