mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 20:00:11 -06:00
Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts: ; ROADMAP.md
This commit is contained in:
@@ -150,6 +150,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
|
||||
- 2026-11-08: Layout: a turned inline picture whose file keeps no room for the turn (no or a zero `wp:effectExtent`, as other tools write) keeps its line to its frame and overhangs it, cut at the line's top and bottom, as Word does: the frame as it is in Word 2007 layout, with width and height exchanged from 45° to 135° and 225° to 315° in Word 2013+ layout. Such a file saves the same way. Pictures with the room recorded (Word's own files) still take their rotated bounds. Readiness unchanged.
|
||||
- 2026-11-08: XML text and CDATA read with their line ends normalised (XML 1.0 §2.11): CR LF and a lone CR are one line feed, ` ` stays a CR, so a line break inside `w:t` in a file saved with Windows line ends is one space, not two (#5897); readiness estimates unchanged.
|
||||
- 2026-11-08: Layout: contextual spacing drops the space after a table cell's last paragraph when that paragraph is in the Normal style, the end-of-cell mark counting as a Normal paragraph next to it; a last paragraph in another style keeps its space after. The rule is inferred from docxide-pdf's notes and its `cell-end-*` fixtures (`cell-end-text-context`, `cell-end-empty-context`); a Word comparison that tells it apart from the neighbouring paragraph's style is still open. Readiness unchanged.
|
||||
- 2026-11-06: Arabic interface: the Home tab's Styles gallery reads from the right, as the rest of the mirrored ribbon does (#5566). Normal is the rightmost tile, the next styles follow leftwards and the scroll arrows and More sit at the gallery's left end; left to right is unchanged. Readiness unchanged.
|
||||
- 2026-11-05: ODT heading text of another size keeps it (gap 19): a smaller word in a heading came back at the heading's size in LibreOffice after a save.
|
||||
- 2026-11-05: ODT table rows kept from breaking across pages stay so (gap 19): they came back from a save free to break.
|
||||
@@ -167,6 +168,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
- 2026-11-07: DOCX: a VML text box keeps its text direction: `mso-layout-flow-alt:bottom-to-top` reads as Rotate all text 270° whatever `layout-flow` says, and otherwise `layout-flow:vertical` (or `vertical-ideographic`) as 90°, so the turned text box in a compatibility-mode document's header is drawn turned, as in Word. (#5193)
|
||||
- 2026-11-06: Change Picture keeps the picture's width and takes the new picture's aspect ratio, dropping the old crop, instead of stretching the new picture into the old frame (a revised figure came out about 2% taller in a real manuscript) (#5761). Readiness estimates unchanged.
|
||||
- 2026-11-06: Superscript and subscript text is set at 0.65 of its size rounded to the half point, as Word does (8 pt → 5, 11 → 7, 12 → 8, 24 → 15.5; measured in Word for Mac from digit advances with `cargo xtask word-compare`), not 0.65 exactly: lines with citation numbers and footnote references break where Word's do (the Arial superscript and author-line probes now match Word line for line). Readiness estimates unchanged.
|
||||
- 2026-11-06: Line numbers take the document's "Line Number" style over its defaults, as in Word (Aptos 12 pt black in a manuscript whose defaults are Aptos 12 pt, measured in Word for Mac), instead of a fixed 10 pt grey. Readiness estimates unchanged.
|
||||
- 2026-11-06: Windows print submissions run in separate helper processes (#5710). A helper acknowledges cancellation before it stops, so a driver stalled in a cancelled job cannot hold another job’s provider lock. A private temporary PDF and bounded settings messages cross the process boundary; normal shutdown reaps helpers and removes their inputs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Jobs already sending still complete. Readiness estimates unchanged.
|
||||
- 2026-11-06: File › Print: before sending starts, Cancel printing stops a waiting job, frees the Print page and keeps the settings; a successfully cancelled job is never sent. Cancellation remains available after the last document closes. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. Readiness estimates unchanged.
|
||||
- 2026-11-06: File › Print on Windows no longer waits on printer drivers to list printers (#5710). The list used to wait until each printer's driver said whether it prints on both sides, and a networked printer asleep in power save could keep it empty with Refresh unavailable; now discovery asks no driver (the Windows spooler still supplies the list), and printing still checks the job's options with the driver. The reporter's sleeping printer has not been retested yet. Readiness estimates unchanged.
|
||||
@@ -248,6 +250,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
- 2026-11-03: Security: `.doc` import attaches each section to the paragraph that ends it through an index of paragraph ends, instead of scanning every paragraph for every section, so a file with a thousand sections that end nowhere can't multiply the work over its paragraphs. Readiness unchanged.
|
||||
- 2026-11-03: Layout: a line can break after a hyphen that a slash and a letter follow ("Dataprotection-" / "/Secrecyviolation"), as Word does ("fitness-/1589", with a digit after the slash, stays whole), where UAX #14 forbids a break before the slash; an auto-fit table column holding such a compound wraps it instead of widening and taking the width from its neighbours. (#3408)
|
||||
- 2026-11-03: Layout: a section's closing paragraph whose mark is hidden and that holds only an in-line picture (or a note reference) is no longer laid out as an empty hidden mark. The picture keeps its room, so the next continuous section's text starts below it instead of being drawn over it. A paragraph with only floating objects and a hidden mark still takes no room. Not observed in Word. (#5288)
|
||||
- 2026-11-03: Security: DOCX import looks relationships up by id through an index built once per part, the first relationship with an id still winning, instead of scanning every relationship for each hyperlink, picture or embedded part that names one. Readiness unchanged.
|
||||
- 2026-11-03: Security: DOCX import no longer carries a file's part named like WordCraft's own bookkeeping (`wordcraft:unmodelled.roots` and the like), which a save read back as that bookkeeping, adding relationships the file chose. Readiness unchanged.
|
||||
- 2026-11-03: Language packs: language-packs-2 is published and WordCraft pins its index: 75 languages, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch (#4145, #2505) and Swedish (Sweden, Finland), which need craft-proof's flag-based compounding (storytold/craft-libs#497, pinned). English (Australia) is checked against SCOWL's en-AU list as well as en-GB's (storytold/craft-libs#496). Mongolian and Thai stay hidden. Readiness unchanged.
|
||||
- 2026-11-03: Language lists (CP4.3): a language installed from a Language pack (Bulgarian, say) can now be chosen. Set Proofing Language lists every built-in language and every installed or available pack's, each once and sorted by name; Review › Language and the status bar's menu add the installed packs' languages to the short list, with one way of working out the check mark, by tag. A pack's language is stored as a tag that finds the pack again (`sr-Latn-RS` is proofed by the Latin `sr-latn` pack, not the Cyrillic one; Valencian is Word's `ca-ES-valencia`). Every pack in language-packs-2 now has an English name (Belarusian used to show its own name, Беларуская). Readiness unchanged.
|
||||
@@ -869,6 +872,7 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-08 | trivial | Progress log: XML line ends in text normalised (#5897) |
|
||||
| 2026-11-08 | trivial | Progress log: contextual spacing drops the space after a cell's last Normal paragraph (#5450) |
|
||||
| 2026-11-06 | trivial | Progress log: the Arabic interface's Styles gallery reads from the right (#5566) |
|
||||
| 2026-11-07 | trivial | Page Setup also bounds the unfocused margin display after a long numeric paste; the accepted value is retained (#5650). |
|
||||
| 2026-11-05 | trivial | Page Setup's margin, paper-size and document-grid number boxes scroll long text inside the field instead of enlarging the dialog (#5650). Readiness estimates unchanged. |
|
||||
@@ -880,6 +884,7 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
| 2026-11-02 | trivial | Progress log: alternative format chunks and MHT open |
|
||||
| 2026-11-06 | trivial | Progress log: Change Picture keeps the width and takes the new picture's aspect ratio (#5761) |
|
||||
| 2026-11-06 | trivial | Progress log: superscript and subscript text at 0.65 of its size rounded to the half point (Word for Mac) |
|
||||
| 2026-11-06 | trivial | Progress log: line numbers take the document's "Line Number" style |
|
||||
| 2026-11-06 | trivial | Print helpers remove loaded PDF/settings files before driver work, tolerate prior removal and report cleanup errors; a held-driver regression checks the files are already absent (#5710) |
|
||||
| 2026-11-06 | trivial | Windows print submissions isolated per helper process; cancelled driver work cannot block a retry (#5710) |
|
||||
| 2026-11-06 | trivial | Progress log: print cancellation before sending, including without a document, and the Windows repeated-conversion limitation (#5710) |
|
||||
|
||||
@@ -350,6 +350,7 @@ impl Package {
|
||||
let path = resolve(dir, target);
|
||||
self.canonical(&path).map_or(path, str::to_string)
|
||||
};
|
||||
rels.ids.entry(id.to_string()).or_insert(rels.list.len());
|
||||
rels.list.push(Rel { id: id.to_string(), kind: r.attr("Type").unwrap_or("").to_string(), target: resolved, external });
|
||||
}
|
||||
rels
|
||||
@@ -405,11 +406,13 @@ pub struct Rel {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Rels {
|
||||
pub list: Vec<Rel>,
|
||||
/// Each id's first relationship in `list`, so a lookup doesn't scan every relationship.
|
||||
ids: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Rels {
|
||||
pub fn by_id(&self, id: &str) -> Option<&Rel> {
|
||||
self.list.iter().find(|r| r.id == id)
|
||||
self.ids.get(id).and_then(|&i| self.list.get(i))
|
||||
}
|
||||
pub fn by_type(&self, full: &str) -> Option<&Rel> {
|
||||
self.list.iter().find(|r| rel_is(&r.kind, full))
|
||||
@@ -547,6 +550,22 @@ mod tests {
|
||||
assert_eq!(crate::package_title(std::io::Cursor::new(z), 1 << 20).as_deref(), Some("Plan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relationships_are_looked_up_by_id_the_first_one_winning() {
|
||||
let mut rels = String::from(r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#);
|
||||
for i in 0..20_000 {
|
||||
rels.push_str(&format!(r#"<Relationship Id="rId{i}" Type="hyperlink" Target="https://example.com/{i}" TargetMode="External"/>"#));
|
||||
}
|
||||
rels.push_str(r#"<Relationship Id="rId7" Type="hyperlink" Target="https://example.com/dup" TargetMode="External"/></Relationships>"#);
|
||||
let z = zip_of(&[("word/_rels/document.xml.rels", rels.into_bytes())]);
|
||||
let r = Package::open(&z).unwrap().rels("word/document.xml");
|
||||
assert_eq!(r.by_id("rId7").map(|r| r.target.as_str()), Some("https://example.com/7"), "the first rId7 wins");
|
||||
assert_eq!(r.by_id("rId19999").map(|r| r.target.as_str()), Some("https://example.com/19999"));
|
||||
assert!(r.by_id("rId20000").is_none());
|
||||
// Hyperlink-style lookups, one per relationship, are each a map lookup.
|
||||
assert_eq!((0..20_000).filter(|i| r.by_id(&format!("rId{i}")).is_some()).count(), 20_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decompression_bomb_is_rejected() {
|
||||
let z = zip_of(&[("word/document.xml", vec![b' '; 2_000_000])]);
|
||||
|
||||
@@ -366,8 +366,12 @@ pub(crate) fn layout_box(ctx: &mut Ctx, input: BoxInput<'_>) -> BoxLayout {
|
||||
items.splice(at..at, back);
|
||||
items.extend(front);
|
||||
}
|
||||
// Automatic space after: none after the last paragraph of a cell or box.
|
||||
let after = if pl.rp.after_auto && i + 1 == blocks.len() { 0.0 } else { pl.rp.space_after };
|
||||
// Automatic space after: none after the last paragraph of a cell or box. Nor
|
||||
// contextual spacing after a cell's last paragraph in the default style (or an
|
||||
// undefined one, formatted as Normal): the end-of-cell mark is a Normal paragraph.
|
||||
let last = i + 1 == blocks.len();
|
||||
let normal = || pl.rp.style == "Normal" || ctx.doc.styles.get(&pl.rp.style).is_none();
|
||||
let after = if last && (pl.rp.after_auto || (in_cell && ctxl && normal())) { 0.0 } else { pl.rp.space_after };
|
||||
y += edges.top + pl.height + edges.bottom + after;
|
||||
prev_after = after;
|
||||
prev_after_auto = pl.rp.after_auto;
|
||||
|
||||
@@ -4,9 +4,12 @@ use super::headers::HfLinks;
|
||||
use super::placed::Placed;
|
||||
use super::{
|
||||
Anchor, Arc, Block, CharProps, Counters, DocLayout, Document, FieldCtx, Float, HashMap, HashSet, HideDeleted, InlineObject, LayoutOptions, Level,
|
||||
ParaLayout, Paragraph, Rect, Rgb, SectionProps, StoryRef, ViewMode, Wrap, contour, math, para, styleref, textbox,
|
||||
ParaLayout, Paragraph, Rect, SectionProps, StoryRef, ViewMode, Wrap, contour, math, para, styleref, textbox,
|
||||
};
|
||||
|
||||
/// The character style Word formats line numbers with (`w:styleId="LineNumber"`).
|
||||
const LINE_NUMBER_STYLE: &str = "LineNumber";
|
||||
|
||||
/// A layout pass: finished, or to be done again with the page totals it found, or with the
|
||||
/// floats placed above their anchors it found ([`EarlyFloats`]).
|
||||
pub(crate) enum Pass {
|
||||
@@ -193,10 +196,11 @@ impl Ctx<'_> {
|
||||
if let Some(p) = self.numbers.get(&n) {
|
||||
return p.clone();
|
||||
}
|
||||
let mut p = Paragraph::with_text(
|
||||
&n.to_string(),
|
||||
CharProps { size: Some(10.0), color: Some(wordcraft_doc::TextColor::Rgb(Rgb(0x60, 0x60, 0x60))), ..Default::default() },
|
||||
);
|
||||
// Word formats line numbers with the "Line Number" character style over the document's
|
||||
// defaults (measured in Word for Mac: Aptos 12 pt black in a document whose defaults are
|
||||
// Aptos 12 pt), not in a fixed small grey.
|
||||
let style = self.doc.styles.get(LINE_NUMBER_STYLE).map(|s| s.id.clone());
|
||||
let mut p = Paragraph::with_text(&n.to_string(), CharProps { style, ..Default::default() });
|
||||
p.props.space_after = Some(0.0);
|
||||
p.props.space_before = Some(0.0);
|
||||
let env = para::ParaEnv {
|
||||
|
||||
@@ -1201,14 +1201,16 @@ fn align_in_part(
|
||||
}
|
||||
let limit = cut - row.bottom.clamp(0.0, cut);
|
||||
let mut kept = f32::MIN;
|
||||
for ((items, splits), _) in cells.iter().zip(splits).zip(realign).filter(|(_, al)| al.is_none()) {
|
||||
for (((items, splits), _), end) in cells.iter().zip(splits).zip(realign).zip(ends.iter()).filter(|((_, al), _)| al.is_none()) {
|
||||
// No further than the cell's content: its last paragraph may have dropped its space after.
|
||||
let end = end.unwrap_or(f32::MAX);
|
||||
for (it, &split) in items.iter().zip(splits) {
|
||||
let bottom = match (it.turned_bounds(), it) {
|
||||
(Some(r), _) => Some(r.bottom()),
|
||||
// A paragraph kept whole ends with its space after.
|
||||
(None, Placed::Lines { para, l0, y, .. }) if split > *l0 => crate::item_bottom(*y, para, *l0, split)
|
||||
.filter(|&b| b <= limit + 0.01)
|
||||
.map(|b| if split == para.lines.len() { (b + para.rp.space_after).min(limit) } else { b }),
|
||||
.map(|b| if split == para.lines.len() { (b + para.rp.space_after).min(end.max(b)).min(limit) } else { b }),
|
||||
(None, Placed::Image { rect, .. } | Placed::Shape { rect, .. } | Placed::Graphic { rect, .. } | Placed::Clip { rect, .. }) => {
|
||||
Some(rect.bottom())
|
||||
}
|
||||
|
||||
@@ -298,6 +298,82 @@ fn legacy_table_contextual_spacing_flag_is_scoped_to_cells_and_legacy_mode() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Baseline of the paragraph after a one-row table, less that of the first cell's last
|
||||
/// paragraph, which has 12 pt after in `style`, contextual or not. `next_cell` adds a second cell
|
||||
/// whose paragraph has that style; `after_table` styles the paragraph after the table; `text` is
|
||||
/// the last paragraph's text.
|
||||
fn cell_end_gap(style: &str, contextual: bool) -> f32 {
|
||||
cell_end_gap_in(Document::new(), style, contextual, None, None, "Beta probe")
|
||||
}
|
||||
|
||||
fn cell_end_gap_in(mut d: Document, style: &str, contextual: bool, next_cell: Option<&str>, after_table: Option<&str>, text: &str) -> f32 {
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
|
||||
id: "Other".into(),
|
||||
name: "Other".into(),
|
||||
based_on: Some("Normal".into()),
|
||||
..Default::default()
|
||||
});
|
||||
let chr = wordcraft_doc::CharProps { font: Some("Source Sans 3".into()), size: Some(12.0), ..Default::default() };
|
||||
let props = ParaProps { space_before: Some(0.0), space_after: Some(0.0), line_spacing: Some(LineSpacing::Exactly(24.0)), ..Default::default() };
|
||||
let para = |text: &str, props: ParaProps| {
|
||||
let mut p = wordcraft_doc::Paragraph::with_text(text, chr.clone());
|
||||
p.props = props;
|
||||
wordcraft_doc::para_block(p)
|
||||
};
|
||||
let styled = |s: Option<&str>| ParaProps { style: s.map(Into::into), ..props.clone() };
|
||||
let mut table = Table::new(1, if next_cell.is_some() { 2 } else { 1 }, 468.0);
|
||||
table.props.style = None;
|
||||
table.props.cell_margins = Some([0.0; 4]);
|
||||
let last = ParaProps { style: Some(style.into()), space_after: Some(12.0), contextual_spacing: Some(contextual), ..props.clone() };
|
||||
table.rows[0].cells[0].blocks = vec![para("Alpha probe", props.clone()), para(text, last)];
|
||||
if let Some(cell) = table.rows[0].cells.get_mut(1) {
|
||||
cell.blocks = vec![para("Delta probe", styled(next_cell))];
|
||||
}
|
||||
d.body = vec![std::sync::Arc::new(wordcraft_doc::Block::Table(table)), para("Gamma probe", styled(after_table))];
|
||||
let l = lay(&d);
|
||||
let baseline = |at: &[u32]| {
|
||||
l.pages[0].items.iter().find_map(|i| match i {
|
||||
Placed::Lines { para, y, path, .. } if path.0 == at => Some(y + para.lines[0].baseline),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let (Some(cell), Some(after)) = (baseline(&[0, 0, 0, 1]), baseline(&[1])) else { panic!("probe lines missing") };
|
||||
after - cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_spacing_drops_the_space_after_a_cells_last_normal_paragraph() {
|
||||
// The end-of-cell mark is a Normal paragraph: contextual spacing drops the 12 pt after a
|
||||
// Normal last paragraph (docxide-pdf `cell-end-text-context`, `cell-end-empty-context`), not
|
||||
// after one in another style (`cell-end-other-style`) or without contextual spacing.
|
||||
let kept = cell_end_gap("Normal", false);
|
||||
assert!((cell_end_gap("Other", true) - kept).abs() < 0.01);
|
||||
let dropped = cell_end_gap("Normal", true);
|
||||
assert!((kept - dropped - 12.0).abs() < 0.01, "kept {kept}, dropped {dropped}");
|
||||
// An undefined style is formatted as Normal, and drops it too.
|
||||
assert!((cell_end_gap("Missing", true) - dropped).abs() < 0.01);
|
||||
// The mark decides, not a neighbour: another style in the next cell or after the table
|
||||
// leaves the drop as it is.
|
||||
for (next_cell, after_table) in [(Some("Other"), None), (None, Some("Other"))] {
|
||||
let gap = cell_end_gap_in(Document::new(), "Normal", true, next_cell, after_table, "Beta probe");
|
||||
assert!((gap - dropped).abs() < 0.01, "{next_cell:?} {after_table:?}: {gap}, dropped {dropped}");
|
||||
}
|
||||
// And the reverse: another style keeps its space beside Normal neighbours.
|
||||
let gap = cell_end_gap_in(Document::new(), "Other", true, Some("Normal"), Some("Normal"), "Beta probe");
|
||||
assert!((gap - kept).abs() < 0.01, "other beside Normal: {gap}, kept {kept}");
|
||||
// An empty last paragraph drops it too (`cell-end-empty-context`).
|
||||
let gap = cell_end_gap_in(Document::new(), "Normal", true, None, None, "");
|
||||
assert!((gap - dropped).abs() < 0.01, "empty: {gap}, dropped {dropped}");
|
||||
// Where `allowSpaceOfSameStyleInTable` keeps same-style space in cells (modes 11/12/14), the
|
||||
// 12 pt stays.
|
||||
let mut legacy = Document::new();
|
||||
legacy.settings.compat_mode = 14;
|
||||
legacy.settings.allow_space_of_same_style_in_table = true;
|
||||
let gap = cell_end_gap_in(legacy, "Normal", true, None, None, "Beta probe");
|
||||
assert!((gap - kept).abs() < 0.01, "legacy {gap}, kept {kept}");
|
||||
}
|
||||
|
||||
fn contextual_gap(after: f32, before: f32, first: bool, second: bool, different_style: bool, header: bool) -> f32 {
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
let mut d = Document::new();
|
||||
@@ -3851,6 +3927,40 @@ fn extreme_page_and_line_number_starts_do_not_overflow() {
|
||||
assert!(l.pages.len() >= 3, "{} pages", l.pages.len());
|
||||
}
|
||||
|
||||
/// Line numbers take the document's character formatting through the "Line Number" style, as in
|
||||
/// Word (Aptos 12 pt black in a document whose defaults are Aptos 12 pt), not a fixed small grey.
|
||||
#[test]
|
||||
fn line_numbers_follow_the_line_number_style() {
|
||||
let number_runs = |d: &Document| {
|
||||
let l = lay(d);
|
||||
l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|it| match it {
|
||||
Placed::Lines { story, para, .. } if *story == DECOR_STORY => para.styles.first().map(|s| (s.size, s.rc.color)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut d = Document::from_text("one\ntwo\nthree");
|
||||
d.last_section.line_numbers = Some(Default::default());
|
||||
Arc::make_mut(&mut d.styles).default_chr.size = Some(12.0);
|
||||
let plain = number_runs(&d);
|
||||
assert_eq!(plain.len(), 3);
|
||||
assert!(plain.iter().all(|(size, color)| *size == 12.0 && *color == wordcraft_doc::TextColor::Auto), "{plain:?}");
|
||||
|
||||
let red = wordcraft_doc::TextColor::Rgb(wordcraft_doc::Rgb(0xC0, 0, 0));
|
||||
Arc::make_mut(&mut d.styles).upsert(wordcraft_doc::Style {
|
||||
id: "LineNumber".into(),
|
||||
name: "line number".into(),
|
||||
kind: wordcraft_doc::StyleKind::Character,
|
||||
chr: CharProps { size: Some(8.0), color: Some(red), ..Default::default() },
|
||||
..Default::default()
|
||||
});
|
||||
let styled = number_runs(&d);
|
||||
assert!(styled.iter().all(|(size, color)| *size == 8.0 && *color == red), "{styled:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_numbers_borders_text_boxes() {
|
||||
let mut d = Document::from_text("one\ntwo\nthree");
|
||||
@@ -12152,6 +12262,88 @@ fn a_middle_or_bottom_cell_of_a_split_row_lines_up_on_the_first_page() {
|
||||
assert_eq!(pages[1][0].len(), 25 - labels.len(), "{pages:?}");
|
||||
}
|
||||
|
||||
/// The line tops on the first page of a row split across pages, by cell: a label aligned with
|
||||
/// `valign`, 30 lines that split, and a 160 pt spacer above `note`.
|
||||
fn split_row_beside_a_note(valign: wordcraft_doc::props::VAlign, note: &ParaProps) -> [Vec<f32>; 3] {
|
||||
use wordcraft_doc::props::LineSpacing;
|
||||
let plain = split_note_plain();
|
||||
let para = |text: &str, props: ParaProps| {
|
||||
let mut p = Paragraph::with_text(text, Default::default());
|
||||
p.props = props;
|
||||
wordcraft_doc::para_block(p)
|
||||
};
|
||||
let mut d = Document::new();
|
||||
(d.last_section.page_w, d.last_section.page_h) = (400.0, 360.0);
|
||||
d.last_section.margin_top = 36.0;
|
||||
d.last_section.margin_bottom = 36.0;
|
||||
let mut t = Table::new(1, 3, 328.0);
|
||||
t.props.style = None;
|
||||
t.props.cell_margins = Some([0.0; 4]);
|
||||
t.rows[0].cells[0].props.valign = Some(valign);
|
||||
t.rows[0].cells[0].blocks = vec![para("Label", plain.clone())];
|
||||
t.rows[0].cells[1].blocks = (0..30).map(|i| para(&format!("Line {i}"), plain.clone())).collect();
|
||||
let spacer = ParaProps { line_spacing: Some(LineSpacing::Exactly(160.0)), ..plain.clone() };
|
||||
t.rows[0].cells[2].blocks = vec![para("Spacer", spacer), para("Note", note.clone())];
|
||||
d.body = vec![para("Filler", ParaProps { line_spacing: Some(LineSpacing::Exactly(100.0)), ..plain }), Arc::new(Block::Table(t))];
|
||||
let l = lay(&d);
|
||||
let tops = |cell: u32| -> Vec<f32> {
|
||||
l.pages[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
Placed::Lines { path, y, .. } if path.0.len() == 4 && path.0[0] == 1 && path.0[2] == cell => Some(*y),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let cells = [tops(0), tops(1), tops(2)];
|
||||
assert!(cells[1].len() > 5 && cells[1].len() < 30, "the row splits: {cells:?}");
|
||||
assert_eq!(cells[2].len(), 2, "the note stays on the page: {cells:?}");
|
||||
cells
|
||||
}
|
||||
|
||||
/// Single 12 pt lines with no space around them.
|
||||
fn split_note_plain() -> ParaProps {
|
||||
ParaProps {
|
||||
space_before: Some(0.0),
|
||||
space_after: Some(0.0),
|
||||
line_spacing: Some(wordcraft_doc::props::LineSpacing::Exactly(12.0)),
|
||||
widow_control: Some(false),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bottom_cell_of_a_split_row_ignores_a_dropped_space_after_beside_it() {
|
||||
use wordcraft_doc::props::VAlign;
|
||||
// The note's space after (contextual 300 pt, or automatic) drops at the cell's end and would
|
||||
// otherwise reach below the split lines: a bottom label lines up with those, a middle one is
|
||||
// centred between their top and bottom.
|
||||
let plain = split_note_plain();
|
||||
let contextual = ParaProps { space_after: Some(300.0), contextual_spacing: Some(true), ..plain.clone() };
|
||||
let automatic = ParaProps { space_after_auto: Some(true), ..plain };
|
||||
for note in [contextual, automatic] {
|
||||
let [labels, lines, _] = split_row_beside_a_note(VAlign::Bottom, ¬e);
|
||||
assert_eq!(labels.last(), lines.last(), "{note:?}: labels {labels:?}, lines {lines:?}");
|
||||
let [labels, lines, _] = split_row_beside_a_note(VAlign::Center, ¬e);
|
||||
let (Some(&first), Some(&last), Some(&label)) = (lines.first(), lines.last(), labels.first()) else { panic!("{labels:?} {lines:?}") };
|
||||
let want = first + (last - first) / 2.0;
|
||||
assert!((label - want).abs() < 0.01, "{note:?}: centred label at {label}, want {want}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bottom_cell_of_a_split_row_lines_up_with_a_kept_space_after_beside_it() {
|
||||
use wordcraft_doc::props::VAlign;
|
||||
// A note that keeps its 14 pt after (no contextual spacing) still ends there: the bottom
|
||||
// label's line ends 14 pt below the note's.
|
||||
let note = ParaProps { space_after: Some(14.0), ..split_note_plain() };
|
||||
let [labels, lines, notes] = split_row_beside_a_note(VAlign::Bottom, ¬e);
|
||||
let (Some(&label), Some(¬e_top), Some(&line)) = (labels.last(), notes.last(), lines.last()) else { panic!("{labels:?} {notes:?}") };
|
||||
assert!(note_top + 14.0 > line, "the note's space after reaches below the split lines: {notes:?} {lines:?}");
|
||||
assert!((label - (note_top + 14.0)).abs() < 0.01, "label at {label}, note at {note_top}");
|
||||
}
|
||||
|
||||
fn row_keep_pages(d: &Document) -> Vec<usize> {
|
||||
let l = lay(d);
|
||||
(0..d.table(StoryRef::Body, &Path::top(1)).unwrap().rows.len())
|
||||
|
||||
@@ -88,6 +88,8 @@ Page drawing indexes text-box warp lookups by paragraph and story for the curren
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options only partially covered; no page-by-page comparison harness | B | SECTIONPAGES now counts shared continuous-section pages in body and header/footer fields; legacy same-style cell spacing preserved for measured modes 11/12/14, with mode 15 and non-cell controls; Contextual spacing corrected in body/header probes and repeated table-cell runs; [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps; non-overlapping body floating tables clear header tables; stray labels on page-break-only list paragraphs fixed multi-paragraph field-code blank lines fixed; centred numbered heading labels after leading page breaks fixed (#2964); Arial Unicode MS no longer selects monospace substitutes; a slash opening a word travels whole to the next line (#2963); a hyphen before a slash and a letter is also a break opportunity (#3408), while a hyphen before a slash and a digit stays whole A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, so a TOC entry's page number keeps its leader (#1840). | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options only partially covered; no page-by-page comparison harness | B | SECTIONPAGES now counts shared continuous-section pages in body and header/footer fields; legacy same-style cell spacing preserved for measured modes 11/12/14, with mode 15 and non-cell controls; Contextual spacing corrected in body/header probes and repeated table-cell runs; [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps; non-overlapping body floating tables clear header tables; stray labels on page-break-only list paragraphs fixed multi-paragraph field-code blank lines fixed; centred numbered heading labels after leading page breaks fixed (#2964); Arial Unicode MS no longer selects monospace substitutes; a slash opening a word travels whole to the next line (#2963); a nested table's Can't Split row moves on with its outer row when the row splits across pages (#5456) A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, so a TOC entry's page number keeps its leader (#1840). | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options only partially covered; no page-by-page comparison harness | B | SECTIONPAGES now counts shared continuous-section pages in body and header/footer fields; legacy same-style cell spacing preserved for measured modes 11/12/14, with mode 15 and non-cell controls; Contextual spacing corrected in body/header probes and repeated table-cell runs; [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps; non-overlapping body floating tables clear header tables; stray labels on page-break-only list paragraphs fixed multi-paragraph field-code blank lines fixed; centred numbered heading labels after leading page breaks fixed (#2964); Arial Unicode MS no longer selects monospace substitutes; a slash opening a word travels whole to the next line (#2963); a hyphen before a slash and a letter is also a break opportunity (#3408), while a hyphen before a slash and a digit stays whole; a full stop followed by an opening parenthesis stays on one line (#5437) A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, so a TOC entry's page number keeps its leader (#1840). | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options only partially covered; no page-by-page comparison harness | B | In-line pictures raised or lowered (`w:position`) set their line's height as in Word, past their own height too (#3984, #5796). SECTIONPAGES now counts shared continuous-section pages in body and header/footer fields; legacy same-style cell spacing preserved for measured modes 11/12/14, with mode 15 and non-cell controls; Contextual spacing corrected in body/header probes and repeated table-cell runs; a cell's last Normal paragraph drops its contextual space after (#5450); [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps; non-overlapping body floating tables clear header tables; stray labels on page-break-only list paragraphs fixed multi-paragraph field-code blank lines fixed; centred numbered heading labels after leading page breaks fixed (#2964); Arial Unicode MS no longer selects monospace substitutes; a slash opening a word travels whole to the next line (#2963) A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, so a TOC entry's page number keeps its leader (#1840). A floating object anchored on a later page of a long paragraph is drawn on that page, also when footnotes or widow control move its line on (#4250). Fonts the document uses that aren't installed are flagged in the font box and status bar, except those drawn in a metric-compatible substitute (Carlito for Calibri, Liberation Sans for Arial) and symbol-font text (Symbol, Wingdings, Webdings), so the user knows when line breaks can differ from where the document was made; `font.missing` lists the substituted fonts in the text the current view lays out (capped, for hostile documents), marking the metric-compatible ones `metricCompatible`. | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options only partially covered; no page-by-page comparison harness | B | SECTIONPAGES now counts shared continuous-section pages in body and header/footer fields; legacy same-style cell spacing preserved for measured modes 11/12/14, with mode 15 and non-cell controls; Contextual spacing corrected in body/header probes and repeated table-cell runs; a cell's last Normal paragraph drops its contextual space after (#5450); [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps; non-overlapping body floating tables clear header tables; stray labels on page-break-only list paragraphs fixed multi-paragraph field-code blank lines fixed; centred numbered heading labels after leading page breaks fixed (#2964); Arial Unicode MS no longer selects monospace substitutes; a slash opening a word travels whole to the next line (#2963) A right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, so a TOC entry's page number keeps its leader (#1840). A floating object anchored on a later page of a long paragraph is drawn on that page, also when footnotes or widow control move its line on (#4250); one anchored right after a page break, before text, is drawn on the new page (#4262). Fonts the document uses that aren't installed are flagged in the font box and status bar, except those drawn in a metric-compatible substitute (Carlito for Calibri, Liberation Sans for Arial) and symbol-font text (Symbol, Wingdings, Webdings), so the user knows when line breaks can differ from where the document was made; `font.missing` lists the substituted fonts in the text the current view lays out (capped, for hostile documents), marking the metric-compatible ones `metricCompatible`. | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
|
||||
| 2 | **Word's native ink is not drawn from its strokes**; OLE objects show only their picture | B | Every native ink form is preserved: drawing-level ink with its optional picture fallback, direct run-level content parts, and ink members of groups and drawing canvases (kept in their place among the members, drawn from their fallback picture). Drawing canvases are read and written (#344); group shapes (`wpg`) read and written (#267). Charts and SmartArt are drawn (#292) and, with OLE objects, written back with all their parts on save (#319) | Pen notes from Word show only as their picture and can't be edited; OLE objects can't be opened | 45–70 to render and edit charts and SmartArt (#13) | [file-format-parity.md](file-format-parity.md) |
|
||||
| 5 | **Content controls** landed (#381): `w:sdt` kept, shown and edited (Developer tab, Design Mode, Properties). Move tracking landed (#400): `w:moveFrom`/`w:moveTo` with their named ranges round-trip, a tracked cut and paste of a paragraph or sentence records a move, either end accepts/rejects both, green double strike/underline markup and "Moved from/to" balloons; Track Changes Options has the Moves rows (Moved from / Moved to mark and colour) and `w:doNotTrackMoves` (Track moves off: a cut and paste is a deletion and an insertion) round-trips; still missing: drag-and-drop moves (no text drag yet). Formatting revisions landed (#41): `w:rPrChange`/`w:pPrChange`/`w:tblPrChange`/`w:trPrChange`/`w:tcPrChange`/`w:sectPrChange`/`w:numberingChange` round-trip, are recorded while tracking, accepted/rejected and shown in balloons; still missing: tracked style-definition changes (table, row, cell and section property changes now record in WordCraft too, #41 follow-up). Opaque custom XML backing stores now survive saves; binding evaluation remains absent. Replace All now checks content-control locks before editing any match; a refused batch preserves the document and undo history. | B | List-label pitch corrected in synthetic metric controls; Issues #41, #400; source grep | Legal and editorial review workflows; templates with form controls | 5–8 | [file-format-parity.md](file-format-parity.md) |
|
||||
| 6 | **Print dialog depth**: desktop printer choice, copies, collation, duplex and grayscale submit through craft-print; File › Print retains preview, page ranges and odd/even (#641) and scales pages to another paper size (#2108); print-only N-up and booklets are available; driver tray/options remain; web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
|
||||
@@ -195,6 +197,7 @@ Page Setup's margin, paper-size and document-grid number boxes scroll long text
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-08 | trivial | #10 narrowed: a turned inline picture with no recorded effect extent keeps its line to its frame (exchanged near a quarter turn in Word 2013+ layout), cut at the line; gap stays open |
|
||||
| 2026-11-08 | trivial | #4 narrowed: a cell's last Normal paragraph drops its contextual space after, the end-of-cell mark counting as a Normal paragraph (#5450) |
|
||||
| 2026-11-07 | trivial | Page Setup also bounds the unfocused margin display after a long numeric paste; the accepted value is retained (#5650). |
|
||||
| 2026-11-05 | trivial | Page Setup's margin, paper-size and document-grid number boxes scroll long text inside the field instead of enlarging the dialog (#5650). Readiness estimates unchanged. |
|
||||
| 2026-11-04 | trivial | Field argument import and page-format-aware layout/cache follow-ups; no ranked gap changed |
|
||||
|
||||
@@ -46,6 +46,7 @@ Exact line spacing keeps the run font baseline around tall equations; automatic
|
||||
| List bullets stored as a private-use character (U+E000–U+F8FF) in a font that isn't installed and isn't a mapped symbol-font code: drawn as the level's default bullet (• ○ ▪), not as whatever an installed icon font has at that code | ✅ | `layout::tests::private_use_bullet_no_font_draws_shows_the_levels_default_bullet` (#1683: U+F081 showed Font Awesome's Twitter logo) | — |
|
||||
| Indents in characters (`w:firstLineChars`, `w:leftChars`…), measured in the paragraph mark's font size | ✅ | #534, `layout::tests::character_indents_use_the_paragraph_mark_size`; spacing in lines (`w:beforeLines`/`w:afterLines`) uses the stored points (needs the document grid) | — |
|
||||
| Line spacing (single, 1.5, double, at least, exactly, multiple), spacing before/after, contextual spacing | 🟡 | Contextual spacing measured in body paragraphs and headers (below), with a four-paragraph table-cell regression; list-label ascent overflow is unscaled by multiple spacing and uses descent for automatic and at-least spacing (#601); a superscript or subscript sizes its line at the full font size and its raise or drop doesn't grow the line, while a `w:position` raise or lowering does, and its text is set at 0.65 of its size rounded to the half point (Word for Mac); other line-spacing combinations remain (#108, #135 open) | 3–5 |
|
||||
| Line spacing (single, 1.5, double, at least, exactly, multiple), spacing before/after, contextual spacing | 🟡 | Contextual spacing measured in body paragraphs and headers (below), with a four-paragraph table-cell regression; a cell's last paragraph with contextual spacing, in the Normal style (or an undefined one), drops its space after, the end-of-cell mark counting as a Normal paragraph beside it (not where `allowSpaceOfSameStyleInTable` keeps same-style space in modes 11/12/14); this rule is inferred from docxide-pdf's notes and its `cell-end-*` fixtures, and a Word comparison that tells it apart from the neighbouring paragraph's style is still open; a middle or bottom cell of a split row lines up without that dropped space (`contextual_spacing_drops_the_space_after_a_cells_last_normal_paragraph`, `a_bottom_cell_of_a_split_row_ignores_a_dropped_space_after_beside_it`, `a_bottom_cell_of_a_split_row_lines_up_with_a_kept_space_after_beside_it`); list-label ascent overflow is unscaled by multiple spacing and uses descent for automatic and at-least spacing (#601); a superscript or subscript sizes its line at the full font size and its raise or drop doesn't grow the line, while a `w:position` raise or lowering does (Word for Mac); other line-spacing combinations remain (#108, #135 open) | 3–5 |
|
||||
| Kerning only where Kerning for fonts asks for it (`w:kern`, at or above its size) | ✅ | Measured in Word for Windows: no `w:kern` or `w:kern="0"` is unkerned; WordCraft kerned every run, so its lines held more text than Word's. `layout::tests::kerning_follows_kerning_for_fonts_like_word` | — |
|
||||
| OpenType ligatures as the document asks (`w14:ligatures`: standard `liga`, contextual `clig`, historical `hlig`, discretional `dlig`, any combination, in runs, styles and defaults) | 🟡 | `layout::tests::ligatures_follow_w14_ligatures`, `ligature_flags_map_to_opentype_features`. Without the property WordCraft keeps drawing standard and contextual ligatures, while Word draws none; the Font dialog's Ligatures choice is still missing | 1 |
|
||||
| Automatic (HTML-style) paragraph spacing (`w:beforeAutospacing`, `w:afterAutospacing`) | ✅ | Measured in Word for Windows: 14 pt at any size, none at the top of the document or a cell or after a cell's last paragraph, the larger of two neighbouring spaces. `layout::tests::automatic_paragraph_spacing_like_word`, `…_drops_at_cell_edges` | — |
|
||||
@@ -305,11 +306,13 @@ The front-of-text opt-out is retained from the earlier implementation; this patc
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-08 | trivial | Turned inline pictures with no recorded effect extent keep their line to the frame (exchanged near a quarter turn in Word 2013+ layout) and overhang it, cut at the line's top and bottom, as in Word |
|
||||
| 2026-11-08 | trivial | Contextual spacing drops the space after a table cell's last paragraph in the Normal style, the end-of-cell mark counting as a Normal paragraph; another style keeps it, and a middle or bottom cell of a split row lines up without the dropped space (#5450) (`layout::tests::contextual_spacing_drops_the_space_after_a_cells_last_normal_paragraph`, `a_bottom_cell_of_a_split_row_ignores_a_dropped_space_after_beside_it`). |
|
||||
| 2026-11-04 | trivial | Header table measurements and fitted text-box heights use the page section’s PAGE format; cache reuse follows the field context |
|
||||
| 2026-11-03 | trivial | Body PAGE fields in tables use the active section number format; reflow views measure headers and footers with it before body placement |
|
||||
| 2026-11-06 | trivial | An in-line picture lowered further than its own height (`w:position`) was held with its top on the baseline and its line only as deep as the picture; Word keeps the top there but lowers the line as far as the position says. A 6 pt picture lowered 30 pt now makes a 30 pt line alone and the text's ascent plus 30 pt beside text, as in Word. This closes the gap noted on 2026-11-05: all 156 synthetic lines measured then now match Word within 0.35 pt, heights and picture positions. Exact spacing keeps the line's baseline as before (`tests::lowered_pictures_and_check_boxes_take_words_line_heights`, `tests::an_inline_picture_moves_with_its_runs_position`). |
|
||||
| 2026-10-31 | trivial | List labels: a level with numFmt none is an ordinary level whose own number reads as nothing: it shows its literal text (`RESPONSE:`), counts and restarts the levels below it, stays empty in legal numbering, and an empty label still tabs the text to the indent, as in Word (#4985) |
|
||||
| 2026-11-06 | trivial | Superscript and subscript text: Word for Mac sets it at 0.65 of the run's size rounded to the half point (from digit advances in its PDF export, base sizes 8–36 pt: 8 → 5, 9 → 6, 10 → 6.5, 11 → 7, 12 → 8, 13 → 8.5, 14 → 9, 16 → 10.5, 20 → 13, 24 → 15.5, 36 → 23.5). WordCraft used 0.65 exactly (11 pt → 7.15), so lines with superscripts came out up to 0.5 pt wider and some broke a word early; the `cargo xtask word-compare` superscript and author-line probes in Arial go from 95–97% of lines with Word's breaks to 100%. `doc::resolve::tests::raised_and_lowered_text_is_sized_to_the_half_point`, `layout::tests::superscript_digits_advance_at_the_half_point_size` |
|
||||
| 2026-11-06 | trivial | Line numbers are laid out with the document's "Line Number" character style over its defaults, as Word draws them, not in a fixed 10 pt grey (`layout::tests::line_numbers_follow_the_line_number_style`) |
|
||||
| 2026-11-05 | trivial | PR #4545: eligible cell floats retain in-bounds offsets and touch the outer cell edge only on overflow, ignoring wrap distances for object placement. Front-of-text, turned-cell and outside-cell opt-outs remain. Synthetic geometry controls; native front/behind-text parity remains unqualified. |
|
||||
| 2026-11-05 | trivial | With multiple spacing a line of only in-line pictures is as tall as they reach, raised or lowered, and never less than its font's height (a list label's font counting too): a picture shorter than the font keeps the font's ascent and descent, as noted for ODT documents below; a taller one ends at the line's bottom with no descent under it. The spacing adds its share of the font's height, not of a lowered picture's. A legacy check box sizes its line as text of its size. Measured in Word with Arial at 10 and 20 pt, single, 1.15, 1.25 and 1.5 spacing, 156 synthetic lines (`tests::lowered_pictures_and_check_boxes_take_words_line_heights`). Known gap: a picture lowered further than its own height is limited to that height where Word's line keeps growing. |
|
||||
| 2026-11-04 | trivial | PR #4609: relative REF keeps context already displayed by the own or a needed ancestor label; six synthetic legal-template cases and separate-list context follow the Word for Windows measurements reported by @pakiat in PR discussion. Existing unknown-switch/cache boundaries and readiness estimates unchanged; no new native Word run. |
|
||||
|
||||
Reference in New Issue
Block a user