IDML: resolve table styles and correct cell height fitting (#288)
* ci: simplify testkit dependency match for Clippy * IDML: resolve table styles and shared cell edges * Compose: size table cells by baseline and physical bounds --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d0d4812ada
commit
b581031d91
File diff suppressed because it is too large
Load Diff
|
|
@ -148,6 +148,236 @@ impl Default for Styles {
|
|||
|
||||
pub const NO_CELL_STYLE: &str = "[None]";
|
||||
pub const BASIC_TABLE: &str = "[Basic Table]";
|
||||
pub const NO_TABLE_STYLE: &str = "[No table style]";
|
||||
|
||||
/// Partial edge formatting. Missing attributes inherit; zero weight and the None swatch
|
||||
/// are explicit values, rather than a request to use the parent/default edge.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct CellStrokeAttrs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub weight: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tint: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub kind: Option<crate::item::StrokeType>,
|
||||
}
|
||||
|
||||
impl CellStrokeAttrs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.weight.is_none() && self.color.is_none() && self.tint.is_none() && self.kind.is_none()
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, stroke: &mut crate::table::CellStroke) {
|
||||
if let Some(v) = self.weight {
|
||||
stroke.weight = v;
|
||||
}
|
||||
if let Some(v) = &self.color {
|
||||
stroke.color = v.clone();
|
||||
}
|
||||
if let Some(v) = self.tint {
|
||||
stroke.tint = v;
|
||||
}
|
||||
if let Some(v) = &self.kind {
|
||||
stroke.kind = v.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
if other.weight.is_some() {
|
||||
self.weight = other.weight;
|
||||
}
|
||||
if other.color.is_some() {
|
||||
self.color = other.color.clone();
|
||||
}
|
||||
if other.tint.is_some() {
|
||||
self.tint = other.tint;
|
||||
}
|
||||
if other.kind.is_some() {
|
||||
self.kind = other.kind.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn from_stroke(stroke: &crate::table::CellStroke) -> Self {
|
||||
Self { weight: Some(stroke.weight), color: Some(stroke.color.clone()), tint: Some(stroke.tint), kind: Some(stroke.kind.clone()) }
|
||||
}
|
||||
}
|
||||
|
||||
fn no_edge_attrs(edges: &[CellStrokeAttrs; 4]) -> bool {
|
||||
edges.iter().all(CellStrokeAttrs::is_empty)
|
||||
}
|
||||
|
||||
fn no_inset_attrs(insets: &[Option<f64>; 4]) -> bool {
|
||||
insets.iter().all(Option::is_none)
|
||||
}
|
||||
|
||||
/// Partial alternating-fill settings, retained separately from the legacy complete value.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct AltFillsAttrs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first_color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first_tint: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_tint: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_first: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_last: Option<u32>,
|
||||
}
|
||||
|
||||
impl AltFillsAttrs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self == &Self::default()
|
||||
}
|
||||
|
||||
/// Overlay supplied attributes, retaining explicit zero counts and None swatches.
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
if other.first.is_some() {
|
||||
self.first = other.first;
|
||||
}
|
||||
if other.first_color.is_some() {
|
||||
self.first_color = other.first_color.clone();
|
||||
}
|
||||
if other.first_tint.is_some() {
|
||||
self.first_tint = other.first_tint;
|
||||
}
|
||||
if other.next.is_some() {
|
||||
self.next = other.next;
|
||||
}
|
||||
if other.next_color.is_some() {
|
||||
self.next_color = other.next_color.clone();
|
||||
}
|
||||
if other.next_tint.is_some() {
|
||||
self.next_tint = other.next_tint;
|
||||
}
|
||||
if other.skip_first.is_some() {
|
||||
self.skip_first = other.skip_first;
|
||||
}
|
||||
if other.skip_last.is_some() {
|
||||
self.skip_last = other.skip_last;
|
||||
}
|
||||
}
|
||||
|
||||
fn from_fills(fills: &crate::table::AltFills) -> Self {
|
||||
Self {
|
||||
first: Some(fills.first),
|
||||
first_color: Some(fills.first_color.clone()),
|
||||
first_tint: Some(fills.first_tint),
|
||||
next: Some(fills.next),
|
||||
next_color: Some(fills.next_color.clone()),
|
||||
next_tint: Some(fills.next_tint),
|
||||
skip_first: Some(fills.skip_first),
|
||||
skip_last: Some(fills.skip_last),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, fills: &mut Option<crate::table::AltFills>) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut value = fills.clone().unwrap_or_default();
|
||||
if let Some(v) = self.first {
|
||||
value.first = v;
|
||||
}
|
||||
if let Some(v) = &self.first_color {
|
||||
value.first_color = v.clone();
|
||||
}
|
||||
if let Some(v) = self.first_tint {
|
||||
value.first_tint = v;
|
||||
}
|
||||
if let Some(v) = self.next {
|
||||
value.next = v;
|
||||
}
|
||||
if let Some(v) = &self.next_color {
|
||||
value.next_color = v.clone();
|
||||
}
|
||||
if let Some(v) = self.next_tint {
|
||||
value.next_tint = v;
|
||||
}
|
||||
if let Some(v) = self.skip_first {
|
||||
value.skip_first = v;
|
||||
}
|
||||
if let Some(v) = self.skip_last {
|
||||
value.skip_last = v;
|
||||
}
|
||||
*fills = (value.first > 0).then_some(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Partial table alternating strokes; missing attributes inherit independently.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct AltStrokesAttrs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub first: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next: Option<u32>,
|
||||
#[serde(skip_serializing_if = "CellStrokeAttrs::is_empty")]
|
||||
pub first_stroke: CellStrokeAttrs,
|
||||
#[serde(skip_serializing_if = "CellStrokeAttrs::is_empty")]
|
||||
pub next_stroke: CellStrokeAttrs,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_first: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_last: Option<u32>,
|
||||
}
|
||||
|
||||
impl AltStrokesAttrs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self == &Self::default()
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
if other.first.is_some() {
|
||||
self.first = other.first;
|
||||
}
|
||||
if other.next.is_some() {
|
||||
self.next = other.next;
|
||||
}
|
||||
self.first_stroke.merge(&other.first_stroke);
|
||||
self.next_stroke.merge(&other.next_stroke);
|
||||
if other.skip_first.is_some() {
|
||||
self.skip_first = other.skip_first;
|
||||
}
|
||||
if other.skip_last.is_some() {
|
||||
self.skip_last = other.skip_last;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to(&self, strokes: &mut Option<crate::table::AltStrokes>) {
|
||||
if self.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut value = strokes.clone().unwrap_or_default();
|
||||
if let Some(v) = self.first {
|
||||
value.first = v;
|
||||
}
|
||||
if let Some(v) = self.next {
|
||||
value.next = v;
|
||||
}
|
||||
self.first_stroke.apply_to(&mut value.first_stroke);
|
||||
self.next_stroke.apply_to(&mut value.next_stroke);
|
||||
if let Some(v) = self.skip_first {
|
||||
value.skip_first = v;
|
||||
}
|
||||
if let Some(v) = self.skip_last {
|
||||
value.skip_last = v;
|
||||
}
|
||||
// Zero counts disable rendering, but the remaining settings still belong to
|
||||
// the document (and are reused if the pattern is enabled later).
|
||||
*strokes = Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// A cell style: the cell attributes it sets (unset = left as is) and a paragraph style for the
|
||||
/// cell's text.
|
||||
|
|
@ -156,16 +386,24 @@ pub const BASIC_TABLE: &str = "[Basic Table]";
|
|||
pub struct CellStyle {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub based_on: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fill: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fill_tint: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub insets: Option<[f64; 4]>,
|
||||
/// Per-side values override `insets`; top, left, bottom, right.
|
||||
#[serde(skip_serializing_if = "no_inset_attrs")]
|
||||
pub inset_overrides: [Option<f64>; 4],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vj: Option<crate::item::VerticalJustification>,
|
||||
/// All four edges.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stroke: Option<crate::table::CellStroke>,
|
||||
/// Per-edge attributes override the legacy uniform `stroke`.
|
||||
#[serde(skip_serializing_if = "no_edge_attrs")]
|
||||
pub strokes: [CellStrokeAttrs; 4],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub paragraph_style: Option<String>,
|
||||
}
|
||||
|
|
@ -176,6 +414,16 @@ pub struct CellStyle {
|
|||
pub struct TableStyle {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub based_on: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_same_as_body: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub footer_same_as_body: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub left_column_same_as_body: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub right_column_same_as_body: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<String>,
|
||||
|
|
@ -187,8 +435,18 @@ pub struct TableStyle {
|
|||
pub right_column: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub border: Option<crate::table::CellStroke>,
|
||||
#[serde(skip_serializing_if = "no_edge_attrs")]
|
||||
pub borders: [CellStrokeAttrs; 4],
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alt_rows: Option<crate::table::AltFills>,
|
||||
#[serde(skip_serializing_if = "AltFillsAttrs::is_empty")]
|
||||
pub row_fills: AltFillsAttrs,
|
||||
#[serde(skip_serializing_if = "AltFillsAttrs::is_empty")]
|
||||
pub column_fills: AltFillsAttrs,
|
||||
#[serde(skip_serializing_if = "AltStrokesAttrs::is_empty")]
|
||||
pub row_strokes: AltStrokesAttrs,
|
||||
#[serde(skip_serializing_if = "AltStrokesAttrs::is_empty")]
|
||||
pub column_strokes: AltStrokesAttrs,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub space_before: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -207,11 +465,25 @@ impl CellStyle {
|
|||
if let Some(v) = self.insets {
|
||||
cell.insets = v;
|
||||
}
|
||||
for (slot, value) in cell.insets.iter_mut().zip(self.inset_overrides) {
|
||||
if let Some(value) = value {
|
||||
*slot = value;
|
||||
}
|
||||
}
|
||||
if let Some(v) = self.vj {
|
||||
cell.vj = v;
|
||||
}
|
||||
if let Some(v) = &self.stroke {
|
||||
cell.strokes = [v.clone(), v.clone(), v.clone(), v.clone()];
|
||||
cell.border_overrides = [true; 4];
|
||||
cell.stroke_defined = [true; 4];
|
||||
}
|
||||
for (i, edge) in self.strokes.iter().enumerate() {
|
||||
edge.apply_to(&mut cell.strokes[i]);
|
||||
if !edge.is_empty() {
|
||||
cell.border_overrides[i] = true;
|
||||
cell.stroke_defined[i] = true;
|
||||
}
|
||||
}
|
||||
if let Some(ps) = &self.paragraph_style {
|
||||
for p in &mut cell.text.paras {
|
||||
|
|
@ -224,14 +496,33 @@ impl CellStyle {
|
|||
}
|
||||
|
||||
impl TableStyle {
|
||||
/// Write this style into `t`: options, then each cell's region style.
|
||||
/// Write the resolved table options, then region cell styles in precedence order:
|
||||
/// body, left/right columns, header/footer. Explicit cell formatting is applied later.
|
||||
pub fn apply_to(&self, t: &mut crate::table::Table, cells: &[CellStyle]) {
|
||||
if let Some(b) = &self.border {
|
||||
t.options.border = b.clone();
|
||||
t.options.borders = Default::default();
|
||||
}
|
||||
for (i, attrs) in self.borders.iter().enumerate() {
|
||||
if !attrs.is_empty() {
|
||||
let mut edge = t.options.border_for(i).clone();
|
||||
attrs.apply_to(&mut edge);
|
||||
t.options.borders[i] = Some(edge);
|
||||
}
|
||||
}
|
||||
if self.borders.iter().any(|edge| !edge.is_empty()) {
|
||||
// Keep the legacy uniform accessor representative without changing absent sides.
|
||||
let borders = std::array::from_fn::<_, 4, _>(|i| t.options.border_for(i).clone());
|
||||
t.options.border = borders[0].clone();
|
||||
t.options.borders = borders.map(Some);
|
||||
}
|
||||
if self.alt_rows.is_some() {
|
||||
t.options.alt_rows = self.alt_rows.clone();
|
||||
}
|
||||
self.row_fills.apply_to(&mut t.options.alt_rows);
|
||||
self.column_fills.apply_to(&mut t.options.alt_cols);
|
||||
self.row_strokes.apply_to(&mut t.options.row_strokes);
|
||||
self.column_strokes.apply_to(&mut t.options.column_strokes);
|
||||
if let Some(v) = self.space_before {
|
||||
t.options.space_before = v;
|
||||
}
|
||||
|
|
@ -243,27 +534,148 @@ impl TableStyle {
|
|||
let kind = t.rows[r].kind;
|
||||
for c in 0..nc {
|
||||
let visual_column = if t.options.direction == crate::TextDirection::RightToLeft { nc - 1 - c } else { c };
|
||||
let region = match kind {
|
||||
crate::table::RowKind::Header => self.header.as_ref(),
|
||||
crate::table::RowKind::Footer => self.footer.as_ref(),
|
||||
_ if visual_column == 0 && self.left_column.is_some() => self.left_column.as_ref(),
|
||||
_ if visual_column + 1 == nc && self.right_column.is_some() => self.right_column.as_ref(),
|
||||
_ => self.body.as_ref(),
|
||||
let same_or_region = |same, region: &Option<String>| {
|
||||
if same == Some(true) { self.body.clone() } else { region.clone() }
|
||||
};
|
||||
if let (Some(name), Some(cell)) = (region, t.cells.get_mut(r * nc + c))
|
||||
&& let Some(cs) = cells.iter().find(|s| s.name == *name)
|
||||
{
|
||||
cs.apply_to(cell);
|
||||
let left = same_or_region(self.left_column_same_as_body, &self.left_column);
|
||||
let column = if visual_column == 0 && left.is_some() {
|
||||
left
|
||||
} else if visual_column + 1 == nc {
|
||||
same_or_region(self.right_column_same_as_body, &self.right_column)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let row = match kind {
|
||||
crate::table::RowKind::Header => same_or_region(self.header_same_as_body, &self.header),
|
||||
crate::table::RowKind::Footer => same_or_region(self.footer_same_as_body, &self.footer),
|
||||
crate::table::RowKind::Body => None,
|
||||
};
|
||||
if let Some(cell) = t.cells.get_mut(r * nc + c) {
|
||||
for name in [self.body.as_ref(), column.as_ref(), row.as_ref()].into_iter().flatten() {
|
||||
if name != NO_CELL_STYLE {
|
||||
resolve_cell_style(cells, name).apply_to(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.style = if self.name == BASIC_TABLE { String::new() } else { self.name.clone() };
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
if other.header.is_some() {
|
||||
self.header = other.header.clone();
|
||||
}
|
||||
if other.body.is_some() {
|
||||
self.body = other.body.clone();
|
||||
}
|
||||
if other.footer.is_some() {
|
||||
self.footer = other.footer.clone();
|
||||
}
|
||||
if other.left_column.is_some() {
|
||||
self.left_column = other.left_column.clone();
|
||||
}
|
||||
if other.right_column.is_some() {
|
||||
self.right_column = other.right_column.clone();
|
||||
}
|
||||
if other.header_same_as_body.is_some() {
|
||||
self.header_same_as_body = other.header_same_as_body;
|
||||
}
|
||||
if other.footer_same_as_body.is_some() {
|
||||
self.footer_same_as_body = other.footer_same_as_body;
|
||||
}
|
||||
if other.left_column_same_as_body.is_some() {
|
||||
self.left_column_same_as_body = other.left_column_same_as_body;
|
||||
}
|
||||
if other.right_column_same_as_body.is_some() {
|
||||
self.right_column_same_as_body = other.right_column_same_as_body;
|
||||
}
|
||||
if let Some(border) = &other.border {
|
||||
self.borders = std::array::from_fn(|_| CellStrokeAttrs::from_stroke(border));
|
||||
}
|
||||
for (edge, value) in self.borders.iter_mut().zip(&other.borders) {
|
||||
edge.merge(value);
|
||||
}
|
||||
if let Some(fills) = &other.alt_rows {
|
||||
self.row_fills = AltFillsAttrs::from_fills(fills);
|
||||
}
|
||||
self.row_fills.merge(&other.row_fills);
|
||||
self.column_fills.merge(&other.column_fills);
|
||||
self.row_strokes.merge(&other.row_strokes);
|
||||
self.column_strokes.merge(&other.column_strokes);
|
||||
if other.space_before.is_some() {
|
||||
self.space_before = other.space_before;
|
||||
}
|
||||
if other.space_after.is_some() {
|
||||
self.space_after = other.space_after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a cell style with the same 32-level/cycle limit as paragraph styles.
|
||||
pub fn resolve_cell_style(cells: &[CellStyle], name: &str) -> CellStyle {
|
||||
let mut chain = Vec::new();
|
||||
let mut cur = cells.iter().find(|s| s.name == name);
|
||||
while let Some(style) = cur {
|
||||
if chain.len() >= MAX_DEPTH || chain.iter().any(|s: &&CellStyle| s.name == style.name) {
|
||||
break;
|
||||
}
|
||||
chain.push(style);
|
||||
cur = style.based_on.as_deref().and_then(|base| cells.iter().find(|s| s.name == base));
|
||||
}
|
||||
let mut out = CellStyle { name: name.into(), ..Default::default() };
|
||||
for style in chain.into_iter().rev() {
|
||||
if style.fill.is_some() {
|
||||
out.fill = style.fill.clone();
|
||||
}
|
||||
if style.fill_tint.is_some() {
|
||||
out.fill_tint = style.fill_tint;
|
||||
}
|
||||
if let Some(insets) = style.insets {
|
||||
out.inset_overrides = insets.map(Some);
|
||||
}
|
||||
for (slot, value) in out.inset_overrides.iter_mut().zip(style.inset_overrides) {
|
||||
if value.is_some() {
|
||||
*slot = value;
|
||||
}
|
||||
}
|
||||
if style.vj.is_some() {
|
||||
out.vj = style.vj;
|
||||
}
|
||||
if let Some(stroke) = &style.stroke {
|
||||
out.strokes = std::array::from_fn(|_| CellStrokeAttrs::from_stroke(stroke));
|
||||
}
|
||||
for (slot, attrs) in out.strokes.iter_mut().zip(&style.strokes) {
|
||||
slot.merge(attrs);
|
||||
}
|
||||
if style.paragraph_style.is_some() {
|
||||
out.paragraph_style = style.paragraph_style.clone();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
const MAX_DEPTH: usize = 32;
|
||||
|
||||
impl Styles {
|
||||
/// Resolve table style inheritance without recursion; repeated/missing bases stop the chain.
|
||||
pub fn resolve_table_style(&self, name: &str) -> TableStyle {
|
||||
let mut chain = Vec::new();
|
||||
let mut cur = self.table.iter().find(|s| s.name == name);
|
||||
while let Some(style) = cur {
|
||||
if chain.len() >= MAX_DEPTH || chain.iter().any(|s: &&TableStyle| s.name == style.name) {
|
||||
break;
|
||||
}
|
||||
chain.push(style);
|
||||
cur = style.based_on.as_deref().and_then(|base| self.table.iter().find(|s| s.name == base));
|
||||
}
|
||||
let mut out = TableStyle { name: name.into(), ..Default::default() };
|
||||
for style in chain.into_iter().rev() {
|
||||
out.merge(style);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn para(&self, name: &str) -> Option<&ParagraphStyle> {
|
||||
self.paragraph.iter().find(|s| s.name == name)
|
||||
}
|
||||
|
|
@ -411,6 +823,104 @@ mod tests {
|
|||
assert!(!st.para_based_on_cycles(BASIC_PARAGRAPH, NO_PARA_STYLE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_cell_and_table_style_json_keep_uniform_values() {
|
||||
let cell: CellStyle =
|
||||
serde_json::from_str(r#"{"name":"Legacy","insets":[1,2,3,4],"stroke":{"weight":2,"color":"Brand","tint":0.5,"kind":{"kind":"solid"}}}"#)
|
||||
.unwrap();
|
||||
assert!(cell.based_on.is_none());
|
||||
assert!(no_edge_attrs(&cell.strokes));
|
||||
assert!(no_inset_attrs(&cell.inset_overrides));
|
||||
let resolved = resolve_cell_style(std::slice::from_ref(&cell), "Legacy");
|
||||
let mut target = crate::table::Cell::default();
|
||||
resolved.apply_to(&mut target);
|
||||
assert_eq!(target.insets, [1.0, 2.0, 3.0, 4.0]);
|
||||
assert!(target.strokes.iter().all(|s| s.weight == 2.0 && s.color == "Brand" && s.tint == 0.5));
|
||||
let json = serde_json::to_value(&cell).unwrap();
|
||||
assert!(json.get("strokes").is_none());
|
||||
assert_eq!(serde_json::from_value::<CellStyle>(json).unwrap(), cell);
|
||||
|
||||
let table: TableStyle =
|
||||
serde_json::from_str(r#"{"name":"Legacy","body":"Legacy","border":{"weight":3},"altRows":{"first":2,"firstColor":"Brand"}}"#).unwrap();
|
||||
let styles = Styles { cell: vec![cell], table: vec![table.clone()], ..Styles::default() };
|
||||
let mut target = crate::table::Table::new(1, 1, 2, 0, 0, 100.0);
|
||||
styles.resolve_table_style("Legacy").apply_to(&mut target, &styles.cell);
|
||||
assert_eq!(target.options.border_for(3).weight, 3.0);
|
||||
assert_eq!(target.options.alt_rows.as_ref().unwrap().first, 2);
|
||||
assert_eq!(target.cell(0, 0).unwrap().strokes[0].weight, 2.0);
|
||||
assert_eq!(serde_json::from_str::<TableStyle>(&serde_json::to_string(&table).unwrap()).unwrap(), table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_region_flags_cover_footer_right_column_and_rtl() {
|
||||
let cells = [
|
||||
CellStyle { name: "Body".into(), fill: Some("Body".into()), ..Default::default() },
|
||||
CellStyle { name: "Footer".into(), fill: Some("Footer".into()), ..Default::default() },
|
||||
CellStyle { name: "Right".into(), fill: Some("Right".into()), ..Default::default() },
|
||||
];
|
||||
let mut style = TableStyle {
|
||||
body: Some("Body".into()),
|
||||
footer: Some("Footer".into()),
|
||||
right_column: Some("Right".into()),
|
||||
footer_same_as_body: Some(false),
|
||||
right_column_same_as_body: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
for direction in [crate::TextDirection::LeftToRight, crate::TextDirection::RightToLeft] {
|
||||
let mut table = crate::table::Table::new(1, 1, 3, 0, 1, 100.0);
|
||||
table.options.direction = direction;
|
||||
let right = if direction == crate::TextDirection::LeftToRight { 2 } else { 0 };
|
||||
style.apply_to(&mut table, &cells);
|
||||
assert_eq!(table.cell(0, right).unwrap().fill, "Right");
|
||||
assert_eq!(table.cell(1, right).unwrap().fill, "Footer");
|
||||
}
|
||||
style.footer_same_as_body = Some(true);
|
||||
style.right_column_same_as_body = Some(true);
|
||||
let mut table = crate::table::Table::new(1, 1, 3, 0, 1, 100.0);
|
||||
style.apply_to(&mut table, &cells);
|
||||
assert!(table.cells.iter().all(|c| c.fill == "Body"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_column_tables_use_a_configured_right_region_when_left_is_absent() {
|
||||
let cells = [CellStyle { name: "Right".into(), fill: Some("Right".into()), ..Default::default() }];
|
||||
let style = TableStyle { right_column: Some("Right".into()), ..Default::default() };
|
||||
let mut table = crate::table::Table::new(1, 1, 1, 0, 0, 100.0);
|
||||
style.apply_to(&mut table, &cells);
|
||||
assert_eq!(table.cell(0, 0).unwrap().fill, "Right");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_and_table_style_resolution_stop_at_the_depth_limit() {
|
||||
let cells: Vec<_> = (0..40)
|
||||
.map(|i| CellStyle {
|
||||
name: i.to_string(),
|
||||
based_on: Some((i + 1).to_string()),
|
||||
fill: (i == 32).then(|| "Too deep".into()),
|
||||
fill_tint: (i == 31).then_some(0.5),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
let resolved = resolve_cell_style(&cells, "0");
|
||||
assert!(resolved.fill.is_none());
|
||||
assert_eq!(resolved.fill_tint, Some(0.5));
|
||||
let styles = Styles {
|
||||
table: (0..40)
|
||||
.map(|i| TableStyle {
|
||||
name: i.to_string(),
|
||||
based_on: Some((i + 1).to_string()),
|
||||
space_before: (i == 32).then_some(123.0),
|
||||
space_after: (i == 31).then_some(15.0),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
..Styles::default()
|
||||
};
|
||||
let resolved = styles.resolve_table_style("0");
|
||||
assert!(resolved.space_before.is_none());
|
||||
assert_eq!(resolved.space_after, Some(15.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_style_attrs_include_its_based_on_chain_only() {
|
||||
let mut st = Styles::default();
|
||||
|
|
|
|||
|
|
@ -79,6 +79,58 @@ impl CellStroke {
|
|||
}
|
||||
}
|
||||
|
||||
/// Table-level alternating row/column strokes. Retained independently of cell formatting
|
||||
/// so an explicit cell style or local edge continues to take precedence.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct AltStrokes {
|
||||
pub first: u32,
|
||||
pub next: u32,
|
||||
pub first_stroke: CellStroke,
|
||||
pub next_stroke: CellStroke,
|
||||
pub skip_first: u32,
|
||||
pub skip_last: u32,
|
||||
}
|
||||
|
||||
impl Default for AltStrokes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first: 0,
|
||||
next: 0,
|
||||
first_stroke: CellStroke::default(),
|
||||
next_stroke: CellStroke { weight: 0.25, ..Default::default() },
|
||||
skip_first: 0,
|
||||
skip_last: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AltStrokes {
|
||||
/// A phase-independent stroke for unskipped, visually uniform patterns. Unequal
|
||||
/// patterns and skip settings are preserved for round trips until their boundary
|
||||
/// phase is supported; they must not be rendered with a guessed phase.
|
||||
pub fn uniform_stroke(&self) -> Option<&CellStroke> {
|
||||
if self.skip_first > 0 || self.skip_last > 0 {
|
||||
return None;
|
||||
}
|
||||
match (self.first > 0, self.next > 0) {
|
||||
(true, false) => Some(&self.first_stroke),
|
||||
(false, true) => Some(&self.next_stroke),
|
||||
(true, true) if self.first_stroke == self.next_stroke || (!self.first_stroke.is_visible() && !self.next_stroke.is_visible()) => {
|
||||
Some(&self.first_stroke)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn no_flags(flags: &[bool; 4]) -> bool {
|
||||
!flags.iter().any(|v| *v)
|
||||
}
|
||||
fn no_priorities(priorities: &[i32; 4]) -> bool {
|
||||
priorities.iter().all(|v| *v == 0)
|
||||
}
|
||||
|
||||
fn one() -> u32 {
|
||||
1
|
||||
}
|
||||
|
|
@ -120,6 +172,12 @@ pub struct Cell {
|
|||
/// top, left, bottom, right order as `strokes`. Older documents keep their table border.
|
||||
#[serde(default)]
|
||||
pub border_overrides: [bool; 4],
|
||||
/// Whether an edge has supplied cell/style attributes, independently of perimeter priority.
|
||||
#[serde(default, skip_serializing_if = "no_flags")]
|
||||
pub stroke_defined: [bool; 4],
|
||||
/// IDML edge-conflict priorities, in top, left, bottom, right order.
|
||||
#[serde(default, skip_serializing_if = "no_priorities")]
|
||||
pub stroke_priorities: [i32; 4],
|
||||
/// Applied cell style ("" = [None]).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub style: String,
|
||||
|
|
@ -142,6 +200,8 @@ impl Default for Cell {
|
|||
rotation: 0.0,
|
||||
strokes: Default::default(),
|
||||
border_overrides: [false; 4],
|
||||
stroke_priorities: [0; 4],
|
||||
stroke_defined: [false; 4],
|
||||
style: String::new(),
|
||||
graphic: None,
|
||||
}
|
||||
|
|
@ -200,12 +260,12 @@ impl AltFills {
|
|||
/// Fill for the `i`-th of `n` body rows/columns, if the pattern paints it.
|
||||
pub fn fill_for(&self, i: usize, n: usize) -> Option<(&str, f32)> {
|
||||
let (sf, sl) = (self.skip_first as usize, self.skip_last as usize);
|
||||
if i < sf || i + sl >= n {
|
||||
if i < sf || i >= n.saturating_sub(sl) {
|
||||
return None;
|
||||
}
|
||||
let period = (self.first + self.next).max(1) as usize;
|
||||
let k = (i - sf) % period;
|
||||
let (c, t) = if k < self.first as usize { (&self.first_color, self.first_tint) } else { (&self.next_color, self.next_tint) };
|
||||
let period = (u64::from(self.first) + u64::from(self.next)).max(1);
|
||||
let k = (i - sf) as u64 % period;
|
||||
let (c, t) = if k < u64::from(self.first) { (&self.first_color, self.first_tint) } else { (&self.next_color, self.next_tint) };
|
||||
(c != designcraft_color::swatch::NONE).then_some((c.as_str(), t))
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +277,9 @@ pub struct TableOptions {
|
|||
pub direction: crate::TextDirection,
|
||||
/// Outer border (overrides the outer cell edges).
|
||||
pub border: CellStroke,
|
||||
/// Per-side borders; absent sides use the legacy uniform `border`.
|
||||
#[serde(skip_serializing_if = "no_borders")]
|
||||
pub borders: [Option<CellStroke>; 4],
|
||||
pub space_before: f64,
|
||||
pub space_after: f64,
|
||||
/// Header rows repeat at the top of every text column / frame the table continues into.
|
||||
|
|
@ -226,6 +289,20 @@ pub struct TableOptions {
|
|||
pub alt_rows: Option<AltFills>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alt_cols: Option<AltFills>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub row_strokes: Option<AltStrokes>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub column_strokes: Option<AltStrokes>,
|
||||
}
|
||||
|
||||
fn no_borders(borders: &[Option<CellStroke>; 4]) -> bool {
|
||||
borders.iter().all(Option::is_none)
|
||||
}
|
||||
|
||||
impl TableOptions {
|
||||
pub fn border_for(&self, side: usize) -> &CellStroke {
|
||||
self.borders.get(side).and_then(Option::as_ref).unwrap_or(&self.border)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TableOptions {
|
||||
|
|
@ -233,12 +310,15 @@ impl Default for TableOptions {
|
|||
TableOptions {
|
||||
direction: crate::TextDirection::LeftToRight,
|
||||
border: CellStroke::default(),
|
||||
borders: Default::default(),
|
||||
space_before: 4.0,
|
||||
space_after: -4.0,
|
||||
repeat_header: true,
|
||||
repeat_footer: true,
|
||||
alt_rows: None,
|
||||
alt_cols: None,
|
||||
row_strokes: None,
|
||||
column_strokes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1097,6 +1177,16 @@ mod tests {
|
|||
assert!(b.fill_for(3, 9).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternating_fill_counts_cannot_overflow() {
|
||||
let fills = AltFills { first: u32::MAX, next: 1, ..Default::default() };
|
||||
assert!(fills.fill_for(0, 2).is_some());
|
||||
assert!(fills.fill_for(1, 2).is_some());
|
||||
let skipped = AltFills { skip_last: u32::MAX, ..fills };
|
||||
assert!(skipped.fill_for(1, 2).is_none());
|
||||
assert!(skipped.fill_for(usize::MAX, usize::MAX).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_roundtrip() {
|
||||
let mut s = story("x");
|
||||
|
|
@ -1118,8 +1208,22 @@ mod tests {
|
|||
old.as_object_mut().unwrap().remove("borderOverrides");
|
||||
let back: Cell = serde_json::from_value(old).unwrap();
|
||||
assert_eq!(back.border_overrides, [false; 4]);
|
||||
assert_eq!(back.stroke_defined, [false; 4]);
|
||||
assert_eq!(back.stroke_priorities, [0; 4]);
|
||||
let legacy_options: TableOptions = serde_json::from_str(r#"{"border":{"weight":3}}"#).unwrap();
|
||||
assert!(legacy_options.borders.iter().all(Option::is_none));
|
||||
assert!(legacy_options.row_strokes.is_none());
|
||||
assert!(legacy_options.column_strokes.is_none());
|
||||
for side in 0..4 {
|
||||
assert_eq!(legacy_options.border_for(side).weight, 3.0);
|
||||
}
|
||||
|
||||
let explicit = Cell { border_overrides: [true, false, true, false], ..cell };
|
||||
let explicit = Cell {
|
||||
border_overrides: [true, false, true, false],
|
||||
stroke_defined: [true, true, false, false],
|
||||
stroke_priorities: [3, 0, 7, 0],
|
||||
..cell
|
||||
};
|
||||
let back: Cell = serde_json::from_str(&serde_json::to_string(&explicit).unwrap()).unwrap();
|
||||
assert_eq!(back, explicit);
|
||||
assert_eq!(back.blank_like().border_overrides, explicit.border_overrides);
|
||||
|
|
|
|||
|
|
@ -650,6 +650,11 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
edit_table(s, &g, "table.setCell", |t| {
|
||||
let owners = t.owners();
|
||||
let nc = t.ncols();
|
||||
let nr = t.nrows();
|
||||
let rtl = t.options.direction == designcraft_doc::TextDirection::RightToLeft;
|
||||
let borders: [CellStroke; 4] = std::array::from_fn(|side| t.options.border_for(side).clone());
|
||||
// Local edits outrank previously imported edges, including explicit zero/None.
|
||||
let priority = if stroke.is_some() { next_cell_stroke_priority(t) } else { 0 };
|
||||
let rg = g.range;
|
||||
for r in rg.r0..=rg.r1 {
|
||||
for c in rg.c0..=rg.c1 {
|
||||
|
|
@ -675,40 +680,57 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
if let Some(sv) = &stroke {
|
||||
let edges = str_param(sv, "edges").unwrap_or("all");
|
||||
let at_bottom = r.saturating_add(cell.row_span.max(1) as usize);
|
||||
let at_right = c.saturating_add(cell.col_span.max(1) as usize);
|
||||
let perimeter = [r == 0, if rtl { at_right == nc } else { c == 0 }, at_bottom == nr, if rtl { c == 0 } else { at_right == nc }];
|
||||
for (i, on) in [
|
||||
("top", r == rg.r0),
|
||||
("left", c == rg.c0),
|
||||
("bottom", r + cell.row_span as usize - 1 == rg.r1),
|
||||
("right", c + cell.col_span as usize - 1 == rg.c1),
|
||||
("left", if rtl { at_right.saturating_sub(1) == rg.c1 } else { c == rg.c0 }),
|
||||
("bottom", at_bottom.saturating_sub(1) == rg.r1),
|
||||
("right", if rtl { c == rg.c0 } else { at_right.saturating_sub(1) == rg.c1 }),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, outer))| (i, edges == "all" || edges == *name || (edges == "outer" && *outer) || (edges == "inner" && !*outer)))
|
||||
{
|
||||
if on {
|
||||
cell.strokes[i] = parse_stroke(sv, cell.strokes[i].clone());
|
||||
let base = if perimeter[i] && !cell.border_overrides[i] { &borders[i] } else { &cell.strokes[i] };
|
||||
cell.strokes[i] = parse_stroke(sv, base.clone());
|
||||
cell.stroke_defined[i] = true;
|
||||
cell.stroke_priorities[i] = priority;
|
||||
cell.border_overrides[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Neighbouring cells share edges: mirror the edges along the range boundary.
|
||||
// Keep both copies of unmerged shared boundaries consistent. Left/right name
|
||||
// physical sides, also in RTL tables.
|
||||
if let Some(sv) = &stroke {
|
||||
let edges = str_param(sv, "edges").unwrap_or("all");
|
||||
if matches!(edges, "all" | "outer" | "bottom") && rg.r1 + 1 < t.nrows() {
|
||||
let selected = |side: &str| edges == "all" || edges == "outer" || edges == side;
|
||||
if selected("top")
|
||||
&& let Some(row) = rg.r0.checked_sub(1)
|
||||
{
|
||||
for c in rg.c0..=rg.c1 {
|
||||
let s0 = t.cell(rg.r1, c).map(|x| x.strokes[2].clone());
|
||||
if let (Some(s0), Some(n)) = (s0, t.cell_mut(rg.r1 + 1, c)) {
|
||||
n.strokes[0] = s0;
|
||||
}
|
||||
mirror_cell_edge(t, &owners, (rg.r0, c), 0, (row, c), 2);
|
||||
}
|
||||
}
|
||||
if matches!(edges, "all" | "outer" | "right") && rg.c1 + 1 < t.ncols() {
|
||||
if selected("bottom") && rg.r1.saturating_add(1) < nr {
|
||||
for c in rg.c0..=rg.c1 {
|
||||
mirror_cell_edge(t, &owners, (rg.r1, c), 2, (rg.r1 + 1, c), 0);
|
||||
}
|
||||
}
|
||||
if selected(if rtl { "right" } else { "left" })
|
||||
&& let Some(col) = rg.c0.checked_sub(1)
|
||||
{
|
||||
for r in rg.r0..=rg.r1 {
|
||||
let s0 = t.cell(r, rg.c1).map(|x| x.strokes[3].clone());
|
||||
if let (Some(s0), Some(n)) = (s0, t.cell_mut(r, rg.c1 + 1)) {
|
||||
n.strokes[1] = s0;
|
||||
}
|
||||
mirror_cell_edge(t, &owners, (r, rg.c0), if rtl { 3 } else { 1 }, (r, col), if rtl { 1 } else { 3 });
|
||||
}
|
||||
}
|
||||
if selected(if rtl { "left" } else { "right" }) && rg.c1.saturating_add(1) < nc {
|
||||
for r in rg.r0..=rg.r1 {
|
||||
mirror_cell_edge(t, &owners, (r, rg.c1), if rtl { 1 } else { 3 }, (r, rg.c1 + 1), if rtl { 3 } else { 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -716,6 +738,53 @@ fn set_cell(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Only saturation needs rebasing. Preserve every distinct priority's order and keep
|
||||
/// zero/negative priorities untouched; the work is bounded by the table's edges.
|
||||
fn next_cell_stroke_priority(t: &mut Table) -> i32 {
|
||||
let highest = t.cells.iter().flat_map(|cell| cell.stroke_priorities).max().unwrap_or(0).max(0);
|
||||
if highest < i32::MAX {
|
||||
return highest + 1;
|
||||
}
|
||||
let values: std::collections::BTreeSet<_> = t.cells.iter().flat_map(|cell| cell.stroke_priorities).filter(|p| *p > 0).collect();
|
||||
let mut ranks = std::collections::BTreeMap::new();
|
||||
let mut highest = 0i32;
|
||||
for value in values {
|
||||
highest = highest.saturating_add(1);
|
||||
ranks.insert(value, highest);
|
||||
}
|
||||
for cell in &mut t.cells {
|
||||
for priority in &mut cell.stroke_priorities {
|
||||
if let Some(rank) = ranks.get(priority) {
|
||||
*priority = *rank;
|
||||
}
|
||||
}
|
||||
}
|
||||
highest.saturating_add(1)
|
||||
}
|
||||
|
||||
fn mirror_cell_edge(t: &mut Table, owners: &[(usize, usize)], from: (usize, usize), edge: usize, to: (usize, usize), opposite: usize) {
|
||||
let owner = |pos: (usize, usize)| owners.get(pos.0.checked_mul(t.ncols())?.checked_add(pos.1)?).copied();
|
||||
if owner(from) != Some(from) || owner(to) != Some(to) {
|
||||
return;
|
||||
}
|
||||
// A merged cell's edge may border several separately editable cells. Mirroring
|
||||
// a segment onto that whole edge would change unselected neighbors; priorities
|
||||
// instead resolve each segment without altering the merged cell's formatting.
|
||||
let merged = |cell: &designcraft_doc::Cell| cell.row_span > 1 || cell.col_span > 1;
|
||||
if t.cell(from.0, from.1).is_some_and(merged) || t.cell(to.0, to.1).is_some_and(merged) {
|
||||
return;
|
||||
}
|
||||
let source = t
|
||||
.cell(from.0, from.1)
|
||||
.map(|cell| (cell.strokes[edge].clone(), cell.stroke_defined[edge], cell.stroke_priorities[edge], cell.border_overrides[edge]));
|
||||
if let (Some((stroke, defined, priority, border_override)), Some(neighbor)) = (source, t.cell_mut(to.0, to.1)) {
|
||||
neighbor.strokes[opposite] = stroke;
|
||||
neighbor.stroke_defined[opposite] = defined;
|
||||
neighbor.stroke_priorities[opposite] = priority;
|
||||
neighbor.border_overrides[opposite] = border_override;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_row_height(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let g = target(s, p, "table.setRowHeight")?;
|
||||
let h = p.get("height").and_then(Value::as_f64).map(|h| h.clamp(0.0, 10000.0));
|
||||
|
|
@ -846,6 +915,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
if let Some(b) = p.get("border") {
|
||||
o.border = parse_stroke(b, o.border.clone());
|
||||
o.borders = Default::default();
|
||||
}
|
||||
o.space_before = f64_or(&p, "spaceBefore", o.space_before);
|
||||
o.space_after = f64_or(&p, "spaceAfter", o.space_after);
|
||||
|
|
@ -982,6 +1052,9 @@ fn stroke_param(v: Option<&Value>) -> Option<designcraft_doc::CellStroke> {
|
|||
|
||||
/// Fill in the cell style fields given in `p`.
|
||||
fn cell_style_fields(cs: &mut designcraft_doc::CellStyle, p: &Value, cmd: &str) -> Result<()> {
|
||||
if let Some(v) = p.get("basedOn") {
|
||||
cs.based_on = v.as_str().map(str::to_string);
|
||||
}
|
||||
if let Some(f) = color_param(p, "fill") {
|
||||
cs.fill = Some(f);
|
||||
}
|
||||
|
|
@ -989,10 +1062,14 @@ fn cell_style_fields(cs: &mut designcraft_doc::CellStyle, p: &Value, cmd: &str)
|
|||
cs.fill_tint = Some(t.clamp(0.0, 1.0) as f32);
|
||||
}
|
||||
match p.get("insets") {
|
||||
Some(Value::Number(n)) => cs.insets = n.as_f64().map(|v| [v.max(0.0); 4]),
|
||||
Some(Value::Number(n)) => {
|
||||
cs.insets = n.as_f64().map(|v| [v.max(0.0); 4]);
|
||||
cs.inset_overrides = Default::default();
|
||||
}
|
||||
Some(Value::Array(a)) if a.len() == 4 => {
|
||||
let v: Vec<f64> = a.iter().map(|x| x.as_f64().unwrap_or(0.0).max(0.0)).collect();
|
||||
cs.insets = Some([v[0], v[1], v[2], v[3]]);
|
||||
cs.inset_overrides = Default::default();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1001,6 +1078,7 @@ fn cell_style_fields(cs: &mut designcraft_doc::CellStyle, p: &Value, cmd: &str)
|
|||
}
|
||||
if let Some(st) = stroke_param(p.get("stroke")) {
|
||||
cs.stroke = Some(st);
|
||||
cs.strokes = Default::default();
|
||||
}
|
||||
if let Some(ps) = str_param(p, "paragraphStyle") {
|
||||
cs.paragraph_style = Some(ps.to_string());
|
||||
|
|
@ -1037,15 +1115,11 @@ fn cell_style_create(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn cell_style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.cell.apply", "missing name"))?.to_string();
|
||||
let cs = s
|
||||
.doc()?
|
||||
.doc
|
||||
.styles
|
||||
.cell
|
||||
.iter()
|
||||
.find(|c| c.name == name)
|
||||
.cloned()
|
||||
.ok_or_else(|| bad("style.cell.apply", format!("no cell style `{name}`")))?;
|
||||
let styles = &s.doc()?.doc.styles.cell;
|
||||
if !styles.iter().any(|c| c.name == name) {
|
||||
return Err(bad("style.cell.apply", format!("no cell style `{name}`")));
|
||||
}
|
||||
let cs = designcraft_doc::resolve_cell_style(styles, &name);
|
||||
let g = target(s, p, "style.cell.apply")?;
|
||||
edit_table(s, &g, "style.cell.apply", |t| {
|
||||
let owners = t.owners();
|
||||
|
|
@ -1067,7 +1141,7 @@ fn cell_style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
/// Re-apply cell styles named in `names` to every cell using them (after a style edit).
|
||||
fn reapply_cell_styles(d: &mut Document, names: &[String]) {
|
||||
let styles = d.styles.cell.clone();
|
||||
let styles: Vec<_> = names.iter().map(|name| designcraft_doc::resolve_cell_style(&d.styles.cell, name)).collect();
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
let Some(st) = d.story_mut(sid) else { continue };
|
||||
for t in st.tables.values_mut() {
|
||||
|
|
@ -1086,18 +1160,42 @@ fn reapply_cell_styles(d: &mut Document, names: &[String]) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Include descendants when editing an inherited style, with the resolver's depth limit.
|
||||
fn dependent_styles<'a>(name: &str, styles: impl Iterator<Item = (&'a str, Option<&'a str>)>) -> Vec<String> {
|
||||
let styles: Vec<_> = styles.collect();
|
||||
styles
|
||||
.iter()
|
||||
.filter(|(candidate, _)| {
|
||||
let mut current = Some(*candidate);
|
||||
for _ in 0..32 {
|
||||
let Some(current_name) = current else { return false };
|
||||
if current_name == name {
|
||||
return true;
|
||||
}
|
||||
current = styles.iter().find(|(n, _)| *n == current_name).and_then(|(_, base)| *base);
|
||||
}
|
||||
false
|
||||
})
|
||||
.map(|(name, _)| (*name).to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn cell_style_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.cell.edit", "missing name"))?.to_string();
|
||||
let p = p.clone();
|
||||
s.edit(|d, _| {
|
||||
let cs = d.styles_mut().cell.iter_mut().find(|c| c.name == name).ok_or_else(|| bad("style.cell.edit", format!("no cell style `{name}`")))?;
|
||||
cell_style_fields(cs, &p, "style.cell.edit")?;
|
||||
reapply_cell_styles(d, std::slice::from_ref(&name));
|
||||
let names = dependent_styles(&name, d.styles.cell.iter().map(|cs| (cs.name.as_str(), cs.based_on.as_deref())));
|
||||
reapply_cell_styles(d, &names);
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn table_style_fields(ts: &mut designcraft_doc::TableStyle, p: &Value) {
|
||||
if let Some(v) = p.get("basedOn") {
|
||||
ts.based_on = v.as_str().map(str::to_string);
|
||||
}
|
||||
for (k, f) in [
|
||||
("header", &mut ts.header),
|
||||
("body", &mut ts.body),
|
||||
|
|
@ -1111,6 +1209,7 @@ fn table_style_fields(ts: &mut designcraft_doc::TableStyle, p: &Value) {
|
|||
}
|
||||
if let Some(b) = stroke_param(p.get("border")) {
|
||||
ts.border = Some(b);
|
||||
ts.borders = Default::default();
|
||||
}
|
||||
if let Some(a) = p.get("altRows").and_then(Value::as_object) {
|
||||
let n = |k: &str, d: u32| a.get(k).and_then(Value::as_u64).map_or(d, |v| v as u32);
|
||||
|
|
@ -1150,7 +1249,10 @@ fn table_style_create(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
fn table_style_apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.table.apply", "missing name"))?.to_string();
|
||||
let doc = &s.doc()?.doc;
|
||||
let ts = doc.styles.table.iter().find(|c| c.name == name).cloned().ok_or_else(|| bad("style.table.apply", format!("no table style `{name}`")))?;
|
||||
if !doc.styles.table.iter().any(|c| c.name == name) {
|
||||
return Err(bad("style.table.apply", format!("no table style `{name}`")));
|
||||
}
|
||||
let ts = doc.styles.resolve_table_style(&name);
|
||||
let cells = doc.styles.cell.clone();
|
||||
let g = target(s, p, "style.table.apply")?;
|
||||
edit_table(s, &g, "style.table.apply", |t| {
|
||||
|
|
@ -1166,11 +1268,13 @@ fn table_style_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let ts =
|
||||
d.styles_mut().table.iter_mut().find(|c| c.name == name).ok_or_else(|| bad("style.table.edit", format!("no table style `{name}`")))?;
|
||||
table_style_fields(ts, &p);
|
||||
let (ts, cells) = (ts.clone(), d.styles.cell.clone());
|
||||
let names = dependent_styles(&name, d.styles.table.iter().map(|ts| (ts.name.as_str(), ts.based_on.as_deref())));
|
||||
let styles: Vec<_> = names.iter().map(|name| d.styles.resolve_table_style(name)).collect();
|
||||
let cells = d.styles.cell.clone();
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
let Some(st) = d.story_mut(sid) else { continue };
|
||||
for t in st.tables.values_mut() {
|
||||
if t.style == name {
|
||||
if let Some(ts) = styles.iter().find(|ts| ts.name == t.style) {
|
||||
ts.apply_to(std::sync::Arc::make_mut(t), &cells);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,3 +176,226 @@ fn file_roundtrip_keeps_tables() {
|
|||
let st = back.story(designcraft_doc::StoryId(sid)).unwrap();
|
||||
assert_eq!(st.tables.values().next().unwrap().cells[0].text.text, "A1");
|
||||
}
|
||||
|
||||
fn table_for(s: &Session, sid: u64, tid: u64) -> &doc::Table {
|
||||
s.doc().unwrap().doc.story(doc::StoryId(sid)).unwrap().tables.get(&tid).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_cell_edges_win_imported_priorities_and_keep_undo_and_file_roundtrip() {
|
||||
for (direction, neighbors) in [
|
||||
(doc::TextDirection::LeftToRight, [(0, 1, 2), (1, 0, 3), (1, 2, 1), (2, 1, 0)]),
|
||||
(doc::TextDirection::RightToLeft, [(0, 1, 2), (1, 0, 1), (1, 2, 3), (2, 1, 0)]),
|
||||
] {
|
||||
for imported_priority in [-7, 41, i32::MAX] {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 3, "cols": 3})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.edit(|d, _| {
|
||||
let t = d.story_mut(doc::StoryId(sid)).unwrap().table_mut(tid).unwrap();
|
||||
t.options.border = doc::CellStroke::none();
|
||||
t.options.direction = direction;
|
||||
for cell in &mut t.cells {
|
||||
cell.strokes = std::array::from_fn(|_| doc::CellStroke::none());
|
||||
}
|
||||
t.cell_mut(0, 0).unwrap().stroke_priorities[0] = -9;
|
||||
for (row, col, edge) in neighbors {
|
||||
let cell = t.cell_mut(row, col).unwrap();
|
||||
cell.strokes[edge] = doc::CellStroke { weight: 7.0, ..Default::default() };
|
||||
cell.stroke_defined[edge] = true;
|
||||
cell.stroke_priorities[edge] = imported_priority;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = table_for(&s, sid, tid).clone();
|
||||
s.execute("table.setCell", &json!({"row": 1, "col": 1, "stroke": {"weight": 0, "edges": "all"}})).unwrap();
|
||||
let after = table_for(&s, sid, tid).clone();
|
||||
assert_eq!(after.cell(0, 0).unwrap().stroke_priorities[0], -9, "unrelated signed priorities are preserved");
|
||||
let edited = after.cell(1, 1).unwrap();
|
||||
let priority = if imported_priority == i32::MAX { 2 } else { imported_priority.max(0) + 1 };
|
||||
assert_eq!(edited.stroke_defined, [true; 4]);
|
||||
assert_eq!(edited.border_overrides, [true; 4]);
|
||||
assert_eq!(edited.stroke_priorities, [priority; 4]);
|
||||
for (row, col, edge) in neighbors {
|
||||
let neighbor = after.cell(row, col).unwrap();
|
||||
assert_eq!(neighbor.strokes[edge].weight, 0.0);
|
||||
assert!(neighbor.stroke_defined[edge]);
|
||||
assert!(neighbor.border_overrides[edge]);
|
||||
assert_eq!(neighbor.stroke_priorities[edge], priority);
|
||||
}
|
||||
let composed = s.cache.get(&s.doc().unwrap().doc, doc::StoryId(sid), None);
|
||||
assert!(composed.frames.iter().flat_map(|f| &f.tables).all(|t| t.strokes.is_empty()));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(table_for(&s, sid, tid), &before);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(table_for(&s, sid, tid), &after);
|
||||
let bytes = crate::cmd::file_bytes(&s.doc().unwrap().doc);
|
||||
let back = crate::cmd::file_from(&bytes).unwrap();
|
||||
assert_eq!(back.story(doc::StoryId(sid)).unwrap().tables.get(&tid).unwrap().as_ref(), &after);
|
||||
check(&s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perimeter_edits_override_imported_sides_and_uniform_border_clears_side_options() {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.edit(|d, _| {
|
||||
let t = d.story_mut(doc::StoryId(sid)).unwrap().table_mut(tid).unwrap();
|
||||
t.options.border.weight = 4.0;
|
||||
t.options.borders = std::array::from_fn(|_| Some(doc::CellStroke { weight: 9.0, ..Default::default() }));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("table.setCell", &json!({"stroke": {"tint": 0.25, "edges": "left"}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
let cell = t.cell(0, 0).unwrap();
|
||||
assert_eq!(cell.stroke_defined, [false, true, false, false]);
|
||||
assert_eq!(cell.border_overrides, [false, true, false, false]);
|
||||
assert_eq!(cell.strokes[1].weight, 9.0, "partial edits preserve the visible perimeter base");
|
||||
assert_eq!(cell.strokes[1].tint, 0.25);
|
||||
let before = t.clone();
|
||||
s.execute("table.options", &json!({"border": {"weight": 3}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert!(t.options.borders.iter().all(Option::is_none));
|
||||
assert_eq!(t.options.border.weight, 3.0);
|
||||
let composed = s.cache.get(&s.doc().unwrap().doc, doc::StoryId(sid), None);
|
||||
let frag = composed.frames.iter().flat_map(|f| &f.tables).next().unwrap();
|
||||
assert_eq!(frag.strokes.iter().filter(|s| s.stroke.weight == 3.0).count(), 3);
|
||||
assert_eq!(frag.strokes.iter().filter(|s| s.stroke.weight == 9.0 && s.stroke.tint == 0.25).count(), 1);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(table_for(&s, sid, tid), &before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inherited_cell_and_table_styles_apply_and_follow_base_edits() {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 1, "cols": 2})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.execute("style.cell.create", &json!({"name": "Cell Base", "insets": 7, "stroke": {"weight": 2}})).unwrap();
|
||||
s.execute("style.cell.create", &json!({"name": "Cell Child", "basedOn": "Cell Base", "fill": "[Paper]"})).unwrap();
|
||||
s.execute("style.cell.apply", &json!({"name": "Cell Child", "table": tid})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert!(t.cells.iter().all(|cell| cell.style == "Cell Child" && cell.insets == [7.0; 4] && cell.strokes[0].weight == 2.0));
|
||||
s.execute("style.cell.edit", &json!({"name": "Cell Base", "insets": 5})).unwrap();
|
||||
assert!(table_for(&s, sid, tid).cells.iter().all(|cell| cell.insets == [5.0; 4]));
|
||||
s.execute("style.table.create", &json!({"name": "Table Base", "body": "Cell Child", "border": {"weight": 6}, "spaceBefore": 8})).unwrap();
|
||||
s.execute("style.table.create", &json!({"name": "Table Child", "basedOn": "Table Base", "spaceAfter": 3})).unwrap();
|
||||
s.execute("style.table.apply", &json!({"name": "Table Child"})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.style, "Table Child");
|
||||
assert_eq!(t.options.border_for(0).weight, 6.0);
|
||||
assert_eq!(t.options.space_before, 8.0);
|
||||
assert_eq!(t.options.space_after, 3.0);
|
||||
assert!(t.cells.iter().all(|cell| cell.style == "Cell Child" && cell.insets == [5.0; 4]));
|
||||
s.execute("style.table.edit", &json!({"name": "Table Base", "border": {"weight": 4}, "spaceBefore": 10})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.options.border_for(0).weight, 4.0);
|
||||
assert_eq!(t.options.space_before, 10.0);
|
||||
assert_eq!(t.options.space_after, 3.0);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(table_for(&s, sid, tid).options.border_for(0).weight, 6.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uniform_style_edits_replace_imported_per_side_attributes() {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.edit(|d, _| {
|
||||
d.styles_mut().cell.push(doc::CellStyle {
|
||||
name: "Imported Cell".into(),
|
||||
strokes: std::array::from_fn(|_| doc::CellStrokeAttrs { weight: Some(9.0), ..Default::default() }),
|
||||
inset_overrides: [Some(12.0); 4],
|
||||
..Default::default()
|
||||
});
|
||||
d.styles_mut().table.push(doc::TableStyle {
|
||||
name: "Imported Table".into(),
|
||||
borders: std::array::from_fn(|_| doc::CellStrokeAttrs { weight: Some(10.0), ..Default::default() }),
|
||||
..Default::default()
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("style.cell.apply", &json!({"name": "Imported Cell"})).unwrap();
|
||||
s.execute("style.cell.edit", &json!({"name": "Imported Cell", "stroke": {"weight": 2}, "insets": 3})).unwrap();
|
||||
let cell = table_for(&s, sid, tid).cell(0, 0).unwrap();
|
||||
assert!(cell.strokes.iter().all(|edge| edge.weight == 2.0));
|
||||
assert_eq!(cell.insets, [3.0; 4]);
|
||||
s.execute("style.table.apply", &json!({"name": "Imported Table"})).unwrap();
|
||||
s.execute("style.table.edit", &json!({"name": "Imported Table", "border": {"weight": 4}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert!((0..4).all(|edge| t.options.border_for(edge).weight == 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtl_cell_edge_selection_uses_physical_sides() {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 1, "cols": 3})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.execute("table.options", &json!({"direction": "rightToLeft", "border": {"weight": 0}})).unwrap();
|
||||
s.execute("table.setCell", &json!({"cols": [0, 2], "stroke": {"weight": 0}})).unwrap();
|
||||
s.execute("table.setCell", &json!({"row": 0, "col": 0, "stroke": {"weight": 2, "edges": "left"}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[1].weight, 2.0);
|
||||
assert_eq!(t.cell(0, 1).unwrap().strokes[3].weight, 2.0);
|
||||
assert_eq!(t.cell(0, 1).unwrap().strokes[1].weight, 0.0);
|
||||
s.execute("table.setCell", &json!({"cols": [0, 2], "stroke": {"weight": 5, "edges": "outer"}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[3].weight, 5.0);
|
||||
assert_eq!(t.cell(0, 2).unwrap().strokes[1].weight, 5.0);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[1].weight, 2.0);
|
||||
assert_eq!(t.cell(0, 2).unwrap().strokes[3].weight, 0.0);
|
||||
s.execute("table.setCell", &json!({"cols": [0, 2], "stroke": {"weight": 0, "edges": "inner"}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[3].weight, 5.0);
|
||||
assert_eq!(t.cell(0, 2).unwrap().strokes[1].weight, 5.0);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[1].weight, 0.0);
|
||||
assert_eq!(t.cell(0, 1).unwrap().strokes[3].weight, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_beside_a_merged_cell_only_changes_the_selected_edge_segment() {
|
||||
for direction in [doc::TextDirection::LeftToRight, doc::TextDirection::RightToLeft] {
|
||||
for imported_priority in [41, i32::MAX] {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let r = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap();
|
||||
let tid = r["table"].as_u64().unwrap();
|
||||
s.execute("table.merge", &json!({"row": 0, "cols": [0, 1]})).unwrap();
|
||||
s.edit(|d, _| {
|
||||
let t = d.story_mut(doc::StoryId(sid)).unwrap().table_mut(tid).unwrap();
|
||||
t.options.direction = direction;
|
||||
t.options.border = doc::CellStroke::none();
|
||||
for cell in &mut t.cells {
|
||||
cell.strokes = std::array::from_fn(|_| doc::CellStroke::none());
|
||||
}
|
||||
let merged = t.cell_mut(0, 0).unwrap();
|
||||
merged.strokes[2] = doc::CellStroke { weight: 7.0, ..Default::default() };
|
||||
merged.stroke_defined[2] = true;
|
||||
merged.stroke_priorities[2] = imported_priority;
|
||||
t.cell_mut(1, 1).unwrap().stroke_priorities[2] = 7;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let before = table_for(&s, sid, tid).clone();
|
||||
s.execute("table.setCell", &json!({"row": 1, "col": 0, "stroke": {"weight": 0, "edges": "top"}})).unwrap();
|
||||
let t = table_for(&s, sid, tid);
|
||||
assert_eq!(t.cell(0, 0).unwrap().strokes[2].weight, 7.0, "the merged edge itself must not be erased");
|
||||
let priorities =
|
||||
[t.cell(1, 1).unwrap().stroke_priorities[2], t.cell(0, 0).unwrap().stroke_priorities[2], t.cell(1, 0).unwrap().stroke_priorities[0]];
|
||||
assert_eq!(priorities, if imported_priority == i32::MAX { [1, 2, 3] } else { [7, 41, 42] });
|
||||
let composed = s.cache.get(&s.doc().unwrap().doc, doc::StoryId(sid), None);
|
||||
let frag = composed.frames.iter().flat_map(|f| &f.tables).next().unwrap();
|
||||
let unselected = frag.cell(1, 1).unwrap().rect;
|
||||
let visible: Vec<_> = frag.strokes.iter().filter(|s| s.stroke.weight == 7.0).collect();
|
||||
assert_eq!(visible.len(), 1);
|
||||
assert_eq!(visible[0].a, designcraft_geom::Point::new(unselected.x0, unselected.y0));
|
||||
assert_eq!(visible[0].b, designcraft_geom::Point::new(unselected.x1, unselected.y0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(table_for(&s, sid, tid), &before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -886,6 +886,11 @@ impl<'a> Ex<'a> {
|
|||
el.set(k, num(v));
|
||||
}
|
||||
}
|
||||
for (key, value) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().zip(cs.inset_overrides) {
|
||||
if let Some(value) = value {
|
||||
el.set(key, num(value));
|
||||
}
|
||||
}
|
||||
if let Some(vj) = cs.vj {
|
||||
el.set("VerticalJustification", names::vj_out(vj));
|
||||
}
|
||||
|
|
@ -894,20 +899,32 @@ impl<'a> Ex<'a> {
|
|||
self.cell_stroke_attrs(&mut el, side, st);
|
||||
}
|
||||
}
|
||||
for (side, attrs) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().zip(&cs.strokes) {
|
||||
self.partial_cell_stroke_attrs(&mut el, side, attrs);
|
||||
}
|
||||
if let Some(ps) = &cs.paragraph_style {
|
||||
el.set("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, ps));
|
||||
}
|
||||
cells = cells.child(with_props(el, vec![p("BasedOn", "string", "$ID/[None]")]));
|
||||
let base = cs.based_on.as_deref().unwrap_or(designcraft_doc::NO_CELL_STYLE);
|
||||
let based_on = if base == designcraft_doc::NO_CELL_STYLE {
|
||||
p("BasedOn", "string", "$ID/[None]")
|
||||
} else {
|
||||
p("BasedOn", "object", names::style_self("CellStyle", names::CELL_BUILTINS, base))
|
||||
};
|
||||
cells = cells.child(with_props(el, vec![based_on]));
|
||||
}
|
||||
root.push(cells);
|
||||
let mut tables = El::new("RootTableStyleGroup")
|
||||
.attr("Self", self.fresh())
|
||||
.child(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]"))
|
||||
.child(with_props(
|
||||
let mut tables = El::new("RootTableStyleGroup").attr("Self", self.fresh());
|
||||
if !styles.table.iter().any(|s| s.name == designcraft_doc::NO_TABLE_STYLE) {
|
||||
tables.push(El::new("TableStyle").attr("Self", "TableStyle/$ID/[No table style]").attr("Name", "$ID/[No table style]"));
|
||||
}
|
||||
if !styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE) {
|
||||
tables.push(with_props(
|
||||
El::new("TableStyle").attr("Self", "TableStyle/$ID/[Basic Table]").attr("Name", "$ID/[Basic Table]"),
|
||||
vec![p("BasedOn", "string", "$ID/[No table style]")],
|
||||
));
|
||||
for ts in styles.table.iter().filter(|t| t.name != designcraft_doc::BASIC_TABLE) {
|
||||
}
|
||||
for ts in &styles.table {
|
||||
let mut el = El::new("TableStyle")
|
||||
.attr("Self", names::style_self("TableStyle", names::TABLE_BUILTINS, &ts.name))
|
||||
.attr("Name", names::style_name_out(names::TABLE_BUILTINS, &ts.name));
|
||||
|
|
@ -922,11 +939,24 @@ impl<'a> Ex<'a> {
|
|||
el.set(k, names::style_self("CellStyle", names::CELL_BUILTINS, n));
|
||||
}
|
||||
}
|
||||
for (key, value) in [
|
||||
("HeaderRegionSameAsBodyRegion", ts.header_same_as_body),
|
||||
("FooterRegionSameAsBodyRegion", ts.footer_same_as_body),
|
||||
("LeftColumnRegionSameAsBodyRegion", ts.left_column_same_as_body),
|
||||
("RightColumnRegionSameAsBodyRegion", ts.right_column_same_as_body),
|
||||
] {
|
||||
if let Some(value) = value {
|
||||
el.set(key, value);
|
||||
}
|
||||
}
|
||||
if let Some(b) = &ts.border {
|
||||
for side in ["Top", "Left", "Bottom", "Right"] {
|
||||
self.cell_stroke_attrs(&mut el, &format!("{side}Border"), b);
|
||||
}
|
||||
}
|
||||
for (side, attrs) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&ts.borders) {
|
||||
self.partial_cell_stroke_attrs(&mut el, side, attrs);
|
||||
}
|
||||
if let Some(a) = &ts.alt_rows {
|
||||
el.set("StartRowFillColor", self.sw(&a.first_color));
|
||||
el.set("StartRowFillCount", a.first);
|
||||
|
|
@ -935,13 +965,26 @@ impl<'a> Ex<'a> {
|
|||
el.set("EndRowFillCount", a.next);
|
||||
el.set("EndRowFillTint", pct(a.next_tint as f64));
|
||||
}
|
||||
self.partial_alt_fill_attrs(&mut el, "Row", &ts.row_fills);
|
||||
self.partial_alt_fill_attrs(&mut el, "Column", &ts.column_fills);
|
||||
self.partial_alt_stroke_attrs(&mut el, "Row", &ts.row_strokes);
|
||||
self.partial_alt_stroke_attrs(&mut el, "Column", &ts.column_strokes);
|
||||
if let Some(v) = ts.space_before {
|
||||
el.set("SpaceBefore", num(v));
|
||||
}
|
||||
if let Some(v) = ts.space_after {
|
||||
el.set("SpaceAfter", num(v));
|
||||
}
|
||||
tables = tables.child(with_props(el, vec![p("BasedOn", "string", "$ID/[Basic Table]")]));
|
||||
let mut props = Vec::new();
|
||||
if ts.name != designcraft_doc::NO_TABLE_STYLE {
|
||||
let base = ts.based_on.as_deref().unwrap_or(designcraft_doc::NO_TABLE_STYLE);
|
||||
props.push(if base == designcraft_doc::NO_TABLE_STYLE {
|
||||
p("BasedOn", "string", "$ID/[No table style]")
|
||||
} else {
|
||||
p("BasedOn", "object", names::style_self("TableStyle", names::TABLE_BUILTINS, base))
|
||||
});
|
||||
}
|
||||
tables = tables.child(with_props(el, props));
|
||||
}
|
||||
root.push(tables);
|
||||
// Named numbered lists.
|
||||
|
|
@ -2137,6 +2180,71 @@ impl<'a> Ex<'a> {
|
|||
el.set(&format!("{prefix}StrokeType"), names::stroke_type_out(&s.kind));
|
||||
}
|
||||
|
||||
fn partial_cell_stroke_attrs(&self, el: &mut El, prefix: &str, attrs: &designcraft_doc::CellStrokeAttrs) {
|
||||
if let Some(value) = attrs.weight {
|
||||
el.set(&format!("{prefix}StrokeWeight"), num(value));
|
||||
}
|
||||
if let Some(value) = &attrs.color {
|
||||
el.set(&format!("{prefix}StrokeColor"), self.sw(value));
|
||||
}
|
||||
if let Some(value) = attrs.tint {
|
||||
el.set(&format!("{prefix}StrokeTint"), pct(value as f64));
|
||||
}
|
||||
if let Some(value) = &attrs.kind {
|
||||
el.set(&format!("{prefix}StrokeType"), names::stroke_type_out(value));
|
||||
}
|
||||
}
|
||||
|
||||
fn partial_alt_fill_attrs(&self, el: &mut El, kind: &str, attrs: &designcraft_doc::AltFillsAttrs) {
|
||||
if let Some(value) = attrs.first {
|
||||
el.set(&format!("Start{kind}FillCount"), value);
|
||||
}
|
||||
if let Some(value) = &attrs.first_color {
|
||||
el.set(&format!("Start{kind}FillColor"), self.sw(value));
|
||||
}
|
||||
if let Some(value) = attrs.first_tint {
|
||||
el.set(&format!("Start{kind}FillTint"), pct(value as f64));
|
||||
}
|
||||
if let Some(value) = attrs.next {
|
||||
el.set(&format!("End{kind}FillCount"), value);
|
||||
}
|
||||
if let Some(value) = &attrs.next_color {
|
||||
el.set(&format!("End{kind}FillColor"), self.sw(value));
|
||||
}
|
||||
if let Some(value) = attrs.next_tint {
|
||||
el.set(&format!("End{kind}FillTint"), pct(value as f64));
|
||||
}
|
||||
if let Some(value) = attrs.skip_first {
|
||||
el.set(&format!("SkipFirstAlternatingFill{kind}s"), value);
|
||||
}
|
||||
if let Some(value) = attrs.skip_last {
|
||||
el.set(&format!("SkipLastAlternatingFill{kind}s"), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn partial_alt_stroke_attrs(&self, el: &mut El, kind: &str, attrs: &designcraft_doc::AltStrokesAttrs) {
|
||||
if let Some(value) = attrs.first {
|
||||
el.set(&format!("Start{kind}StrokeCount"), value);
|
||||
}
|
||||
if let Some(value) = attrs.next {
|
||||
el.set(&format!("End{kind}StrokeCount"), value);
|
||||
}
|
||||
self.partial_cell_stroke_attrs(el, &format!("Start{kind}"), &attrs.first_stroke);
|
||||
self.partial_cell_stroke_attrs(el, &format!("End{kind}"), &attrs.next_stroke);
|
||||
if kind == "Column"
|
||||
&& let Some(value) = &attrs.next_stroke.kind
|
||||
{
|
||||
el.attrs.retain(|(key, _)| key != "EndColumnStrokeType");
|
||||
el.set("EndColumnLineStyle", names::stroke_type_out(value));
|
||||
}
|
||||
if let Some(value) = attrs.skip_first {
|
||||
el.set(&format!("SkipFirstAlternatingStroke{kind}s"), value);
|
||||
}
|
||||
if let Some(value) = attrs.skip_last {
|
||||
el.set(&format!("SkipLastAlternatingStroke{kind}s"), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// `<Table>` with rows, columns and cells (cell names are `column:row`).
|
||||
fn table_el(&mut self, t: &designcraft_doc::Table) -> El {
|
||||
let id = uid(t.id);
|
||||
|
|
@ -2159,8 +2267,8 @@ impl<'a> Ex<'a> {
|
|||
.attr("SpaceAfter", num(t.options.space_after))
|
||||
.attr("HeaderBehavior", if t.options.repeat_header { "RepeatOnEachTextColumn" } else { "RepeatOnce" })
|
||||
.attr("FooterBehavior", if t.options.repeat_footer { "RepeatOnEachTextColumn" } else { "RepeatOnce" });
|
||||
for side in ["Top", "Left", "Bottom", "Right"] {
|
||||
self.cell_stroke_attrs(&mut el, &format!("{side}Border"), &t.options.border);
|
||||
for (i, side) in ["Top", "Left", "Bottom", "Right"].iter().enumerate() {
|
||||
self.cell_stroke_attrs(&mut el, &format!("{side}Border"), t.options.border_for(i));
|
||||
}
|
||||
for (kind, alt) in [("Row", &t.options.alt_rows), ("Column", &t.options.alt_cols)] {
|
||||
if let Some(a) = alt {
|
||||
|
|
@ -2172,6 +2280,27 @@ impl<'a> Ex<'a> {
|
|||
el.set(&format!("End{kind}FillTint"), pct(a.next_tint as f64));
|
||||
el.set(&format!("SkipFirstAlternatingFill{kind}s"), a.skip_first);
|
||||
el.set(&format!("SkipLastAlternatingFill{kind}s"), a.skip_last);
|
||||
} else {
|
||||
// A local off value must override any enabled pattern in the named style.
|
||||
el.set(&format!("Start{kind}FillCount"), 0);
|
||||
el.set(&format!("End{kind}FillCount"), 0);
|
||||
}
|
||||
}
|
||||
for (kind, strokes) in [("Row", &t.options.row_strokes), ("Column", &t.options.column_strokes)] {
|
||||
if let Some(strokes) = strokes {
|
||||
el.set(&format!("Start{kind}StrokeCount"), strokes.first);
|
||||
el.set(&format!("End{kind}StrokeCount"), strokes.next);
|
||||
self.cell_stroke_attrs(&mut el, &format!("Start{kind}"), &strokes.first_stroke);
|
||||
self.cell_stroke_attrs(&mut el, &format!("End{kind}"), &strokes.next_stroke);
|
||||
if kind == "Column" {
|
||||
el.attrs.retain(|(key, _)| key != "EndColumnStrokeType");
|
||||
el.set("EndColumnLineStyle", names::stroke_type_out(&strokes.next_stroke.kind));
|
||||
}
|
||||
el.set(&format!("SkipFirstAlternatingStroke{kind}s"), strokes.skip_first);
|
||||
el.set(&format!("SkipLastAlternatingStroke{kind}s"), strokes.skip_last);
|
||||
} else {
|
||||
el.set(&format!("Start{kind}StrokeCount"), 0);
|
||||
el.set(&format!("End{kind}StrokeCount"), 0);
|
||||
}
|
||||
}
|
||||
for (r, row) in t.rows.iter().enumerate() {
|
||||
|
|
@ -2223,9 +2352,21 @@ impl<'a> Ex<'a> {
|
|||
.attr("VerticalJustification", vj)
|
||||
.attr("RotationAngle", num(cell.rotation));
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
self.cell_stroke_attrs(&mut ce, side, &cell.strokes[i]);
|
||||
if cell.border_overrides[i] {
|
||||
ce.set(&format!("{side}StrokePriority"), 1);
|
||||
// Do not turn absent native defaults into explicit shared-edge candidates.
|
||||
if cell.stroke_defined[i]
|
||||
|| cell.border_overrides[i]
|
||||
|| cell.stroke_priorities[i] != 0
|
||||
|| cell.strokes[i] != designcraft_doc::CellStroke::default()
|
||||
{
|
||||
self.cell_stroke_attrs(&mut ce, side, &cell.strokes[i]);
|
||||
}
|
||||
let priority = if cell.border_overrides[i] && !cell.stroke_defined[i] {
|
||||
cell.stroke_priorities[i].max(1)
|
||||
} else {
|
||||
cell.stroke_priorities[i]
|
||||
};
|
||||
if priority != 0 {
|
||||
ce.set(&format!("{side}StrokePriority"), priority);
|
||||
}
|
||||
}
|
||||
for psr in self.story_paras(&cell.text) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ use std::sync::Arc;
|
|||
use designcraft_color::cms::lab;
|
||||
use designcraft_color::{Color, ColorType, Gradient, GradientKind, GradientStop, Swatch, SwatchValue, swatch};
|
||||
use designcraft_doc::{
|
||||
AltFills, Asset, AssetId, BaselineGrid, Cell, CellRange, CellStroke, CharAttrs, CharFormat, CharRun, CharacterStyle, Columns, ColumnsKind,
|
||||
Content, DocSettings, Document, DropShadow, Effects, Fill, GridAlign, GridRelative, Guide, Item, ItemId, Kerning, LAYER_COLORS, Layer, LayerId,
|
||||
ListType, Margins, ObjectStyle, Page, PageId, PageSide, ParaAttrs, ParaFormat, ParagraphStyle, ParentInfo, RowHeightMode, Rule, Section, Shape,
|
||||
SpanColumns, Spread, SpreadId, Story, StoryId, Stroke, Styles, TabStop, Table, TextFrame, TextFrameOptions, TextWrap, VerticalJustification,
|
||||
story as st,
|
||||
Asset, AssetId, BaselineGrid, CellRange, CharAttrs, CharFormat, CharRun, CharacterStyle, Columns, ColumnsKind, Content, DocSettings, Document,
|
||||
DropShadow, Effects, Fill, GridAlign, GridRelative, Guide, Item, ItemId, Kerning, LAYER_COLORS, Layer, LayerId, ListType, Margins, ObjectStyle,
|
||||
Page, PageId, PageSide, ParaAttrs, ParaFormat, ParagraphStyle, ParentInfo, RowHeightMode, Rule, Section, Shape, SpanColumns, Spread, SpreadId,
|
||||
Story, StoryId, Stroke, Styles, TabStop, Table, TextFrame, TextFrameOptions, TextWrap, story as st,
|
||||
};
|
||||
use designcraft_geom::corners::{Corner, CornerOptions};
|
||||
use designcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath};
|
||||
|
|
@ -20,6 +19,16 @@ use crate::names::{self, CHAR_BUILTINS, OBJECT_BUILTINS, PARA_BUILTINS, unescape
|
|||
use crate::xml::{El, Node, parse};
|
||||
use crate::{IdmlError, MIMETYPE, Result, base64_decode, sniff_image};
|
||||
|
||||
/// Preserve the legacy uniform field only when all four sides explicitly specify every
|
||||
/// property. Partial records must stay sparse so they can inherit from a parent style.
|
||||
fn uniform_cell_stroke(edges: &[designcraft_doc::CellStrokeAttrs; 4]) -> Option<designcraft_doc::CellStroke> {
|
||||
let first = edges.first()?;
|
||||
if edges.iter().any(|edge| edge != first) {
|
||||
return None;
|
||||
}
|
||||
Some(designcraft_doc::CellStroke { weight: first.weight?, color: first.color.clone()?, tint: first.tint?, kind: first.kind.clone()? })
|
||||
}
|
||||
|
||||
/// Import an IDML package. Linked images are read from disk when available (not on wasm).
|
||||
pub fn import_idml(bytes: &[u8]) -> Result<Document> {
|
||||
import_idml_with(bytes, &default_reader)
|
||||
|
|
@ -109,6 +118,8 @@ struct Importer<'r> {
|
|||
char_names: HashMap<String, String>,
|
||||
lists: Vec<designcraft_doc::NumberedList>,
|
||||
object_names: HashMap<String, String>,
|
||||
cell_names: HashMap<String, String>,
|
||||
table_names: HashMap<String, String>,
|
||||
/// Object style Self → element (attribute fallback for page items).
|
||||
object_els: HashMap<String, El>,
|
||||
layers: Vec<Layer>,
|
||||
|
|
@ -193,6 +204,8 @@ impl<'r> Importer<'r> {
|
|||
char_names: HashMap::new(),
|
||||
lists: Vec::new(),
|
||||
object_names: HashMap::new(),
|
||||
cell_names: HashMap::new(),
|
||||
table_names: HashMap::new(),
|
||||
object_els: HashMap::new(),
|
||||
layers: Vec::new(),
|
||||
layer_ids: HashMap::new(),
|
||||
|
|
@ -815,30 +828,31 @@ impl<'r> Importer<'r> {
|
|||
None => self.styles.character.push(s),
|
||||
}
|
||||
}
|
||||
// Cell and table styles (the built-ins stay as they are).
|
||||
let cell_ref = |r: &str| names::style_name_in(names::CELL_BUILTINS, &unescape_id(r.trim_start_matches("CellStyle/")));
|
||||
// Keep built-ins as well: [Basic Table] is editable, and [No table style]
|
||||
// supplies the defaults used by named styles in ordinary IDML files.
|
||||
for (id, e) in &cell_styles {
|
||||
self.cell_names.insert(id.clone(), names::style_name_in(names::CELL_BUILTINS, e.get("Name").unwrap_or("")));
|
||||
}
|
||||
for (id, e) in &table_styles {
|
||||
self.table_names.insert(id.clone(), names::style_name_in(names::TABLE_BUILTINS, e.get("Name").unwrap_or("")));
|
||||
}
|
||||
for (_, e) in &cell_styles {
|
||||
let name = names::style_name_in(names::CELL_BUILTINS, e.get("Name").unwrap_or(""));
|
||||
if name == designcraft_doc::NO_CELL_STYLE {
|
||||
continue;
|
||||
}
|
||||
let mut cs = designcraft_doc::CellStyle { name: name.clone(), ..Default::default() };
|
||||
cs.fill = e.get("FillColor").map(|c| self.swatch_ref(c));
|
||||
cs.fill_tint = e.num("FillTint").and_then(|v| tint(Some(v)));
|
||||
let ins: Vec<Option<f64>> = ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().map(|k| e.num(k)).collect();
|
||||
if ins.iter().all(Option::is_some) {
|
||||
cs.insets = Some([ins[0].unwrap_or(0.0), ins[1].unwrap_or(0.0), ins[2].unwrap_or(0.0), ins[3].unwrap_or(0.0)]);
|
||||
cs.based_on =
|
||||
e.prop("BasedOn").map(|r| self.cell_style_ref(&r)).or_else(|| Some(designcraft_doc::NO_CELL_STYLE.into())).filter(|n| *n != name);
|
||||
cs.fill = e.prop("FillColor").map(|c| self.swatch_ref(&c));
|
||||
cs.fill_tint = tint(e.num("FillTint"));
|
||||
cs.inset_overrides = ["TopInset", "LeftInset", "BottomInset", "RightInset"].map(|k| e.num(k).map(|v| v.max(0.0)));
|
||||
if let [Some(top), Some(left), Some(bottom), Some(right)] = cs.inset_overrides {
|
||||
cs.insets = Some([top, left, bottom, right]);
|
||||
cs.inset_overrides = [None; 4];
|
||||
}
|
||||
cs.vj = e.get("VerticalJustification").map(names::vj_in);
|
||||
if e.get("TopEdgeStrokeWeight").is_some() || e.get("TopEdgeStrokeColor").is_some() {
|
||||
let mut st = CellStroke::default();
|
||||
if let Some(w) = e.num("TopEdgeStrokeWeight") {
|
||||
st.weight = w.max(0.0);
|
||||
}
|
||||
if let Some(c) = e.get("TopEdgeStrokeColor") {
|
||||
st.color = self.swatch_ref(c);
|
||||
}
|
||||
cs.stroke = Some(st);
|
||||
cs.strokes = ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].map(|side| self.cell_stroke_attrs(e, side));
|
||||
if let Some(stroke) = uniform_cell_stroke(&cs.strokes) {
|
||||
cs.stroke = Some(stroke);
|
||||
cs.strokes = Default::default();
|
||||
}
|
||||
cs.paragraph_style = e.get("AppliedParagraphStyle").map(|r| self.para_style_ref(r)).filter(|n| n != designcraft_doc::NO_PARA_STYLE);
|
||||
match self.styles.cell.iter_mut().find(|c| c.name == name) {
|
||||
|
|
@ -848,37 +862,29 @@ impl<'r> Importer<'r> {
|
|||
}
|
||||
for (_, e) in &table_styles {
|
||||
let name = names::style_name_in(names::TABLE_BUILTINS, e.get("Name").unwrap_or(""));
|
||||
if name == designcraft_doc::BASIC_TABLE || name == "[No table style]" {
|
||||
continue;
|
||||
}
|
||||
let mut ts = designcraft_doc::TableStyle { name: name.clone(), ..Default::default() };
|
||||
ts.header = e.get("HeaderRegionCellStyle").map(cell_ref).filter(|n| n != designcraft_doc::NO_CELL_STYLE);
|
||||
ts.body = e.get("BodyRegionCellStyle").map(cell_ref).filter(|n| n != designcraft_doc::NO_CELL_STYLE);
|
||||
ts.footer = e.get("FooterRegionCellStyle").map(cell_ref).filter(|n| n != designcraft_doc::NO_CELL_STYLE);
|
||||
ts.left_column = e.get("LeftColumnRegionCellStyle").map(cell_ref).filter(|n| n != designcraft_doc::NO_CELL_STYLE);
|
||||
ts.right_column = e.get("RightColumnRegionCellStyle").map(cell_ref).filter(|n| n != designcraft_doc::NO_CELL_STYLE);
|
||||
if e.get("TopBorderStrokeWeight").is_some() {
|
||||
let mut b = CellStroke::default();
|
||||
if let Some(w) = e.num("TopBorderStrokeWeight") {
|
||||
b.weight = w.max(0.0);
|
||||
}
|
||||
if let Some(c) = e.get("TopBorderStrokeColor") {
|
||||
b.color = self.swatch_ref(c);
|
||||
}
|
||||
ts.border = Some(b);
|
||||
}
|
||||
if let Some(first) = e.num("StartRowFillCount").filter(|n| *n > 0.0) {
|
||||
ts.alt_rows = Some(AltFills {
|
||||
first: first as u32,
|
||||
first_color: e.get("StartRowFillColor").map(|c| self.swatch_ref(c)).unwrap_or_else(|| swatch::NONE.into()),
|
||||
first_tint: tint(e.num("StartRowFillTint")).unwrap_or(1.0),
|
||||
next: e.num("EndRowFillCount").unwrap_or(1.0) as u32,
|
||||
next_color: e.get("EndRowFillColor").map(|c| self.swatch_ref(c)).unwrap_or_else(|| swatch::NONE.into()),
|
||||
next_tint: tint(e.num("EndRowFillTint")).unwrap_or(1.0),
|
||||
skip_first: 0,
|
||||
skip_last: 0,
|
||||
});
|
||||
ts.based_on =
|
||||
e.prop("BasedOn").map(|r| self.table_style_ref(&r)).or_else(|| Some(designcraft_doc::NO_TABLE_STYLE.into())).filter(|n| *n != name);
|
||||
ts.header = e.get("HeaderRegionCellStyle").map(|r| if r == "n" { designcraft_doc::NO_CELL_STYLE.into() } else { self.cell_style_ref(r) });
|
||||
ts.body = e.get("BodyRegionCellStyle").map(|r| if r == "n" { designcraft_doc::NO_CELL_STYLE.into() } else { self.cell_style_ref(r) });
|
||||
ts.footer = e.get("FooterRegionCellStyle").map(|r| if r == "n" { designcraft_doc::NO_CELL_STYLE.into() } else { self.cell_style_ref(r) });
|
||||
ts.left_column =
|
||||
e.get("LeftColumnRegionCellStyle").map(|r| if r == "n" { designcraft_doc::NO_CELL_STYLE.into() } else { self.cell_style_ref(r) });
|
||||
ts.right_column =
|
||||
e.get("RightColumnRegionCellStyle").map(|r| if r == "n" { designcraft_doc::NO_CELL_STYLE.into() } else { self.cell_style_ref(r) });
|
||||
ts.header_same_as_body = e.boolean("HeaderRegionSameAsBodyRegion");
|
||||
ts.footer_same_as_body = e.boolean("FooterRegionSameAsBodyRegion");
|
||||
ts.left_column_same_as_body = e.boolean("LeftColumnRegionSameAsBodyRegion");
|
||||
ts.right_column_same_as_body = e.boolean("RightColumnRegionSameAsBodyRegion");
|
||||
ts.borders = ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].map(|side| self.cell_stroke_attrs(e, side));
|
||||
if let Some(border) = uniform_cell_stroke(&ts.borders) {
|
||||
ts.border = Some(border);
|
||||
ts.borders = Default::default();
|
||||
}
|
||||
ts.row_fills = self.alt_fill_attrs(e, "Row");
|
||||
ts.column_fills = self.alt_fill_attrs(e, "Column");
|
||||
ts.row_strokes = self.alt_stroke_attrs(e, "Row");
|
||||
ts.column_strokes = self.alt_stroke_attrs(e, "Column");
|
||||
ts.space_before = e.num("SpaceBefore");
|
||||
ts.space_after = e.num("SpaceAfter");
|
||||
match self.styles.table.iter_mut().find(|c| c.name == name) {
|
||||
|
|
@ -1386,12 +1392,73 @@ impl<'r> Importer<'r> {
|
|||
Some(r)
|
||||
}
|
||||
|
||||
fn cell_style_ref(&self, reference: &str) -> String {
|
||||
let reference = reference.trim();
|
||||
self.cell_names
|
||||
.get(reference)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| names::style_name_in(names::CELL_BUILTINS, &unescape_id(reference.trim_start_matches("CellStyle/"))))
|
||||
}
|
||||
|
||||
fn table_style_ref(&self, reference: &str) -> String {
|
||||
let reference = reference.trim();
|
||||
self.table_names
|
||||
.get(reference)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| names::style_name_in(names::TABLE_BUILTINS, &unescape_id(reference.trim_start_matches("TableStyle/"))))
|
||||
}
|
||||
|
||||
fn cell_stroke_attrs(&mut self, el: &El, prefix: &str) -> designcraft_doc::CellStrokeAttrs {
|
||||
designcraft_doc::CellStrokeAttrs {
|
||||
weight: el.num(&format!("{prefix}StrokeWeight")).map(|v| v.max(0.0)),
|
||||
color: el.prop(&format!("{prefix}StrokeColor")).map(|v| self.swatch_ref(&v)),
|
||||
tint: tint(el.num(&format!("{prefix}StrokeTint"))),
|
||||
kind: el.prop(&format!("{prefix}StrokeType")).map(|v| names::stroke_type_in(&v)),
|
||||
}
|
||||
}
|
||||
|
||||
fn alt_fill_attrs(&mut self, el: &El, kind: &str) -> designcraft_doc::AltFillsAttrs {
|
||||
designcraft_doc::AltFillsAttrs {
|
||||
first: el.num(&format!("Start{kind}FillCount")).map(|v| v.max(0.0) as u32),
|
||||
first_color: el.prop(&format!("Start{kind}FillColor")).map(|v| self.swatch_ref(&v)),
|
||||
first_tint: tint(el.num(&format!("Start{kind}FillTint"))),
|
||||
next: el.num(&format!("End{kind}FillCount")).map(|v| v.max(0.0) as u32),
|
||||
next_color: el.prop(&format!("End{kind}FillColor")).map(|v| self.swatch_ref(&v)),
|
||||
next_tint: tint(el.num(&format!("End{kind}FillTint"))),
|
||||
skip_first: el.num(&format!("SkipFirstAlternatingFill{kind}s")).map(|v| v.max(0.0) as u32),
|
||||
skip_last: el.num(&format!("SkipLastAlternatingFill{kind}s")).map(|v| v.max(0.0) as u32),
|
||||
}
|
||||
}
|
||||
|
||||
fn alt_stroke_attrs(&mut self, el: &El, kind: &str) -> designcraft_doc::AltStrokesAttrs {
|
||||
let first_stroke = self.cell_stroke_attrs(el, &format!("Start{kind}"));
|
||||
let mut next_stroke = self.cell_stroke_attrs(el, &format!("End{kind}"));
|
||||
// IDML uses LineStyle for this one attribute (rather than StrokeType).
|
||||
if kind == "Column"
|
||||
&& let Some(value) = el.prop("EndColumnLineStyle")
|
||||
{
|
||||
next_stroke.kind = Some(names::stroke_type_in(&value));
|
||||
}
|
||||
designcraft_doc::AltStrokesAttrs {
|
||||
first: el.num(&format!("Start{kind}StrokeCount")).map(|v| v.max(0.0) as u32),
|
||||
next: el.num(&format!("End{kind}StrokeCount")).map(|v| v.max(0.0) as u32),
|
||||
first_stroke,
|
||||
next_stroke,
|
||||
skip_first: el.num(&format!("SkipFirstAlternatingStroke{kind}s")).map(|v| v.max(0.0) as u32),
|
||||
skip_last: el.num(&format!("SkipLastAlternatingStroke{kind}s")).map(|v| v.max(0.0) as u32),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- stories ----------
|
||||
|
||||
fn story(&mut self, id: StoryId, e: &El) -> Story {
|
||||
self.story_with_para(id, e, ParaFormat::default())
|
||||
}
|
||||
|
||||
fn story_with_para(&mut self, id: StoryId, e: &El, default_para: ParaFormat) -> Story {
|
||||
let mut b = StoryBuilder {
|
||||
text: String::new(),
|
||||
paras: vec![ParaFormat::default()],
|
||||
paras: vec![default_para.clone()],
|
||||
runs: Vec::new(),
|
||||
fresh: true,
|
||||
last: CharFormat::default(),
|
||||
|
|
@ -1403,7 +1470,7 @@ impl<'r> Importer<'r> {
|
|||
index_refs: Vec::new(),
|
||||
objects: Vec::new(),
|
||||
};
|
||||
self.walk_story(e, &mut b, &ParaFormat::default(), &CharAttrs::default(), &CharFormat::default(), None);
|
||||
self.walk_story(e, &mut b, &default_para, &CharAttrs::default(), &CharFormat::default(), None);
|
||||
let StoryBuilder { text, mut paras, runs, last, tables: tbls, notes, anchors, xrefs, index_refs, objects, .. } = b;
|
||||
let mut chars: Vec<CharRun> = runs.into_iter().filter(|r| r.len > 0).collect();
|
||||
if chars.is_empty() {
|
||||
|
|
@ -1490,50 +1557,39 @@ impl<'r> Importer<'r> {
|
|||
if h.saturating_add(f) < nr {
|
||||
t.set_header_footer(h, f);
|
||||
}
|
||||
let stroke = |me: &mut Self, el: &El, prefix: &str, base: &CellStroke| -> CellStroke {
|
||||
let mut s = base.clone();
|
||||
if let Some(w) = el.num(&format!("{prefix}StrokeWeight")) {
|
||||
s.weight = w.max(0.0);
|
||||
}
|
||||
if let Some(c) = el.get(&format!("{prefix}StrokeColor")) {
|
||||
s.color = me.swatch_ref(c);
|
||||
}
|
||||
if let Some(v) = el.num(&format!("{prefix}StrokeTint")) {
|
||||
s.tint = tint(Some(v)).unwrap_or(1.0);
|
||||
}
|
||||
if let Some(v) = el.get(&format!("{prefix}StrokeType")) {
|
||||
s.kind = names::stroke_type_in(v);
|
||||
}
|
||||
s
|
||||
};
|
||||
t.options.border = stroke(self, e, "TopBorder", &t.options.border);
|
||||
let style_name = e.get("AppliedTableStyle").map(|r| self.table_style_ref(r)).unwrap_or_else(|| designcraft_doc::BASIC_TABLE.into());
|
||||
let style = self.styles.resolve_table_style(&style_name);
|
||||
style.apply_to(&mut t, &self.styles.cell);
|
||||
// IDML distinguishes an explicit AppliedCellStyle from the effective region style.
|
||||
for cell in &mut t.cells {
|
||||
cell.style.clear();
|
||||
}
|
||||
// Table-local options override the named table style. Cell styles are already
|
||||
// materialized independently, so their edges retain their higher precedence.
|
||||
let mut borders = std::array::from_fn::<_, 4, _>(|i| t.options.border_for(i).clone());
|
||||
for (side, border) in ["TopBorder", "LeftBorder", "BottomBorder", "RightBorder"].iter().zip(&mut borders) {
|
||||
self.cell_stroke_attrs(e, side).apply_to(border);
|
||||
}
|
||||
t.options.border = borders[0].clone();
|
||||
t.options.borders = borders.map(Some);
|
||||
t.options.space_before = e.num("SpaceBefore").unwrap_or(t.options.space_before);
|
||||
t.options.space_after = e.num("SpaceAfter").unwrap_or(t.options.space_after);
|
||||
t.options.repeat_header = e.get("HeaderBehavior") != Some("RepeatOnce");
|
||||
t.options.repeat_footer = e.get("FooterBehavior") != Some("RepeatOnce");
|
||||
for kind in ["Row", "Column"] {
|
||||
let Some(first) = e.num(&format!("Start{kind}FillCount")).filter(|n| *n > 0.0) else { continue };
|
||||
let alt = AltFills {
|
||||
first: first as u32,
|
||||
first_color: e.get(&format!("Start{kind}FillColor")).map(|c| self.swatch_ref(c)).unwrap_or_else(|| swatch::NONE.into()),
|
||||
first_tint: tint(e.num(&format!("Start{kind}FillTint"))).unwrap_or(1.0),
|
||||
next: e.num(&format!("End{kind}FillCount")).unwrap_or(1.0) as u32,
|
||||
next_color: e.get(&format!("End{kind}FillColor")).map(|c| self.swatch_ref(c)).unwrap_or_else(|| swatch::NONE.into()),
|
||||
next_tint: tint(e.num(&format!("End{kind}FillTint"))).unwrap_or(1.0),
|
||||
skip_first: e.num(&format!("SkipFirstAlternatingFill{kind}s")).unwrap_or(0.0) as u32,
|
||||
skip_last: e.num(&format!("SkipLastAlternatingFill{kind}s")).unwrap_or(0.0) as u32,
|
||||
};
|
||||
if kind == "Row" {
|
||||
t.options.alt_rows = Some(alt);
|
||||
} else {
|
||||
t.options.alt_cols = Some(alt);
|
||||
}
|
||||
}
|
||||
t.style = e
|
||||
.get("AppliedTableStyle")
|
||||
.map(|r| names::style_name_in(names::TABLE_BUILTINS, &unescape_id(r.trim_start_matches("TableStyle/"))))
|
||||
.filter(|n| n != designcraft_doc::BASIC_TABLE && n != "[No table style]")
|
||||
.unwrap_or_default();
|
||||
// A local count may enable a pattern disabled by its parent style. Merge before
|
||||
// materialization so the parent's colors/tints are still available in that case.
|
||||
let mut row_fills = style.row_fills.clone();
|
||||
row_fills.merge(&self.alt_fill_attrs(e, "Row"));
|
||||
row_fills.apply_to(&mut t.options.alt_rows);
|
||||
let mut column_fills = style.column_fills.clone();
|
||||
column_fills.merge(&self.alt_fill_attrs(e, "Column"));
|
||||
column_fills.apply_to(&mut t.options.alt_cols);
|
||||
let mut row_strokes = style.row_strokes.clone();
|
||||
row_strokes.merge(&self.alt_stroke_attrs(e, "Row"));
|
||||
row_strokes.apply_to(&mut t.options.row_strokes);
|
||||
let mut column_strokes = style.column_strokes.clone();
|
||||
column_strokes.merge(&self.alt_stroke_attrs(e, "Column"));
|
||||
column_strokes.apply_to(&mut t.options.column_strokes);
|
||||
let mut regions = Vec::new();
|
||||
for ce in e.find_all("Cell") {
|
||||
let Some((c, r)) =
|
||||
|
|
@ -1544,36 +1600,39 @@ impl<'r> Importer<'r> {
|
|||
if r >= nr || c >= nc {
|
||||
continue;
|
||||
}
|
||||
let rs = ce.num("RowSpan").unwrap_or(1.0).max(1.0) as usize;
|
||||
let cs = ce.num("ColumnSpan").unwrap_or(1.0).max(1.0) as usize;
|
||||
let text = self.story(StoryId(0), ce);
|
||||
let mut cell = Cell { text, ..Default::default() };
|
||||
cell.style = ce
|
||||
.get("AppliedCellStyle")
|
||||
.map(|r| names::style_name_in(names::CELL_BUILTINS, &unescape_id(r.trim_start_matches("CellStyle/"))))
|
||||
.filter(|n| n != designcraft_doc::NO_CELL_STYLE)
|
||||
.unwrap_or_default();
|
||||
let rs = (ce.num("RowSpan").unwrap_or(1.0).max(1.0) as usize).min(nr - r);
|
||||
let cs = (ce.num("ColumnSpan").unwrap_or(1.0).max(1.0) as usize).min(nc - c);
|
||||
let mut cell = t.cell(r, c).cloned().unwrap_or_default();
|
||||
let cell_style = ce.get("AppliedCellStyle").map(|r| self.cell_style_ref(r)).unwrap_or_else(|| designcraft_doc::NO_CELL_STYLE.into());
|
||||
if cell_style != designcraft_doc::NO_CELL_STYLE {
|
||||
designcraft_doc::resolve_cell_style(&self.styles.cell, &cell_style).apply_to(&mut cell);
|
||||
}
|
||||
cell.style = if cell_style == designcraft_doc::NO_CELL_STYLE { String::new() } else { cell_style };
|
||||
let default_para = cell.text.paras.first().cloned().unwrap_or_default();
|
||||
cell.text = self.story_with_para(StoryId(0), ce, default_para);
|
||||
if let Some(fc) = ce.get("FillColor") {
|
||||
cell.fill = self.swatch_ref(fc);
|
||||
}
|
||||
cell.fill_tint = tint(ce.num("FillTint")).unwrap_or(1.0);
|
||||
if let Some(tint) = tint(ce.num("FillTint")) {
|
||||
cell.fill_tint = tint;
|
||||
}
|
||||
for (i, k) in ["TopInset", "LeftInset", "BottomInset", "RightInset"].iter().enumerate() {
|
||||
if let Some(v) = ce.num(k) {
|
||||
cell.insets[i] = v.max(0.0);
|
||||
}
|
||||
}
|
||||
cell.vj = match ce.get("VerticalJustification") {
|
||||
Some("CenterAlign") => VerticalJustification::Center,
|
||||
Some("BottomAlign") => VerticalJustification::Bottom,
|
||||
Some("JustifyAlign") => VerticalJustification::Justify,
|
||||
_ => VerticalJustification::Top,
|
||||
};
|
||||
if let Some(vj) = ce.get("VerticalJustification") {
|
||||
cell.vj = names::vj_in(vj);
|
||||
}
|
||||
cell.rotation = ce.num("RotationAngle").unwrap_or(cell.rotation);
|
||||
for (i, side) in ["TopEdge", "LeftEdge", "BottomEdge", "RightEdge"].iter().enumerate() {
|
||||
let base = cell.strokes[i].clone();
|
||||
cell.strokes[i] = stroke(self, ce, side, &base);
|
||||
// A positive edge priority records a local cell override, including an
|
||||
// explicit None stroke. Keep it when this edge meets the table perimeter.
|
||||
cell.border_overrides[i] = ce.num(&format!("{side}StrokePriority")).is_some_and(|priority| priority > 0.0);
|
||||
let attrs = self.cell_stroke_attrs(ce, side);
|
||||
attrs.apply_to(&mut cell.strokes[i]);
|
||||
cell.stroke_defined[i] |= !attrs.is_empty();
|
||||
let priority = ce.prop(&format!("{side}StrokePriority")).and_then(|v| v.trim().parse::<i32>().ok()).unwrap_or(0);
|
||||
cell.stroke_priorities[i] = priority;
|
||||
// Preserve style-defined edges and #102's positive-priority local overrides.
|
||||
cell.border_overrides[i] |= priority > 0;
|
||||
}
|
||||
if let Some(slot) = t.cell_mut(r, c) {
|
||||
*slot = cell;
|
||||
|
|
@ -1593,7 +1652,7 @@ impl<'r> Importer<'r> {
|
|||
match n {
|
||||
Node::El(c) => match c.local() {
|
||||
"ParagraphStyleRange" => {
|
||||
let style = c.get("AppliedParagraphStyle").map(|r| self.para_style_ref(r)).unwrap_or_else(|| st::BASIC_PARAGRAPH.into());
|
||||
let style = c.get("AppliedParagraphStyle").map(|r| self.para_style_ref(r)).unwrap_or_else(|| pf.style.clone());
|
||||
let para = self.para_attrs(c);
|
||||
let chars = self.char_attrs(c);
|
||||
let npf = ParaFormat { style, para, chars: CharAttrs::default(), table: None };
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ pub fn unescape_id(s: &str) -> String {
|
|||
pub const PARA_BUILTINS: &[(&str, &str)] = &[("[No Paragraph Style]", "$ID/[No paragraph style]"), ("[Basic Paragraph]", "$ID/NormalParagraphStyle")];
|
||||
pub const CHAR_BUILTINS: &[(&str, &str)] = &[("[None]", "$ID/[No character style]")];
|
||||
pub const CELL_BUILTINS: &[(&str, &str)] = &[("[None]", "$ID/[None]")];
|
||||
pub const TABLE_BUILTINS: &[(&str, &str)] = &[("[Basic Table]", "$ID/[Basic Table]")];
|
||||
pub const TABLE_BUILTINS: &[(&str, &str)] = &[("[Basic Table]", "$ID/[Basic Table]"), ("[No table style]", "$ID/[No table style]")];
|
||||
pub const OBJECT_BUILTINS: &[(&str, &str)] =
|
||||
&[("[None]", "$ID/[None]"), ("[Basic Graphics Frame]", "$ID/[Normal Graphics Frame]"), ("[Basic Text Frame]", "$ID/[Normal Text Frame]")];
|
||||
|
||||
|
|
|
|||
|
|
@ -928,6 +928,336 @@ fn arabic_story_and_table_directions_survive_idml_export() {
|
|||
assert_eq!(st.direction, designcraft_doc::TextDirection::RightToLeft);
|
||||
assert_eq!(st.tables.values().next().unwrap().options.direction, designcraft_doc::TextDirection::RightToLeft);
|
||||
}
|
||||
/// Fully synthetic style-only table, including sparse based-on attributes and local overrides.
|
||||
const CASCADE_STYLES: &str = r#"<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<RootCellStyleGroup>
|
||||
<CellStyle Self="CellStyle/$ID/[None]" Name="$ID/[None]"/>
|
||||
<CellStyle Self="cs-base" Name="Base" FillColor="Color/Brand" FillTint="25"
|
||||
TopInset="2" LeftInset="3" BottomInset="4" RightInset="5" VerticalJustification="CenterAlign"
|
||||
TopEdgeStrokeWeight="2" TopEdgeStrokeColor="Color/Brand" TopEdgeStrokeTint="40" TopEdgeStrokeType="StrokeStyle/$ID/Dashed"
|
||||
LeftEdgeStrokeWeight="3" BottomEdgeStrokeWeight="4" RightEdgeStrokeWeight="5"
|
||||
AppliedParagraphStyle="ParagraphStyle/CellParagraph"/>
|
||||
<CellStyle Self="cs-body" Name="Body" FillTint="50"><Properties><BasedOn type="object">cs-base</BasedOn></Properties></CellStyle>
|
||||
<CellStyle Self="cs-left" Name="Left" FillColor="Color/Black" LeftEdgeStrokeWeight="0"><Properties><BasedOn type="object">cs-body</BasedOn></Properties></CellStyle>
|
||||
<CellStyle Self="cs-header" Name="Header" FillColor="Color/Paper" BottomEdgeStrokeColor="Color/Brand" BottomEdgeStrokeWeight="6">
|
||||
<Properties><BasedOn type="object">cs-body</BasedOn></Properties>
|
||||
</CellStyle>
|
||||
<CellStyle Self="cs-explicit" Name="Explicit" FillColor="Color/Brand" FillTint="75" TopInset="0"
|
||||
TopEdgeStrokeColor="Swatch/None" RightEdgeStrokeTint="30"/>
|
||||
</RootCellStyleGroup>
|
||||
<RootParagraphStyleGroup><ParagraphStyle Self="ParagraphStyle/CellParagraph" Name="CellParagraph" PointSize="9"/></RootParagraphStyleGroup>
|
||||
<RootTableStyleGroup>
|
||||
<TableStyle Self="TableStyle/$ID/[No table style]" Name="$ID/[No table style]"
|
||||
TopBorderStrokeWeight="7" LeftBorderStrokeWeight="8" BottomBorderStrokeWeight="9" RightBorderStrokeWeight="10"
|
||||
StartRowFillCount="0" StartRowFillColor="Color/Brand" StartRowFillTint="20" EndRowFillCount="0" EndRowFillColor="Swatch/None"
|
||||
HeaderRegionSameAsBodyRegion="true" FooterRegionSameAsBodyRegion="true"
|
||||
LeftColumnRegionSameAsBodyRegion="true" RightColumnRegionSameAsBodyRegion="true"/>
|
||||
<TableStyle Self="ts-base" Name="BaseTable" BodyRegionCellStyle="cs-body" HeaderRegionCellStyle="cs-header"
|
||||
LeftColumnRegionCellStyle="cs-left" HeaderRegionSameAsBodyRegion="false" LeftColumnRegionSameAsBodyRegion="false">
|
||||
<Properties><BasedOn type="string">$ID/[No table style]</BasedOn></Properties>
|
||||
</TableStyle>
|
||||
<TableStyle Self="ts-child" Name="Table" SpaceBefore="12" StartRowFillCount="1" EndRowFillCount="1">
|
||||
<Properties><BasedOn type="object">ts-base</BasedOn></Properties>
|
||||
</TableStyle>
|
||||
<TableStyle Self="TableStyle/$ID/[Basic Table]" Name="$ID/[Basic Table]" SpaceAfter="17">
|
||||
<Properties><BasedOn type="object">ts-child</BasedOn></Properties>
|
||||
</TableStyle>
|
||||
</RootTableStyleGroup></idPkg:Styles>"#;
|
||||
|
||||
const CASCADE_STORY: &str = r#"<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<Story Self="s1"><ParagraphStyleRange><CharacterStyleRange><Table AppliedTableStyle="ts-child" HeaderRowCount="1" SpaceBefore="19" RightBorderStrokeColor="Color/Brand">
|
||||
<Row Name="0" MinimumHeight="24"/><Row Name="1" MinimumHeight="24"/>
|
||||
<Column Name="0" SingleColumnWidth="60"/><Column Name="1" SingleColumnWidth="60"/><Column Name="2" SingleColumnWidth="60"/>
|
||||
<Cell Name="0:0" AppliedCellStyle="CellStyle/$ID/[None]"><ParagraphStyleRange><CharacterStyleRange><Content>Header left</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
<Cell Name="1:0" AppliedCellStyle="cs-explicit"><ParagraphStyleRange><CharacterStyleRange><Content>Explicit</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
<Cell Name="2:0" AppliedCellStyle="cs-explicit" FillColor="Swatch/None" FillTint="0" LeftInset="0" VerticalJustification="BottomAlign"
|
||||
TopEdgeStrokeWeight="0" RightEdgeStrokeColor="Swatch/None" RightEdgeStrokePriority="7">
|
||||
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle"><CharacterStyleRange><Content>Local</Content></CharacterStyleRange></ParagraphStyleRange>
|
||||
</Cell>
|
||||
<Cell Name="0:1" AppliedCellStyle="CellStyle/$ID/[None]"><ParagraphStyleRange><CharacterStyleRange><Content>Left</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
<Cell Name="1:1"><ParagraphStyleRange><CharacterStyleRange><Content>Body</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
<Cell Name="2:1"><ParagraphStyleRange><CharacterStyleRange><Content>Body right</Content></CharacterStyleRange></ParagraphStyleRange></Cell>
|
||||
</Table></CharacterStyleRange></ParagraphStyleRange></Story></idPkg:Story>"#;
|
||||
|
||||
fn fixture_with_table_styles(styles: &str, story: &str) -> Vec<u8> {
|
||||
zip_files(&[
|
||||
("designmap.xml", DESIGNMAP),
|
||||
("Resources/Graphic.xml", GRAPHIC),
|
||||
("Resources/Styles.xml", styles),
|
||||
("Resources/Preferences.xml", PREFS),
|
||||
("MasterSpreads/MasterSpread_m1.xml", MASTER),
|
||||
("Spreads/Spread_sp1.xml", SPREAD),
|
||||
("Stories/Story_s1.xml", story),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_style_cascade_preserves_sparse_edges_and_local_overrides() {
|
||||
let d = import_idml(&fixture_with_table_styles(CASCADE_STYLES, CASCADE_STORY)).unwrap();
|
||||
d.check().unwrap();
|
||||
let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!((table.nrows(), table.ncols()), (2, 3));
|
||||
assert_eq!(table.style, "Table");
|
||||
assert_eq!(table.options.space_before, 19.0);
|
||||
assert_eq!(std::array::from_fn::<_, 4, _>(|i| table.options.border_for(i).weight), [7.0, 8.0, 9.0, 10.0]);
|
||||
assert_eq!(table.options.border_for(3).color, "Brand");
|
||||
let fills = table.options.alt_rows.as_ref().unwrap();
|
||||
assert_eq!((fills.first, fills.next, fills.first_tint), (1, 1, 0.2));
|
||||
assert_eq!(fills.first_color, "Brand");
|
||||
let header = table.cell(0, 0).unwrap();
|
||||
assert_eq!(header.fill, designcraft_color::swatch::PAPER, "header takes precedence over left region");
|
||||
assert!(header.style.is_empty(), "a table region does not become AppliedCellStyle");
|
||||
assert_eq!(header.insets, [2.0, 3.0, 4.0, 5.0]);
|
||||
assert_eq!(header.vj, designcraft_doc::VerticalJustification::Center);
|
||||
assert_eq!(header.text.paras[0].style, "CellParagraph");
|
||||
assert_eq!(header.strokes[0].weight, 2.0);
|
||||
assert_eq!(header.strokes[0].tint, 0.4);
|
||||
assert_ne!(header.strokes[0].kind, designcraft_doc::StrokeType::Solid);
|
||||
assert_eq!(header.strokes[2].weight, 6.0);
|
||||
assert_eq!(header.border_overrides, [true; 4]);
|
||||
assert_eq!(header.stroke_defined, [true; 4]);
|
||||
assert_eq!(header.stroke_priorities, [0; 4]);
|
||||
let explicit = table.cell(0, 1).unwrap();
|
||||
assert_eq!(explicit.fill, "Brand");
|
||||
assert_eq!(explicit.fill_tint, 0.75);
|
||||
assert_eq!(explicit.insets, [0.0, 3.0, 4.0, 5.0]);
|
||||
assert_eq!(explicit.strokes[0].weight, 2.0, "color-only override inherits weight");
|
||||
assert_eq!(explicit.strokes[0].color, designcraft_color::swatch::NONE);
|
||||
assert_eq!(explicit.strokes[3].weight, 5.0);
|
||||
assert_eq!(explicit.strokes[3].tint, 0.3);
|
||||
let local = table.cell(0, 2).unwrap();
|
||||
assert_eq!(local.fill, designcraft_color::swatch::NONE);
|
||||
assert_eq!(local.fill_tint, 0.0);
|
||||
assert_eq!(local.insets[1], 0.0);
|
||||
assert_eq!(local.vj, designcraft_doc::VerticalJustification::Bottom);
|
||||
assert_eq!(local.strokes[0].weight, 0.0);
|
||||
assert_eq!(local.strokes[3].color, designcraft_color::swatch::NONE);
|
||||
assert_eq!(local.stroke_priorities, [0, 0, 0, 7]);
|
||||
assert_eq!(local.text.paras[0].style, st::BASIC_PARAGRAPH, "explicit paragraph style wins");
|
||||
assert_eq!(table.cell(1, 0).unwrap().fill, designcraft_color::swatch::BLACK);
|
||||
assert_eq!(table.cell(1, 0).unwrap().strokes[1].weight, 0.0);
|
||||
assert_eq!(table.cell(1, 1).unwrap().fill_tint, 0.5);
|
||||
assert_eq!(table.cell(1, 2).unwrap().fill, "Brand");
|
||||
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
let roundtrip = back.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
for (before, after) in table.cells.iter().zip(&roundtrip.cells) {
|
||||
assert_eq!(after.strokes, before.strokes);
|
||||
assert_eq!(after.border_overrides, before.border_overrides);
|
||||
assert_eq!(after.stroke_priorities, before.stroke_priorities);
|
||||
assert_eq!(after.style, before.style);
|
||||
assert_eq!(after.fill, before.fill);
|
||||
assert_eq!(after.insets, before.insets);
|
||||
}
|
||||
for i in 0..4 {
|
||||
assert_eq!(roundtrip.options.border_for(i), table.options.border_for(i));
|
||||
}
|
||||
let style = back.styles.cell.iter().find(|s| s.name == "Explicit").unwrap();
|
||||
assert_eq!(style.strokes[0].weight, None, "style export keeps absence distinct from zero");
|
||||
assert_eq!(style.strokes[0].color.as_deref(), Some(designcraft_color::swatch::NONE));
|
||||
assert_eq!(back.styles.table.iter().find(|s| s.name == "Table").unwrap().based_on.as_deref(), Some("BaseTable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_style_same_as_body_flags_and_editable_basic_table_are_inherited() {
|
||||
let styles = CASCADE_STYLES.replace(
|
||||
"Name=\"Table\" SpaceBefore",
|
||||
"Name=\"Table\" HeaderRegionSameAsBodyRegion=\"true\" LeftColumnRegionSameAsBodyRegion=\"true\" SpaceBefore",
|
||||
);
|
||||
let story = CASCADE_STORY.replace("AppliedTableStyle=\"ts-child\"", "AppliedTableStyle=\"TableStyle/$ID/[Basic Table]\"");
|
||||
let d = import_idml(&fixture_with_table_styles(&styles, &story)).unwrap();
|
||||
let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert!(table.style.is_empty());
|
||||
assert_eq!(table.options.space_after, 17.0);
|
||||
assert_eq!(table.cell(0, 0).unwrap().fill, "Brand");
|
||||
assert_eq!(table.cell(0, 0).unwrap().strokes[2].weight, 4.0);
|
||||
assert_eq!(table.cell(1, 0).unwrap().fill, "Brand");
|
||||
assert_eq!(table.cell(1, 0).unwrap().strokes[1].weight, 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_style_cycles_and_missing_bases_terminate_without_losing_nearest_values() {
|
||||
let styles = r#"<idPkg:Styles xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging">
|
||||
<RootCellStyleGroup>
|
||||
<CellStyle Self="CellStyle/A" Name="A" TopEdgeStrokeWeight="0"><Properties><BasedOn type="object">CellStyle/B</BasedOn></Properties></CellStyle>
|
||||
<CellStyle Self="CellStyle/B" Name="B" TopEdgeStrokeColor="Color/Brand"><Properties><BasedOn type="object">CellStyle/A</BasedOn></Properties></CellStyle>
|
||||
<CellStyle Self="CellStyle/Missing" Name="Missing" LeftInset="0"><Properties><BasedOn type="object">CellStyle/Unknown</BasedOn></Properties></CellStyle>
|
||||
</RootCellStyleGroup><RootTableStyleGroup>
|
||||
<TableStyle Self="ts-child" Name="A" BodyRegionCellStyle="CellStyle/A"><Properties><BasedOn type="object">TableStyle/B</BasedOn></Properties></TableStyle>
|
||||
<TableStyle Self="TableStyle/B" Name="B" SpaceAfter="20"><Properties><BasedOn type="object">ts-child</BasedOn></Properties></TableStyle>
|
||||
</RootTableStyleGroup></idPkg:Styles>"#;
|
||||
let d = import_idml(&fixture_with_table_styles(styles, CASCADE_STORY)).unwrap();
|
||||
let table = d.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(table.options.space_after, 20.0);
|
||||
assert_eq!(table.cell(1, 1).unwrap().strokes[0].weight, 0.0);
|
||||
assert_eq!(table.cell(1, 1).unwrap().strokes[0].color, "Brand");
|
||||
let missing = designcraft_doc::resolve_cell_style(&d.styles.cell, "Missing");
|
||||
assert_eq!(missing.inset_overrides[1], Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_local_alternating_fill_off_survives_named_style_roundtrip() {
|
||||
let styles = CASCADE_STYLES.replace(
|
||||
"Name=\"Table\" SpaceBefore",
|
||||
"Name=\"Table\" StartColumnFillCount=\"1\" EndColumnFillCount=\"1\" StartColumnFillColor=\"Color/Brand\" SpaceBefore",
|
||||
);
|
||||
let story = CASCADE_STORY.replace("HeaderRowCount=\"1\"", "HeaderRowCount=\"1\" StartRowFillCount=\"0\" StartColumnFillCount=\"0\"");
|
||||
let document = import_idml(&fixture_with_table_styles(&styles, &story)).unwrap();
|
||||
for document in [&document, &import_idml(&export_idml(&document)).unwrap()] {
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert!(table.options.alt_rows.is_none());
|
||||
assert!(table.options.alt_cols.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_local_fill_counts_enable_inherited_disabled_pattern_colors() {
|
||||
let story =
|
||||
CASCADE_STORY.replace("AppliedTableStyle=\"ts-child\"", "AppliedTableStyle=\"ts-base\" StartRowFillCount=\"1\" EndRowFillCount=\"1\"");
|
||||
let document = import_idml(&fixture_with_table_styles(CASCADE_STYLES, &story)).unwrap();
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
let fills = table.options.alt_rows.as_ref().unwrap();
|
||||
assert_eq!((fills.first, fills.next), (1, 1));
|
||||
assert_eq!(fills.first_color, "Brand");
|
||||
assert_eq!(fills.first_tint, 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_absent_and_explicit_default_edges_remain_distinct_on_idml_roundtrip() {
|
||||
let story = CASCADE_STORY
|
||||
.replace("AppliedTableStyle=\"ts-child\"", "AppliedTableStyle=\"Missing\"")
|
||||
.replace("Name=\"1:1\"", "Name=\"1:1\" TopEdgeStrokeWeight=\"1\" TopEdgeStrokePriority=\"0\"");
|
||||
let document = import_idml(&fixture_with_table_styles(STYLES, &story)).unwrap();
|
||||
for document in [&document, &import_idml(&export_idml(&document)).unwrap()] {
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(table.cell(1, 0).unwrap().stroke_defined, [false; 4]);
|
||||
assert_eq!(table.cell(1, 1).unwrap().stroke_defined, [true, false, false, false]);
|
||||
assert_eq!(table.cell(1, 1).unwrap().border_overrides, [false; 4]);
|
||||
assert_eq!(table.cell(1, 1).unwrap().stroke_priorities, [0; 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiline_table_and_cell_style_bases_resolve() {
|
||||
let styles = CASCADE_STYLES.replace(">cs-base</BasedOn>", ">\n cs-base \n</BasedOn>").replace(">ts-base</BasedOn>", ">\n ts-base \n</BasedOn>");
|
||||
let document = import_idml(&fixture_with_table_styles(&styles, CASCADE_STORY)).unwrap();
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(table.cell(1, 1).unwrap().strokes[0].weight, 2.0);
|
||||
assert_eq!(table.options.border_for(0).weight, 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_native_documents_export_both_builtin_table_styles_when_missing() {
|
||||
let story = CASCADE_STORY.replace("AppliedTableStyle=\"ts-child\"", "AppliedTableStyle=\"TableStyle/$ID/[Basic Table]\"");
|
||||
let mut document = import_idml(&fixture_with_story(&story)).unwrap();
|
||||
document.styles_mut().table.clear();
|
||||
let bytes = export_idml(&document);
|
||||
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap();
|
||||
let mut styles = String::new();
|
||||
std::io::Read::read_to_string(&mut zip.by_name("Resources/Styles.xml").unwrap(), &mut styles).unwrap();
|
||||
assert_eq!(styles.matches("Self=\"TableStyle/$ID/[Basic Table]\"").count(), 1);
|
||||
assert_eq!(styles.matches("Self=\"TableStyle/$ID/[No table style]\"").count(), 1);
|
||||
let back = import_idml(&bytes).unwrap();
|
||||
assert!(back.styles.table.iter().any(|s| s.name == designcraft_doc::BASIC_TABLE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_table_spans_are_clamped_before_arithmetic() {
|
||||
let story = CASCADE_STORY.replace("Name=\"1:1\"", "Name=\"1:1\" RowSpan=\"inf\" ColumnSpan=\"inf\"");
|
||||
let document = import_idml(&fixture_with_table_styles(CASCADE_STYLES, &story)).unwrap();
|
||||
document.check().unwrap();
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(table.cell(1, 1).unwrap().row_span, 1);
|
||||
assert_eq!(table.cell(1, 1).unwrap().col_span, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_idml_edge_priorities_roundtrip_without_clamping() {
|
||||
let story = CASCADE_STORY.replace(
|
||||
"Name=\"1:1\"",
|
||||
"Name=\"1:1\" TopEdgeStrokePriority=\"-5\" LeftEdgeStrokePriority=\"-2147483648\" RightEdgeStrokePriority=\"2147483647\"",
|
||||
);
|
||||
let document = import_idml(&fixture_with_table_styles(CASCADE_STYLES, &story)).unwrap();
|
||||
for document in [&document, &import_idml(&export_idml(&document)).unwrap()] {
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(table.cell(1, 1).unwrap().stroke_priorities, [-5, i32::MIN, 0, i32::MAX]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternating_table_strokes_inherit_and_roundtrip_without_flattening_cell_edges() {
|
||||
let styles = CASCADE_STYLES.replace(
|
||||
"Name=\"BaseTable\"",
|
||||
"Name=\"BaseTable\" StartRowStrokeCount=\"1\" EndRowStrokeCount=\"1\" StartRowStrokeWeight=\"0\" EndRowStrokeWeight=\"0\" StartRowStrokeColor=\"Color/Paper\" EndRowStrokeColor=\"Color/Brand\" StartColumnStrokeCount=\"2\" EndColumnStrokeCount=\"1\" StartColumnStrokeWeight=\"1\" EndColumnStrokeWeight=\"2\" EndColumnLineStyle=\"StrokeStyle/$ID/Dotted\" SkipFirstAlternatingStrokeColumns=\"1\"",
|
||||
).replace("Name=\"Table\"", "Name=\"Table\" StartRowStrokeTint=\"25\"");
|
||||
let story = CASCADE_STORY.replace("HeaderRowCount=\"1\"", "HeaderRowCount=\"1\" EndRowStrokeColor=\"Swatch/None\"");
|
||||
let document = import_idml(&fixture_with_table_styles(&styles, &story)).unwrap();
|
||||
let before = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
let row = before.options.row_strokes.as_ref().unwrap();
|
||||
assert_eq!((row.first, row.next), (1, 1));
|
||||
assert_eq!(row.first_stroke.tint, 0.25);
|
||||
assert_eq!(row.first_stroke.color, designcraft_color::swatch::PAPER);
|
||||
assert_eq!(row.next_stroke.color, designcraft_color::swatch::NONE);
|
||||
assert!(!row.uniform_stroke().unwrap().is_visible(), "zero patterns are uniform even when their colors differ");
|
||||
let column = before.options.column_strokes.as_ref().unwrap();
|
||||
assert_eq!((column.first, column.next, column.skip_first), (2, 1, 1));
|
||||
assert_eq!(column.next_stroke.kind, designcraft_doc::StrokeType::Dotted);
|
||||
assert!(column.uniform_stroke().is_none(), "nonuniform/skipped patterns are preserved without guessing their phase");
|
||||
assert_eq!(before.cell(0, 0).unwrap().strokes[2].weight, 6.0, "cell styles remain higher-precedence edges");
|
||||
let back = import_idml(&export_idml(&document)).unwrap();
|
||||
let after = back.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
assert_eq!(after.options.row_strokes, before.options.row_strokes);
|
||||
assert_eq!(after.options.column_strokes, before.options.column_strokes);
|
||||
let style = back.styles.table.iter().find(|s| s.name == "Table").unwrap();
|
||||
assert_eq!(style.row_strokes.first_stroke.weight, None);
|
||||
assert_eq!(style.row_strokes.first_stroke.tint, Some(0.25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternating_table_strokes_local_off_overrides_named_style_on_roundtrip() {
|
||||
let styles = CASCADE_STYLES.replace(
|
||||
"Name=\"Table\"",
|
||||
"Name=\"Table\" StartRowStrokeCount=\"1\" EndRowStrokeCount=\"1\" StartColumnStrokeCount=\"1\" EndColumnStrokeCount=\"1\"",
|
||||
);
|
||||
let story = CASCADE_STORY.replace(
|
||||
"HeaderRowCount=\"1\"",
|
||||
"HeaderRowCount=\"1\" StartRowStrokeCount=\"0\" EndRowStrokeCount=\"0\" StartRowStrokeWeight=\"4\" EndRowStrokeColor=\"Color/Brand\" StartColumnStrokeCount=\"0\" EndColumnStrokeCount=\"0\" SkipFirstAlternatingStrokeColumns=\"3\" EndColumnLineStyle=\"StrokeStyle/$ID/Dotted\"",
|
||||
);
|
||||
let document = import_idml(&fixture_with_table_styles(&styles, &story)).unwrap();
|
||||
for document in [&document, &import_idml(&export_idml(&document)).unwrap()] {
|
||||
let table = document.stories.values().flat_map(|s| s.tables.values()).next().unwrap();
|
||||
let row = table.options.row_strokes.as_ref().unwrap();
|
||||
let column = table.options.column_strokes.as_ref().unwrap();
|
||||
assert_eq!((row.first, row.next, column.first, column.next), (0, 0, 0, 0));
|
||||
assert!(row.uniform_stroke().is_none());
|
||||
assert!(column.uniform_stroke().is_none());
|
||||
assert_eq!(row.first_stroke.weight, 4.0);
|
||||
assert_eq!(row.next_stroke.color, "Brand");
|
||||
assert_eq!(column.skip_first, 3);
|
||||
assert_eq!(column.next_stroke.kind, designcraft_doc::StrokeType::Dotted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_uniform_native_style_fields_keep_legacy_identity_on_idml_roundtrip() {
|
||||
let mut document = import_idml(&fixture()).unwrap();
|
||||
let stroke = designcraft_doc::CellStroke { weight: 3.0, tint: 0.5, ..Default::default() };
|
||||
document.styles_mut().cell.push(designcraft_doc::CellStyle {
|
||||
name: "Uniform".into(),
|
||||
insets: Some([5.0; 4]),
|
||||
stroke: Some(stroke.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
document.styles_mut().table.push(designcraft_doc::TableStyle { name: "Uniform".into(), border: Some(stroke.clone()), ..Default::default() });
|
||||
let back = import_idml(&export_idml(&document)).unwrap();
|
||||
let cell = back.styles.cell.iter().find(|s| s.name == "Uniform").unwrap();
|
||||
assert_eq!(cell.insets, Some([5.0; 4]));
|
||||
assert_eq!(cell.stroke.as_ref(), Some(&stroke));
|
||||
assert!(cell.strokes.iter().all(designcraft_doc::CellStrokeAttrs::is_empty));
|
||||
let table = back.styles.table.iter().find(|s| s.name == "Uniform").unwrap();
|
||||
assert_eq!(table.border.as_ref(), Some(&stroke));
|
||||
assert!(table.borders.iter().all(designcraft_doc::CellStrokeAttrs::is_empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_column_rules_from_frames_and_object_styles() {
|
||||
|
|
|
|||
|
|
@ -171,11 +171,10 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
|||
match classify(&d.name) {
|
||||
// Unregistered deps are reported on their own entry.
|
||||
None => {}
|
||||
Some(Class::Testkit) => {
|
||||
if d.kind != DepKind::Dev {
|
||||
out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() });
|
||||
}
|
||||
Some(Class::Testkit) if d.kind != DepKind::Dev => {
|
||||
out.push(Violation::TestkitAsNormalDep { krate: c.name.clone() });
|
||||
}
|
||||
Some(Class::Testkit) => {}
|
||||
Some(dc) => {
|
||||
let to = dc.layer().unwrap_or(u8::MAX);
|
||||
if to >= layer && !(to == layer && intra_layer_allowed(&c.name, &d.name)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue