mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-08 19:51:32 +00:00
docx: keep a paragraph's text alignment (#5498)
* docx: keep a paragraph's text alignment w:textAlignment (ECMA-376 17.3.1.39) was not read, so a save dropped it. Word sizes a line by it when a list's bullet is larger than the text: without it, those lines grow and every page after them moves. ParaProps gains text_alignment, read and carried through styles, and written back after w:jc. Kept for the file only; the layout does not use it yet. * contributors: add entropybeater --------- Co-authored-by: Brian <255903314+entropybeater@users.noreply.github.com>
This commit is contained in:
1 parent
b3da7a4d36
commit
4123bdc09f
7 files changed
+115
-5
No files matched your search
@@ -26,7 +26,9 @@ pub use char_props::{
|
||||
CharProps, CombineBrackets, DEFAULT_FONT_SIZE, EastAsianLayout, EmphasisMark, Highlight, Ligatures, NumChange, PropChange, Underline, VertAlign,
|
||||
};
|
||||
pub use color::{Rgb, TextColor};
|
||||
pub use para_props::{Align, Border, BorderStyle, Borders, LineSpacing, NumRef, ParaProps, TAB_POS_EPS, TabAlign, TabLeader, TabStop, TextFrame};
|
||||
pub use para_props::{
|
||||
Align, Border, BorderStyle, Borders, LineSpacing, NumRef, ParaProps, TAB_POS_EPS, TabAlign, TabLeader, TabStop, TextAlignment, TextFrame,
|
||||
};
|
||||
pub use table_props::{
|
||||
CellPicture, CellProps, HeightRule, PictureRepeat, RowProps, TableFloat, TableLook, TableProps, TextDirection, TextVert, VAlign, VMerge,
|
||||
anchor_ooxml, valign_from_anchor,
|
||||
|
||||
@@ -26,6 +26,40 @@ impl Align {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vertical alignment of characters of different sizes on a line (`w:textAlignment`,
|
||||
/// ECMA-376 §17.3.1.39).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TextAlignment {
|
||||
Auto,
|
||||
Top,
|
||||
Center,
|
||||
Baseline,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl TextAlignment {
|
||||
pub fn ooxml(self) -> &'static str {
|
||||
match self {
|
||||
TextAlignment::Auto => "auto",
|
||||
TextAlignment::Top => "top",
|
||||
TextAlignment::Center => "center",
|
||||
TextAlignment::Baseline => "baseline",
|
||||
TextAlignment::Bottom => "bottom",
|
||||
}
|
||||
}
|
||||
pub fn from_ooxml(s: &str) -> Option<TextAlignment> {
|
||||
match s {
|
||||
"auto" => Some(TextAlignment::Auto),
|
||||
"top" => Some(TextAlignment::Top),
|
||||
"center" => Some(TextAlignment::Center),
|
||||
"baseline" => Some(TextAlignment::Baseline),
|
||||
"bottom" => Some(TextAlignment::Bottom),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Line spacing rule.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "rule", content = "value")]
|
||||
@@ -398,6 +432,10 @@ pub struct ParaProps {
|
||||
/// `w:snapToGrid`: lines snap to the section's document grid. Unset = on.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub snap_to_grid: Option<bool>,
|
||||
/// `w:textAlignment`: how characters of different sizes line up on a line. Kept for the
|
||||
/// file (not laid out yet); Word sizes a line by it, so dropping it changes Word's pages.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text_alignment: Option<TextAlignment>,
|
||||
}
|
||||
|
||||
impl ParaProps {
|
||||
@@ -418,7 +456,7 @@ impl ParaProps {
|
||||
}
|
||||
|
||||
fn overlay_script_layout(&mut self, patch: &ParaProps) {
|
||||
overlay_fields!(self, patch; bidi, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn);
|
||||
overlay_fields!(self, patch; bidi, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn, text_alignment);
|
||||
}
|
||||
pub fn overlaid(mut self, patch: &ParaProps) -> ParaProps {
|
||||
self.overlay(patch);
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::collections::HashMap;
|
||||
use wordcraft_doc::para::cap_alt;
|
||||
use wordcraft_doc::props::{
|
||||
Align, Border, BorderStyle, Borders, CellProps, CharProps, CombineBrackets, EastAsianLayout, HeightRule, Highlight, LineSpacing, NumRef,
|
||||
ParaProps, Rgb, RowProps, TabAlign, TabLeader, TabStop, TableFloat, TableLook, TableProps, TextColor, TextDirection, Underline, VAlign, VMerge,
|
||||
VertAlign,
|
||||
ParaProps, Rgb, RowProps, TabAlign, TabLeader, TabStop, TableFloat, TableLook, TableProps, TextAlignment, TextColor, TextDirection, Underline,
|
||||
VAlign, VMerge, VertAlign,
|
||||
};
|
||||
use wordcraft_doc::section::{
|
||||
Columns, DocGrid, DocGridType, LineNumberRestart, LineNumbering, MAX_CHAR_SPACE, MAX_GRID_PITCH, NotePos, NoteProps, NoteRestart, NumFormat,
|
||||
@@ -343,6 +343,7 @@ impl PropCtx {
|
||||
"w:autoSpaceDE" => p.auto_space_de = Some(on_off(k)),
|
||||
"w:autoSpaceDN" => p.auto_space_dn = Some(on_off(k)),
|
||||
"w:snapToGrid" => p.snap_to_grid = Some(on_off(k)),
|
||||
"w:textAlignment" => p.text_alignment = k.attr("w:val").and_then(TextAlignment::from_ooxml),
|
||||
"w:framePr" => {
|
||||
if let Some(kind @ ("drop" | "margin")) = k.attr("w:dropCap") {
|
||||
p.drop_cap = Some(k.attr("w:lines").and_then(u32_of).unwrap_or(3).clamp(1, 10) as u8);
|
||||
|
||||
@@ -287,6 +287,9 @@ pub fn ppr_inner(w: &mut W, p: &ParaProps, framed: bool, num_change: Option<&Cha
|
||||
if let Some(a) = p.align {
|
||||
w.val("w:jc", align_val(a));
|
||||
}
|
||||
if let Some(t) = p.text_alignment {
|
||||
w.val("w:textAlignment", t.ooxml());
|
||||
}
|
||||
if let Some(l) = p.outline_level {
|
||||
w.val("w:outlineLvl", &n(l.min(9) as i64));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ mod section_page_number_type;
|
||||
mod section_text_direction;
|
||||
mod section_valign;
|
||||
mod settings_merge;
|
||||
mod text_alignment;
|
||||
mod text_boxes;
|
||||
mod toc_ranges;
|
||||
mod undefined_style;
|
||||
|
||||
@@ -7,7 +7,7 @@ use wordcraft_doc::numbering::ListKind;
|
||||
use wordcraft_doc::para::{Anchor, Float, FloatAlign, NoteKind, ShapeKind, Wrap};
|
||||
use wordcraft_doc::props::{
|
||||
Align, Border, BorderStyle, Borders, CharProps, HeightRule, Highlight, LineSpacing, NumRef, ParaProps, Rgb, RowProps, TabAlign, TabLeader,
|
||||
TabStop, TableLook, TextColor, TextDirection, Underline, VAlign, VMerge, VertAlign,
|
||||
TabStop, TableLook, TextAlignment, TextColor, TextDirection, Underline, VAlign, VMerge, VertAlign,
|
||||
};
|
||||
use wordcraft_doc::section::{Columns, DocGrid, DocGridType, LineNumberRestart, LineNumbering, NumFormat, PageVAlign, SectionProps, SectionStart};
|
||||
use wordcraft_doc::styles::{Style, StyleKind};
|
||||
@@ -221,6 +221,7 @@ fn every_para_prop_round_trips() {
|
||||
num_change: None,
|
||||
fmt_change: None,
|
||||
snap_to_grid: Some(false),
|
||||
text_alignment: Some(TextAlignment::Top),
|
||||
};
|
||||
let variants = [
|
||||
ParaProps { line_spacing: Some(LineSpacing::AtLeast(14.0)), indent_first: Some(24.0), align: Some(Align::Center), ..Default::default() },
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//! A paragraph's vertical text alignment (`w:textAlignment`, ECMA-376 Part 1 §17.3.1.39, values
|
||||
//! from ST_TextAlignment): read and written back as read, in the schema's place after `w:jc`.
|
||||
//! Word sizes a line by it when a list label is larger than the text, so a save that drops it
|
||||
//! moves every line after. Fixtures are hand-written from the spec.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use wordcraft_doc::props::TextAlignment;
|
||||
|
||||
fn docx(body: &str) -> Vec<u8> {
|
||||
let root = r#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>"#;
|
||||
let doc = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>{body}</w:body></w:document>"#
|
||||
);
|
||||
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
|
||||
for (name, bytes) in [("_rels/.rels", root.as_bytes()), ("word/document.xml", doc.as_bytes())] {
|
||||
zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zw.write_all(bytes).unwrap();
|
||||
}
|
||||
zw.finish().unwrap().into_inner()
|
||||
}
|
||||
|
||||
fn document_xml(bytes: &[u8]) -> String {
|
||||
let mut z = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
|
||||
let mut s = String::new();
|
||||
z.by_name("word/document.xml").unwrap().read_to_string(&mut s).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_alignment_is_written_back_as_read() {
|
||||
let para = |ppr: &str, text: &str| format!(r#"<w:p><w:pPr>{ppr}</w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>"#);
|
||||
let body = [
|
||||
para(r#"<w:jc w:val="center"/><w:textAlignment w:val="top"/><w:outlineLvl w:val="1"/>"#, "A"),
|
||||
para(r#"<w:textAlignment w:val="center"/>"#, "B"),
|
||||
para(r#"<w:textAlignment w:val="baseline"/>"#, "C"),
|
||||
para(r#"<w:textAlignment w:val="bottom"/>"#, "D"),
|
||||
para(r#"<w:textAlignment w:val="auto"/>"#, "E"),
|
||||
para(r#"<w:textAlignment w:val="sideways"/>"#, "F"),
|
||||
para("", "G"),
|
||||
]
|
||||
.concat();
|
||||
let d = wordcraft_docx::read(&docx(&body)).unwrap();
|
||||
let got: Vec<Option<TextAlignment>> = d.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.text_alignment).collect();
|
||||
let want = vec![
|
||||
Some(TextAlignment::Top),
|
||||
Some(TextAlignment::Center),
|
||||
Some(TextAlignment::Baseline),
|
||||
Some(TextAlignment::Bottom),
|
||||
Some(TextAlignment::Auto),
|
||||
None,
|
||||
None,
|
||||
];
|
||||
assert_eq!(got, want);
|
||||
let out = wordcraft_docx::write(&d).unwrap();
|
||||
let xml = document_xml(&out);
|
||||
assert!(xml.contains(r#"<w:jc w:val="center"/><w:textAlignment w:val="top"/><w:outlineLvl w:val="1"/>"#), "{xml}");
|
||||
for v in ["center", "baseline", "bottom", "auto"] {
|
||||
assert_eq!(xml.matches(&format!(r#"<w:textAlignment w:val="{v}"/>"#)).count(), 1, "{v}: {xml}");
|
||||
}
|
||||
assert_eq!(xml.matches("w:textAlignment").count(), 5, "an unknown value and none write nothing: {xml}");
|
||||
let back = wordcraft_docx::read(&out).unwrap();
|
||||
assert_eq!(back.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.text_alignment).collect::<Vec<_>>(), want);
|
||||
}
|
||||
Reference in new issue
Block a user