mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 18:36:58 -06:00
fix(formats): support cell and paragraph borders in RTF export (#2207)
* fix(formats): support cell and paragraph borders in RTF export * rustfmt --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
9cd34ebc80
commit
e04dd0e785
@@ -873,6 +873,7 @@ pub fn flow_paras(doc: &Document, p: &Paragraph) -> Vec<Para> {
|
||||
fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
|
||||
let (kind, list) = para_kind(doc, p);
|
||||
let mut done: Vec<Para> = Vec::new();
|
||||
let borders = if kind == Kind::Rule { None } else { p.props.borders.filter(|b| b.any_visible()) };
|
||||
let mut out = Para {
|
||||
kind,
|
||||
list,
|
||||
@@ -880,7 +881,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
|
||||
page_break: p.props.page_break_before.unwrap_or(false),
|
||||
inlines: Vec::new(),
|
||||
spacing: Spacing::default(),
|
||||
borders: None,
|
||||
borders,
|
||||
section: None,
|
||||
mark_ins: None,
|
||||
mark_del: None,
|
||||
@@ -910,7 +911,7 @@ fn flow_paras_in(doc: &Document, p: &Paragraph, tracked: bool) -> Vec<Para> {
|
||||
page_break: true,
|
||||
inlines: Vec::new(),
|
||||
spacing: Spacing::default(),
|
||||
borders: None,
|
||||
borders,
|
||||
section: None,
|
||||
mark_ins: None,
|
||||
mark_del: None,
|
||||
|
||||
@@ -160,7 +160,51 @@ fn style_num(k: Kind) -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn rtf_border_style(s: wordcraft_doc::props::BorderStyle) -> &'static str {
|
||||
use wordcraft_doc::props::BorderStyle;
|
||||
match s {
|
||||
BorderStyle::None => "\\brdrnone",
|
||||
BorderStyle::Single => "\\brdrs",
|
||||
BorderStyle::Double => "\\brdrdb",
|
||||
BorderStyle::Dotted => "\\brdrdot",
|
||||
BorderStyle::Dashed => "\\brdrdash",
|
||||
BorderStyle::Thick => "\\brdrth",
|
||||
BorderStyle::Triple => "\\brdrtriple",
|
||||
BorderStyle::DotDash => "\\brdrdashd",
|
||||
BorderStyle::Wave => "\\brdrwavy",
|
||||
}
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
fn write_border(&mut self, prefix: &str, side: char, b: wordcraft_doc::props::Border, out: &mut String) {
|
||||
out.push_str(&format!("\\{prefix}{side}"));
|
||||
out.push_str(rtf_border_style(b.style));
|
||||
let w = ((b.width * 20.0).round() as i64).clamp(1, 150);
|
||||
out.push_str(&format!("\\brdrw{w}"));
|
||||
if prefix == "brdr" {
|
||||
let sp = (b.space * 20.0).round() as i64;
|
||||
if sp > 0 {
|
||||
out.push_str(&format!("\\brsp{sp}"));
|
||||
}
|
||||
}
|
||||
if let Some(c) = b.color {
|
||||
let i = self.t.color(c);
|
||||
out.push_str(&format!("\\brdrcf{i}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_borders(&mut self, borders: Option<&wordcraft_doc::props::Borders>, borderless: bool, out: &mut String) {
|
||||
if let Some(b) = borders {
|
||||
for (side, opt) in [('t', &b.top), ('l', &b.left), ('b', &b.bottom), ('r', &b.right)] {
|
||||
if let Some(border) = opt.filter(|x| x.is_visible()) {
|
||||
self.write_border("clbrdr", side, border, out);
|
||||
}
|
||||
}
|
||||
} else if !borderless {
|
||||
out.push_str("\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10");
|
||||
}
|
||||
}
|
||||
|
||||
/// `\revtbl` index of a tracked change's author (0 = "Unknown").
|
||||
fn author(&mut self, a: &str) -> usize {
|
||||
let authors = self.authors.get_or_insert_with(Vec::new);
|
||||
@@ -379,6 +423,15 @@ impl Writer {
|
||||
let words = self.rev_words("\\prauth", "\\prdate", &c.mark);
|
||||
out.push_str(&words);
|
||||
}
|
||||
if p.kind != Kind::Rule
|
||||
&& let Some(b) = &p.borders
|
||||
{
|
||||
for (side, opt) in [('t', &b.top), ('l', &b.left), ('b', &b.bottom), ('r', &b.right)] {
|
||||
if let Some(border) = opt.filter(|x| x.is_visible()) {
|
||||
self.write_border("brdr", side, border, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let (Some(li), Some((n, label))) = (p.list, &ls) {
|
||||
let lv = li.level.min(8);
|
||||
out.push_str(&format!("\\ls{n}\\ilvl{lv}\\fi-360\\li{}", 720 * (u32::from(lv) + 1)));
|
||||
@@ -543,7 +596,7 @@ impl Writer {
|
||||
} else if c.rowspan > 1 {
|
||||
out.push_str("\\clvmgf");
|
||||
}
|
||||
out.push_str("\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10");
|
||||
self.cell_borders(c.borders.as_ref(), t.borderless, out);
|
||||
if let Some(s) = c.shading {
|
||||
let i = self.t.color(s);
|
||||
out.push_str(&format!("\\clcbpat{i}"));
|
||||
@@ -2649,6 +2702,13 @@ mod tests {
|
||||
let d = import(rtf).unwrap();
|
||||
let first = d.body.iter().find_map(|b| if let wordcraft_doc::Block::Para(p) = &**b { Some(p) } else { None }).unwrap();
|
||||
assert!(first.props.borders.and_then(|b| b.bottom).is_some());
|
||||
// Paragraph borders round-trip through export and import.
|
||||
let exported = export(&d);
|
||||
assert!(exported.contains("\\brdrb\\brdrs\\brdrw10\\brdrcf1"), "{exported}");
|
||||
let back = import(exported.as_bytes()).unwrap();
|
||||
let first = back.body.iter().find_map(|b| if let wordcraft_doc::Block::Para(p) = &**b { Some(p) } else { None }).unwrap();
|
||||
let b = first.props.borders.unwrap().bottom.unwrap();
|
||||
assert_eq!((b.style, b.width, b.color), (wordcraft_doc::props::BorderStyle::Single, 0.5, Some(Rgb(216, 216, 216))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2808,6 +2868,25 @@ mod tests {
|
||||
let t = d.body.iter().find_map(|b| if let wordcraft_doc::Block::Table(t) = &**b { Some(t) } else { None }).unwrap();
|
||||
assert!(t.props.style.is_none());
|
||||
assert!(t.rows[0].cells[0].props.borders.and_then(|b| b.bottom).is_some());
|
||||
// Borderless tables export without cell borders and round-trip borderless.
|
||||
let d = import(br"{\rtf1\ansi\trowd\cellx2000\pard\intbl a\cell\row}").unwrap();
|
||||
let exported = export(&d);
|
||||
assert!(!exported.contains("\\clbrdr"), "{exported}");
|
||||
let back = import(exported.as_bytes()).unwrap();
|
||||
let tb = back.body.iter().find_map(|b| if let wordcraft_doc::Block::Table(t) = &**b { Some(t) } else { None }).unwrap();
|
||||
assert!(tb.props.style.is_none());
|
||||
assert!(tb.rows[0].cells[0].props.borders.is_none());
|
||||
|
||||
// Cell borders round-trip through export and import.
|
||||
let d = import(br"{\rtf1\ansi{\colortbl;\red174\green174\blue174;}\trowd\clbrdrb\brdrdb\brdrw15\brdrcf1\cellx2000\pard\intbl a\cell\row}")
|
||||
.unwrap();
|
||||
let exported = export(&d);
|
||||
assert!(exported.contains("\\clbrdrb\\brdrdb\\brdrw15\\brdrcf1"), "{exported}");
|
||||
let back = import(exported.as_bytes()).unwrap();
|
||||
let tb = back.body.iter().find_map(|b| if let wordcraft_doc::Block::Table(t) = &**b { Some(t) } else { None }).unwrap();
|
||||
let b = tb.rows[0].cells[0].props.borders.unwrap().bottom.unwrap();
|
||||
assert_eq!((b.style, b.width, b.color), (wordcraft_doc::props::BorderStyle::Double, 0.75, Some(Rgb(174, 174, 174))));
|
||||
|
||||
// The table sizes to the document's page, not Letter.
|
||||
let d = import(br"{\rtf1\ansi\paperw11906\margl1134\margr1134\trowd\cellx4818\cellx9638\pard\intbl a\cell b\cell\row}").unwrap();
|
||||
let t = d.body.iter().find_map(|b| if let wordcraft_doc::Block::Table(t) = &**b { Some(t) } else { None }).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user