mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-03 07:01:09 -06:00
Merge #83 update: drop cap fixes
Drop cap style through IDML as a Dropcap nested style, spaces after the cap, one-line caps, nested styles after the cap, own nested-style lists clearing an inherited drop cap style (cells and notes too), cleared styles round-tripping, tabs in right-to-left drop caps. Conflict: ROADMAP.md: next's INDD, Interface language and Show CJK Features lines (the PR's main-merge resolution, already on next) Conflict: crates/compose/src/lib.rs: next's warichu placement followed by the trimmed trailing spaces Conflict: crates/compose/src/tests.rs: next's side; the PR's side repeats helpers and tests next already has Conflict: crates/idml/src/import.rs: next's range_format Conflict: crates/idml/src/tests.rs: next's side, with the PR's drop_caps_import_and_round_trip and its new drop_cap_fixture and nested-style clearing tests in place of next's older drop cap test
This commit is contained in:
+1
-1
@@ -99,7 +99,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Appearance of Black complete: Printing/Exporting rich black for RGB output (PNG), Overprint [Black] Swatch at 100% (document, IDML OverprintBlack).
|
||||
- Hyperlinks and Bookmarks panels (Window ▸ Interactive): new from the selection, edit (`hyperlink.edit`), go to source (`hyperlink.goToSource`), rename bookmarks (`bookmark.rename`), go to page, delete.
|
||||
- Nested line styles (`nestedLineStyles` paragraph attribute, Paragraph Style Options › Drop Caps and Nested Styles, IDML AllNestedLineStyles): the first lines restyle until the line breaks settle.
|
||||
- Drop caps (`type.para {dropCapLines, dropCapChars, dropCapStyle, dropCapAlignLeft, dropCapScaleDescenders}`, Paragraph panel, Paragraph Style Options › Drop Caps and Nested Styles; IDML DropCapLines/DropCapCharacters/DropcapDetail, DropCapStyle on import): the first characters (code points, as InDesign counts them, never splitting a letter from its combining marks; up to 150) are enlarged from the first line's cap height down to the baseline of line N (up to 25), the lines beside them are indented by their advance (on the right in right-to-left paragraphs), baselines don't move; Align Left Edge (on by default) moves the drop cap left by its first letter's side bearing so its ink starts at the indent; Scale for Descenders is kept but doesn't change the layout yet; a drop cap character style, and nested styles through the drop cap (Dropcap delimiter). Drawn on screen and in PDF (text in order), with caret, hit testing and selection over the enlarged letters. Vertical text sets no drop caps.
|
||||
- Drop caps (`type.para {dropCapLines, dropCapChars, dropCapStyle, dropCapAlignLeft, dropCapScaleDescenders}`, Paragraph panel, Paragraph Style Options › Drop Caps and Nested Styles; IDML DropCapLines/DropCapCharacters/DropcapDetail, the drop cap style as the leading "through 1 Dropcap" nested style): the first characters (code points, as InDesign counts them, never splitting a letter from its combining marks; up to 150) are enlarged from the first line's cap height down N − 1 leadings of the text after them (up to 25 lines; grid steps on a baseline grid), the lines beside them are indented by their advance (on the right in right-to-left paragraphs), spaces right after them are set at their size without widening the indent, baselines don't move; a one-line drop cap keeps a smaller size of its own and sets its tabs at the paragraph's tab stops; Align Left Edge (on by default) moves the drop cap left by its first letter's side bearing so its ink starts at the indent; Scale for Descenders is kept but doesn't change the layout yet; a drop cap character style, and nested styles through the drop cap (Dropcap delimiter). Drawn on screen and in PDF (text in order), with caret, hit testing and selection over the enlarged letters. Vertical text sets no drop caps.
|
||||
- User dictionary hyphenation exceptions (`hyphenation.addException/removeException/list`, Edit ▸ Spelling ▸ User Dictionary): `ex~am~ple` breaks only at `~`, a plain word never hyphenates.
|
||||
- Frame Fitting Options (`object.fittingOptions`, Object ▸ Fitting ▸ Frame Fitting Options…): Auto-Fit refits on resize, fitting, Align From reference point, crop amounts; IDML FrameFittingOption.
|
||||
- Paragraph borders (per-side weights, colour, tint, offsets) and shading offsets, drawn per column part of split paragraphs (also when overset); Paragraph panel controls; IDML ParagraphBorder*/ParagraphShading*Offset.
|
||||
|
||||
+34
-33
@@ -702,6 +702,8 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
let drop_grid = cur_frame.and_then(|f| f.grid).filter(|_| pp.grid_align == GridAlign::AllLines).map(|g| g.1);
|
||||
// Align Left Edge (left-to-right paragraphs).
|
||||
let drop_align = pp.drop_cap_align_left && pp.direction == designcraft_doc::TextDirection::LeftToRight;
|
||||
// Tab stops for the drop cap, with the start-side indent (the right one, right to left).
|
||||
let drop_tabs = (&pp.tabs[..], if pp.direction == designcraft_doc::TextDirection::RightToLeft { pp.right_indent } else { pp.left_indent });
|
||||
if !pp.nested_line_styles.is_empty() && cur.fi < frames.len() {
|
||||
// Nested line styles: find where the first lines end in this column, restyle them, and
|
||||
// look again (the style changes the widths) until the lines settle.
|
||||
@@ -713,7 +715,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
for _ in 0..3 {
|
||||
// The same drop cap, spacing, hyphenation and breaker as the layout below.
|
||||
let mut gl = sp.glyphs.clone();
|
||||
let dc = split_drop_cap(db, &mut gl, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align);
|
||||
let dc = split_drop_cap(db, &mut gl, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align, drop_tabs);
|
||||
let width = |j: usize| {
|
||||
let ind = match &dc {
|
||||
Some(dc) if j < dc.lines => dc.indent(),
|
||||
@@ -782,7 +784,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
let rule_color = |r: &designcraft_doc::Rule, text: &(String, f32)| {
|
||||
if r.color == designcraft_doc::TEXT_COLOR { text.clone() } else { (r.color.clone(), r.tint) }
|
||||
};
|
||||
let drop_cap = split_drop_cap(db, &mut sp.glyphs, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align);
|
||||
let drop_cap = split_drop_cap(db, &mut sp.glyphs, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align, drop_tabs);
|
||||
// List labels take the default super/subscript settings.
|
||||
let label_env = shape::TypeEnv { adv: Default::default(), ..env };
|
||||
let list_label = match pp.list_type {
|
||||
@@ -943,9 +945,6 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
let has_tabs = glyphs.iter().any(|g| g.ch == '\t');
|
||||
let mut first_line_rect: Option<(usize, f64, f64, f64)> = None; // frame, baseline, ascent, x-span
|
||||
let rtl = pp.direction == designcraft_doc::TextDirection::RightToLeft;
|
||||
// The drop cap's line (frame, index) and the baseline it sits on, once that line is set.
|
||||
let mut drop_line: Option<(usize, usize)> = None;
|
||||
let mut drop_baseline: Option<f64> = None;
|
||||
loop {
|
||||
if cur.fi >= frames.len() {
|
||||
if let Some(j) = trial_failed(&mut trial, &mut limits, balance_runs < balance_budget) {
|
||||
@@ -1212,15 +1211,6 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
keep_violation: false,
|
||||
drop_cap: drop_box,
|
||||
});
|
||||
if drop_box.is_some() {
|
||||
drop_line = Some((cur.fi, ft.lines.len() - 1));
|
||||
}
|
||||
if let Some(dc) = &drop_cap
|
||||
&& line_no + 1 == dc.lines
|
||||
&& col_first_line == 0
|
||||
{
|
||||
drop_baseline = Some(baseline);
|
||||
}
|
||||
for k in line_notes {
|
||||
notes.place(doc, k, cur.fi, cur.col, col_w, f, opts);
|
||||
}
|
||||
@@ -1268,19 +1258,6 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The drop cap sits on its last line's baseline as set (it was placed at the leading's).
|
||||
if let (Some((fi, li)), Some(b), Some(n)) = (drop_line, drop_baseline, drop_cap.as_ref().map(|dc| dc.glyphs.len()))
|
||||
&& let Some(l) = out.frames.get_mut(fi).and_then(|f| f.lines.get_mut(li))
|
||||
&& let Some(dc) = l.drop_cap.as_mut()
|
||||
{
|
||||
let d = b - dc.baseline;
|
||||
dc.baseline = b;
|
||||
dc.rect.y1 += d;
|
||||
// Its glyphs start the line.
|
||||
for g in l.glyphs.iter_mut().take(n) {
|
||||
g.y += d;
|
||||
}
|
||||
}
|
||||
info.push(ParaInfo {
|
||||
start: start_at,
|
||||
at_top,
|
||||
@@ -3028,15 +3005,19 @@ struct DropCap {
|
||||
/// How far its origin sits left of the indent: its first letter's left side bearing with
|
||||
/// Align Left Edge, else 0.
|
||||
lsb: f64,
|
||||
/// Its baseline below the first line's, at the paragraph's leading.
|
||||
/// Its baseline below the first line's: `lines − 1` leadings of the text after it (grid steps
|
||||
/// on a baseline grid), wherever line `lines` is set.
|
||||
drop: f64,
|
||||
}
|
||||
|
||||
/// Take the drop cap (the glyphs before byte `end`) off the front of a paragraph's glyphs and scale
|
||||
/// it so its cap height reaches from the first line's cap height down to line `lines`' baseline,
|
||||
/// measured in the leading and cap height of the text after it.
|
||||
/// On a baseline grid of increment `grid`, lines are that many grid steps apart. With
|
||||
/// `align_left` its ink starts at the indent (see [`DropCap::lsb`]).
|
||||
/// measured in the leading and cap height of the text after it. A one-line drop cap keeps its own
|
||||
/// size when that is smaller. On a baseline grid of increment `grid`, lines are that many grid
|
||||
/// steps apart. With `align_left` its ink starts at the indent (see [`DropCap::lsb`]). Tabs inside
|
||||
/// it advance to the stops of `tabs` (stops and the start-side indent; in right-to-left paragraphs
|
||||
/// both measured from the column's right edge). Spaces right after it join it at its size without
|
||||
/// widening it.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn split_drop_cap(
|
||||
db: &ScopedFonts<'_>,
|
||||
@@ -3047,6 +3028,7 @@ fn split_drop_cap(
|
||||
base_cap: f64,
|
||||
grid: Option<f64>,
|
||||
align_left: bool,
|
||||
tabs: (&[designcraft_doc::TabStop], f64),
|
||||
) -> Option<DropCap> {
|
||||
let n = glyphs.iter().take_while(|g| g.byte < end).count();
|
||||
if n == 0 {
|
||||
@@ -3054,6 +3036,9 @@ fn split_drop_cap(
|
||||
}
|
||||
let lines = lines.clamp(1, MAX_DROP_CAP_LINES) as usize;
|
||||
let mut dc: Vec<Glyph> = glyphs.drain(..n).collect();
|
||||
let spaces = glyphs.iter().take_while(|g| g.is_space()).count();
|
||||
let mut after: Vec<Glyph> = glyphs.drain(..spaces).collect();
|
||||
let end = after.last().map_or(end, |g| g.byte.saturating_add(g.len).max(end));
|
||||
let body = glyphs.iter().find(|g| g.len > 0 && g.cap > 0.0);
|
||||
let (lead, cap) = body.map_or((base_leading, base_cap), |g| (g.leading, g.cap));
|
||||
let lead = match grid.map(|inc| (lead / inc - 1e-6).ceil().max(1.0) * inc) {
|
||||
@@ -3063,8 +3048,9 @@ fn split_drop_cap(
|
||||
let drop = (lines - 1) as f64 * lead;
|
||||
let own = dc.iter().map(|g| g.cap).fold(0.0, f64::max);
|
||||
let k = if own > 0.0 { (drop + cap) / own } else { 1.0 };
|
||||
let k = if lines == 1 { k.min(1.0) } else { k };
|
||||
let k = if k.is_finite() && k > 0.0 { k.min(1000.0) } else { 1.0 };
|
||||
for g in &mut dc {
|
||||
for g in dc.iter_mut().chain(after.iter_mut()) {
|
||||
g.adv *= k;
|
||||
g.dx *= k;
|
||||
g.dy *= k;
|
||||
@@ -3078,7 +3064,6 @@ fn split_drop_cap(
|
||||
g.size *= k;
|
||||
g.space *= k;
|
||||
}
|
||||
let width = dc.iter().map(|g| g.adv).sum();
|
||||
let lsb = match dc.first() {
|
||||
Some(g) if align_left => {
|
||||
let ink = designcraft_geom::Shape::bounding_box(&*db.outline(g.face.get(), g.gid));
|
||||
@@ -3087,6 +3072,22 @@ fn split_drop_cap(
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
// Tabs advance to the paragraph's stops, measured from the column's start edge. Right to left,
|
||||
// its glyphs run from that (right) edge in this order (see `place_drop_cap`).
|
||||
let (stops, indent) = tabs;
|
||||
let mut x = indent - lsb;
|
||||
for i in 0..dc.len() {
|
||||
let tab = match dc.get(i) {
|
||||
Some(g) if g.ch == '\t' => Some(tab_advance(stops, indent, x, dc.get(i + 1..).unwrap_or_default()).0),
|
||||
_ => None,
|
||||
};
|
||||
if let (Some(w), Some(g)) = (tab, dc.get_mut(i)) {
|
||||
g.adv = w;
|
||||
}
|
||||
x += dc.get(i).map_or(0.0, |g| g.adv);
|
||||
}
|
||||
let width = dc.iter().map(|g| g.adv).sum();
|
||||
dc.append(&mut after);
|
||||
Some(DropCap { glyphs: dc, end, lines, width, drop, lsb })
|
||||
}
|
||||
|
||||
|
||||
@@ -274,11 +274,30 @@ pub(crate) fn shape_para(
|
||||
// Nested line styles lie under nested and GREP styles, and the drop cap's style over them all
|
||||
// (later overlays win).
|
||||
let mut overlays: Vec<(std::ops::Range<usize>, String)> = lines.to_vec();
|
||||
// The drop cap's style is the nested styles' leading "through 1 Dropcap" entry (the form IDML
|
||||
// stores it in). Set (`[None]` included), it restyles that entry; a real one stands in for a
|
||||
// missing entry. Either way the nested styles after it start after the drop cap.
|
||||
let set = Some(drop.1).filter(|s| !s.is_empty());
|
||||
let cap_style = set.filter(|s| *s != designcraft_doc::NO_CHAR_STYLE);
|
||||
let with_cap: Vec<designcraft_doc::NestedStyle>;
|
||||
let nested = match set {
|
||||
Some(s) if nested.first().is_some_and(designcraft_doc::NestedStyle::is_drop_cap) => {
|
||||
with_cap = std::iter::once(designcraft_doc::NestedStyle::drop_cap(s)).chain(nested.iter().skip(1).cloned()).collect();
|
||||
&with_cap[..]
|
||||
}
|
||||
Some(s) if cap_style.is_some() && !nested.is_empty() => {
|
||||
with_cap = std::iter::once(designcraft_doc::NestedStyle::drop_cap(s)).chain(nested.iter().cloned()).collect();
|
||||
&with_cap[..]
|
||||
}
|
||||
_ => nested,
|
||||
};
|
||||
if !nested.is_empty() || !grep.is_empty() {
|
||||
overlays.extend(crate::overlay::overlays(&story.text, range.clone(), nested, grep, drop.0));
|
||||
}
|
||||
if drop.0 > range.start && !drop.1.is_empty() && drop.1 != designcraft_doc::NO_CHAR_STYLE {
|
||||
overlays.push((range.start..drop.0, drop.1.to_string()));
|
||||
if let Some(s) = cap_style
|
||||
&& drop.0 > range.start
|
||||
{
|
||||
overlays.push((range.start..drop.0, s.to_string()));
|
||||
}
|
||||
// Each run, cut where nested / GREP styles start and end.
|
||||
let mut segments: Vec<(usize, usize, Option<&str>, &designcraft_doc::CharFormat)> = Vec::new();
|
||||
|
||||
@@ -2372,6 +2372,15 @@ fn drop_cap_takes_its_character_style_and_nested_styles_count_it() {
|
||||
assert_eq!(fill(&cs, at(0)), "Initial Red");
|
||||
assert_eq!(fill(&cs, at(2)), "Initial Red");
|
||||
assert_eq!(fill(&cs, at(3)), "[Black]");
|
||||
// The drop cap's style is a leading "through 1 drop cap" nested style: the nested styles start
|
||||
// after the drop cap.
|
||||
let one = designcraft_doc::NestedStyle { style: "Lead".into(), through: true, count: 1, until: designcraft_doc::NestedUntil::Characters };
|
||||
d.story_mut(sid).unwrap().paras[0].para.nested_styles = Some(vec![one]);
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = &cs.frames[0].lines[0];
|
||||
let at = |b: usize| l.glyphs.iter().find(|g| g.byte == b).unwrap();
|
||||
assert_eq!(fill(&cs, at(0)), "Initial Red");
|
||||
assert_eq!((fill(&cs, at(3)), fill(&cs, at(4))), ("Lead Blue".to_string(), "[Black]".to_string()));
|
||||
// A nested style "through 1 drop cap" covers the drop cap; the next one starts after it.
|
||||
let ns = |style: &str, until: designcraft_doc::NestedUntil| designcraft_doc::NestedStyle { style: style.into(), through: true, count: 1, until };
|
||||
d.story_mut(sid).unwrap().paras[0].para = ParaAttrs {
|
||||
@@ -2410,8 +2419,9 @@ fn drop_cap_caret_and_hit_testing() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_cap_sits_on_its_line_as_set() {
|
||||
// Lines on a 15 pt baseline grid (not the 12 pt leading): the drop cap follows line 3.
|
||||
fn drop_cap_on_a_baseline_grid_reaches_down_by_grid_steps() {
|
||||
// Lines on a 15 pt baseline grid (not the 12 pt leading): the drop cap reaches down two grid
|
||||
// steps, to line 3.
|
||||
let (mut d, sid, _) = drop_doc(LOREM, ParaAttrs { grid_align: Some(designcraft_doc::GridAlign::AllLines), ..drop_cap(3, 1) });
|
||||
d.settings.baseline_grid.increment = 15.0;
|
||||
d.settings.baseline_grid.start = 0.0;
|
||||
@@ -2427,6 +2437,86 @@ fn drop_cap_sits_on_its_line_as_set() {
|
||||
assert!((lines[2].baseline - cap_of(dc) - (lines[0].baseline - cap_of(body))).abs() < 0.01);
|
||||
}
|
||||
|
||||
/// A line with a larger auto leading doesn't move the drop cap: it reaches down `lines − 1`
|
||||
/// leadings of the text after it, not to where line N is set.
|
||||
#[test]
|
||||
fn drop_cap_reaches_down_by_the_text_leading_past_a_line_with_larger_leading() {
|
||||
use designcraft_doc::{CharAttrs, Composer, Leading};
|
||||
let text = [LOREM; 2].join(" ");
|
||||
let para = ParaAttrs { composer: Some(Composer::SingleLine), ..drop_cap(3, 1) };
|
||||
let (mut d, sid, _) = doc_with(&text, Rect::new(0.0, 0.0, 300.0, 1000.0), para);
|
||||
d.story_mut(sid).unwrap().paras[0].chars = CharAttrs { size: Some(12.0), leading: Some(Leading::Auto), ..Default::default() };
|
||||
// A larger word starts line 2, so lines 2 and 3 are set further down than 2 × 14.4 pt.
|
||||
let start2 = all_lines(&compose_story(&d, sid, &ComposeOptions::default()))[1].range.start;
|
||||
d.story_mut(sid).unwrap().format_chars(start2..start2 + 3, |f| f.over.size = Some(20.0));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines = all_lines(&cs);
|
||||
assert_eq!(lines[1].range.start, start2);
|
||||
assert!(lines[2].baseline - lines[0].baseline > 2.0 * 14.4 + 5.0, "{} {}", lines[0].baseline, lines[2].baseline);
|
||||
let dc = &lines[0].glyphs[0];
|
||||
assert!((dc.y - 2.0 * 14.4).abs() < 1e-6, "drop cap baseline {} below line 1", dc.y);
|
||||
let b = lines[0].drop_cap.unwrap();
|
||||
assert!((b.baseline - (lines[0].baseline + 2.0 * 14.4)).abs() < 1e-6, "{}", b.baseline);
|
||||
}
|
||||
|
||||
/// A space after the drop cap is set at its size on its baseline and doesn't widen the indent: the
|
||||
/// lines beside it start where its characters end.
|
||||
#[test]
|
||||
fn space_after_a_drop_cap_does_not_widen_the_indent() {
|
||||
let (d, sid, _) = drop_doc(&format!("In 2003 {LOREM}"), drop_cap(2, 2));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines = all_lines(&cs);
|
||||
let (i, n) = (&lines[0].glyphs[0], &lines[0].glyphs[1]);
|
||||
assert_eq!((i.byte, n.byte), (0, 1));
|
||||
let end = n.x + n.adv;
|
||||
for l in &lines[..2] {
|
||||
assert!((text_x(l, 3) - end).abs() < 1e-6, "{} vs {end}", text_x(l, 3));
|
||||
}
|
||||
let space = lines[0].glyphs.iter().find(|g| g.byte == 2).unwrap();
|
||||
assert!((space.sy - n.sy).abs() < 1e-9 && (space.y - n.y).abs() < 1e-9, "the space is set like the drop cap");
|
||||
assert_eq!(lines[0].drop_cap.unwrap().end, 3);
|
||||
}
|
||||
|
||||
/// A one-line drop cap keeps its characters' own size (a smaller verse number stays smaller), and
|
||||
/// the tabs in it advance to the paragraph's tab stops.
|
||||
#[test]
|
||||
fn one_line_drop_cap_keeps_its_size_and_sets_its_tabs() {
|
||||
let stop = |position, align| designcraft_doc::TabStop { position, align, leader: String::new(), align_on: String::new() };
|
||||
let para = ParaAttrs { tabs: Some(vec![stop(30.0, TabAlign::Right), stop(40.0, TabAlign::Left)]), ..drop_cap(1, 3) };
|
||||
let (mut d, sid, _) = drop_doc(&format!("\t7\t{LOREM}"), para);
|
||||
d.story_mut(sid).unwrap().format_chars(0..3, |f| f.over.size = Some(8.0));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = &cs.frames[0].lines[0];
|
||||
let at = |b: usize| l.glyphs.iter().find(|g| g.byte == b).unwrap();
|
||||
let (seven, body) = (at(1), at(3));
|
||||
assert!((seven.sy / body.sy - 0.8).abs() < 1e-9, "{} vs {}", seven.sy, body.sy);
|
||||
assert!(seven.y.abs() < 1e-9);
|
||||
assert!((seven.x + seven.adv - 30.0).abs() < 1e-6, "the number ends at the right tab: {}", seven.x + seven.adv);
|
||||
assert!((text_x(l, 3) - 40.0).abs() < 1e-6, "the text starts at the left tab: {}", text_x(l, 3));
|
||||
}
|
||||
|
||||
/// Right to left, the tabs in a drop cap advance to tab stops measured from the column's right
|
||||
/// edge.
|
||||
#[test]
|
||||
fn right_to_left_drop_cap_sets_its_tabs_from_the_right_edge() {
|
||||
let stop = |position, align| designcraft_doc::TabStop { position, align, leader: String::new(), align_on: String::new() };
|
||||
let para = ParaAttrs {
|
||||
direction: Some(designcraft_doc::TextDirection::RightToLeft),
|
||||
align: Some(Align::Right),
|
||||
tabs: Some(vec![stop(30.0, TabAlign::Right), stop(40.0, TabAlign::Left)]),
|
||||
..drop_cap(1, 3)
|
||||
};
|
||||
let (d, sid, _) = drop_doc(&format!("\t7\t{LOREM}"), para);
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = &cs.frames[0].lines[0];
|
||||
let seven = l.glyphs.iter().find(|g| g.byte == 1).unwrap();
|
||||
// The 300 pt column's right edge is at 300: the number's far (left) end is 30 pt from it.
|
||||
assert!((seven.x - 270.0).abs() < 1e-6, "the number ends at the right tab: {}", seven.x);
|
||||
// The text beside it starts at the left tab, 40 pt from the edge.
|
||||
let right = l.glyphs.iter().filter(|g| g.byte >= 3 && g.visible).map(|g| g.x + g.adv).fold(f64::NEG_INFINITY, f64::max);
|
||||
assert!(right <= 260.0 + 1e-6 && right > 255.0, "text ends at {right}");
|
||||
}
|
||||
|
||||
/// A rule in Text Color takes the colour of the paragraph's text: the first character's for the
|
||||
/// rule above, the last character's for the rule below. A swatch colour is used as is.
|
||||
#[test]
|
||||
|
||||
@@ -497,6 +497,18 @@ impl Default for NestedStyle {
|
||||
}
|
||||
}
|
||||
|
||||
impl NestedStyle {
|
||||
/// The drop cap's character style as a nested style: `style` through 1 drop cap.
|
||||
pub fn drop_cap(style: &str) -> Self {
|
||||
NestedStyle { style: style.into(), through: true, count: 1, until: NestedUntil::Dropcap }
|
||||
}
|
||||
|
||||
/// Is this the form a drop cap's character style takes at the start of a nested style list?
|
||||
pub fn is_drop_cap(&self) -> bool {
|
||||
self.until == NestedUntil::Dropcap && self.through && self.count == 1
|
||||
}
|
||||
}
|
||||
|
||||
/// A character style applied to every match of a regular expression in the paragraph.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
|
||||
@@ -360,6 +360,7 @@ fn type_para_sets_a_drop_cap_that_survives_idml() {
|
||||
let back = designcraft_idml::import_idml(&designcraft_idml::export_idml(&d)).unwrap();
|
||||
let p = &back.stories.values().find(|st| st.text.starts_with("Once")).unwrap().paras[0].para;
|
||||
assert_eq!((p.drop_cap_lines, p.drop_cap_chars, p.drop_cap_align_left), (Some(3), Some(1), Some(false)));
|
||||
// IDML has no attribute for the drop cap's character style.
|
||||
assert_eq!(p.drop_cap_style, None);
|
||||
// The drop cap's character style goes through IDML as a leading Dropcap nested style.
|
||||
assert_eq!(p.drop_cap_style.as_deref(), Some("Initial"));
|
||||
assert_eq!(p.nested_styles, Some(vec![designcraft_doc::NestedStyle::drop_cap("Initial")]));
|
||||
}
|
||||
|
||||
@@ -863,7 +863,8 @@ impl<'a> Ex<'a> {
|
||||
}
|
||||
(ps.para.clone(), ps.chars.clone())
|
||||
};
|
||||
self.para_attrs(&mut el, &mut props, &pa);
|
||||
let parent = (ps.name != designcraft_doc::NO_PARA_STYLE).then(|| ps.based_on.as_deref().unwrap_or(designcraft_doc::NO_PARA_STYLE));
|
||||
self.para_attrs(&mut el, &mut props, &pa, parent);
|
||||
self.char_attrs(&mut el, &mut props, &ca);
|
||||
pg.insert(&ps.name, with_props(el, props));
|
||||
}
|
||||
@@ -1245,7 +1246,9 @@ impl<'a> Ex<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn para_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs) {
|
||||
/// Write paragraph attributes `a`, set over the resolved paragraph style `parent` (`None` for the
|
||||
/// root style).
|
||||
fn para_attrs(&mut self, el: &mut El, props: &mut Vec<El>, a: &ParaAttrs, parent: Option<&str>) {
|
||||
if let Some(v) = &a.mojikumi {
|
||||
props.push(p("Mojikumi", if v.starts_with("MojikumiTable/") { "object" } else { "enumeration" }, v));
|
||||
}
|
||||
@@ -1293,8 +1296,31 @@ impl<'a> Ex<'a> {
|
||||
None => el.set("NumberingContinue", "true"),
|
||||
}
|
||||
}
|
||||
// Nested and GREP styles: property lists of records.
|
||||
if let Some(list) = &a.nested_styles {
|
||||
// Nested and GREP styles: property lists of records. The drop cap's character style is the
|
||||
// nested style list's leading "through 1 Dropcap" entry (IDML has no attribute for it): a
|
||||
// drop cap style set here (`[None]` included) restyles that entry, and a real one is
|
||||
// inserted where there is none. Without a list of its own, the inherited list is written
|
||||
// when that is what carries the drop cap style, since any list replaces the inherited one.
|
||||
let real = |s: &str| !s.is_empty() && s != st::NO_CHAR_STYLE;
|
||||
let cap = a.drop_cap_style.as_deref().filter(|s| !s.is_empty());
|
||||
let mut list = a.nested_styles.clone();
|
||||
if let (Some(cap), None) = (cap, &list) {
|
||||
let inherited = parent.map(|n| self.d.styles.resolve_para_style(n).0);
|
||||
let inherited_cap = inherited
|
||||
.as_ref()
|
||||
.is_some_and(|pp| real(&pp.drop_cap_style) || pp.nested_styles.first().is_some_and(|ns| ns.is_drop_cap() && real(&ns.style)));
|
||||
if real(cap) || inherited_cap {
|
||||
list = Some(inherited.map(|pp| pp.nested_styles).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
if let (Some(cap), Some(list)) = (cap, list.as_mut()) {
|
||||
match list.first_mut() {
|
||||
Some(ns) if ns.is_drop_cap() => ns.style = cap.to_string(),
|
||||
_ if real(cap) => list.insert(0, designcraft_doc::NestedStyle::drop_cap(cap)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(list) = &list {
|
||||
let mut l = El::new("AllNestedStyles").attr("type", "list");
|
||||
for ns in list {
|
||||
let (ty, delim) = names::nested_until_out(&ns.until);
|
||||
@@ -1378,8 +1404,6 @@ impl<'a> Ex<'a> {
|
||||
n!(space_after, "SpaceAfter");
|
||||
n!(drop_cap_lines, "DropCapLines");
|
||||
n!(drop_cap_chars, "DropCapCharacters");
|
||||
// The drop cap's character style isn't written: IDML has no attribute for it (InDesign
|
||||
// applies one through a Dropcap nested style).
|
||||
if a.drop_cap_align_left.is_some() || a.drop_cap_scale_descenders.is_some() {
|
||||
let d = designcraft_doc::ParaProps::default();
|
||||
let bits = u32::from(a.drop_cap_align_left.unwrap_or(d.drop_cap_align_left))
|
||||
@@ -2430,7 +2454,7 @@ impl<'a> Ex<'a> {
|
||||
fn psr_el(&mut self, pf: &ParaFormat) -> El {
|
||||
let mut el = El::new("ParagraphStyleRange").attr("AppliedParagraphStyle", names::style_self("ParagraphStyle", PARA_BUILTINS, &pf.style));
|
||||
let mut props = Vec::new();
|
||||
self.para_attrs(&mut el, &mut props, &pf.para);
|
||||
self.para_attrs(&mut el, &mut props, &pf.para, Some(&pf.style));
|
||||
self.char_attrs(&mut el, &mut props, &pf.chars);
|
||||
with_props(el, props)
|
||||
}
|
||||
|
||||
@@ -1406,6 +1406,9 @@ impl<'r> Importer<'r> {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// IDML has no drop cap style attribute: the drop cap's character style is the list's
|
||||
// leading "through 1 Dropcap" nested style.
|
||||
a.drop_cap_style = v.first().filter(|ns| ns.is_drop_cap()).map(|ns| ns.style.clone());
|
||||
a.nested_styles = Some(v);
|
||||
}
|
||||
if let Some(l) = e.prop_el("AllNestedLineStyles") {
|
||||
@@ -1434,7 +1437,6 @@ impl<'r> Importer<'r> {
|
||||
a.space_after = e.num("SpaceAfter");
|
||||
a.drop_cap_lines = u("DropCapLines");
|
||||
a.drop_cap_chars = u("DropCapCharacters");
|
||||
a.drop_cap_style = e.prop("DropCapStyle").map(|r| self.char_style_ref(r.trim()));
|
||||
// DropcapDetail bits: 1 = Align Left Edge, 2 = Scale for Descenders.
|
||||
if let Some(v) = e.num("DropcapDetail").filter(|v| v.is_finite() && *v >= 0.0) {
|
||||
let bits = v as u32;
|
||||
@@ -2486,12 +2488,35 @@ impl<'r> Importer<'r> {
|
||||
self.spreads.iter().chain(self.parents.iter()).find_map(|s| find(&s.items, id))
|
||||
}
|
||||
|
||||
/// A nested style list replaces the one a style or paragraph inherits, and with it the drop
|
||||
/// cap style (the list's leading "through 1 Dropcap" entry, read into the drop cap style). A
|
||||
/// list of its own without that entry clears an inherited drop cap style.
|
||||
fn clear_inherited_drop_cap_styles(&mut self) {
|
||||
let clears = |a: &designcraft_doc::ParaAttrs| a.drop_cap_style.is_none() && a.nested_styles.is_some();
|
||||
let cleared: Vec<String> = self
|
||||
.styles
|
||||
.paragraph
|
||||
.iter()
|
||||
.filter(|s| clears(&s.para) && inherits_drop_cap_style(&self.styles, s.based_on.as_deref()))
|
||||
.map(|s| s.name.clone())
|
||||
.collect();
|
||||
for name in cleared {
|
||||
if let Some(s) = self.styles.para_mut(&name) {
|
||||
s.para.drop_cap_style = Some(st::NO_CHAR_STYLE.into());
|
||||
}
|
||||
}
|
||||
for story in self.stories.values_mut() {
|
||||
clear_story_drop_cap_styles(&self.styles, story, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self, root: &El) -> Result<Document> {
|
||||
let title = root.get("Name").map(|n| n.trim_end_matches(".indd").to_string()).filter(|n| !n.is_empty()).unwrap_or_else(|| "Untitled".into());
|
||||
if self.spreads.is_empty() {
|
||||
return Err(IdmlError::Invalid("document has no spreads".into()));
|
||||
}
|
||||
self.settings.lists = std::mem::take(&mut self.lists);
|
||||
self.clear_inherited_drop_cap_styles();
|
||||
// Make sure built-in paragraph styles exist and are first.
|
||||
let d = Document {
|
||||
title,
|
||||
@@ -2695,6 +2720,47 @@ fn text_frame_options(prefs: &[&El]) -> TextFrameOptions {
|
||||
o
|
||||
}
|
||||
|
||||
/// Deepest table-in-cell or note nesting the drop cap pass walks into.
|
||||
const MAX_STORY_NESTING: usize = 32;
|
||||
|
||||
/// [`Importer::clear_inherited_drop_cap_styles`] for one story's paragraphs and those of the table
|
||||
/// cells, footnotes and endnotes in it.
|
||||
fn clear_story_drop_cap_styles(styles: &Styles, story: &mut Story, depth: usize) {
|
||||
for p in &mut story.paras {
|
||||
if p.para.drop_cap_style.is_none() && p.para.nested_styles.is_some() && inherits_drop_cap_style(styles, Some(&p.style)) {
|
||||
p.para.drop_cap_style = Some(st::NO_CHAR_STYLE.into());
|
||||
}
|
||||
}
|
||||
if depth >= MAX_STORY_NESTING {
|
||||
return;
|
||||
}
|
||||
for table in story.tables.values_mut() {
|
||||
for cell in &mut Arc::make_mut(table).cells {
|
||||
clear_story_drop_cap_styles(styles, &mut cell.text, depth + 1);
|
||||
}
|
||||
}
|
||||
for note in story.notes.iter_mut().chain(story.endnotes.iter_mut()) {
|
||||
clear_story_drop_cap_styles(styles, &mut Arc::make_mut(note).text, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Does paragraph style `name` (and its based-on chain) set a drop cap character style? A style
|
||||
/// with a nested style list of its own and no drop cap style has none.
|
||||
fn inherits_drop_cap_style<'a>(styles: &'a Styles, mut name: Option<&'a str>) -> bool {
|
||||
let mut seen = HashSet::new();
|
||||
while let Some(n) = name {
|
||||
let Some(s) = styles.para(n).filter(|_| seen.insert(n)) else { break };
|
||||
if let Some(c) = s.para.drop_cap_style.as_deref() {
|
||||
return !c.is_empty() && c != st::NO_CHAR_STYLE;
|
||||
}
|
||||
if s.para.nested_styles.is_some() {
|
||||
return false;
|
||||
}
|
||||
name = s.based_on.as_deref();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Attribute lookup over several elements: the first that has it.
|
||||
struct Prefs<'a>(&'a [&'a El]);
|
||||
|
||||
|
||||
+132
-10
@@ -1380,12 +1380,13 @@ fn arabic_story_and_table_directions_survive_idml_export() {
|
||||
assert_eq!(st.tables.values().next().unwrap().options.direction, designcraft_doc::TextDirection::RightToLeft);
|
||||
}
|
||||
|
||||
/// IDML has no attribute for a drop cap's character style: it is the nested style list's leading
|
||||
/// "through 1 Dropcap" entry, read into the drop cap style and written back from it.
|
||||
#[test]
|
||||
fn drop_caps_import_and_round_trip() {
|
||||
// A style with a drop cap, its character style and a nested style through it; a paragraph
|
||||
// overriding it locally.
|
||||
// A style with a drop cap and its character style; a paragraph overriding both locally.
|
||||
let opener = r#"<ParagraphStyle Self="ParagraphStyle/Opener" Name="Opener" DropCapCharacters="1" DropCapLines="3">
|
||||
<Properties><DropCapStyle type="object">CharacterStyle/Strong</DropCapStyle><AllNestedStyles type="list"><ListItem type="record">
|
||||
<Properties><AllNestedStyles type="list"><ListItem type="record">
|
||||
<AppliedCharacterStyle type="object">CharacterStyle/Strong</AppliedCharacterStyle><Delimiter type="enumeration">Dropcap</Delimiter>
|
||||
<Repetition type="long">1</Repetition><Inclusive type="boolean">true</Inclusive></ListItem></AllNestedStyles></Properties>
|
||||
</ParagraphStyle>
|
||||
@@ -1394,7 +1395,10 @@ fn drop_caps_import_and_round_trip() {
|
||||
let story = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
|
||||
<Story Self="s1">
|
||||
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener" DropCapCharacters="2" DropCapLines="2" DropCapStyle="CharacterStyle/$ID/[No character style]">
|
||||
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener" DropCapCharacters="2" DropCapLines="2">
|
||||
<Properties><AllNestedStyles type="list"><ListItem type="record">
|
||||
<AppliedCharacterStyle type="object">CharacterStyle/$ID/[No character style]</AppliedCharacterStyle><Delimiter type="enumeration">Dropcap</Delimiter>
|
||||
<Repetition type="long">1</Repetition><Inclusive type="boolean">true</Inclusive></ListItem></AllNestedStyles></Properties>
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Once upon a time</Content></CharacterStyleRange>
|
||||
</ParagraphStyleRange>
|
||||
</Story>
|
||||
@@ -1412,18 +1416,21 @@ fn drop_caps_import_and_round_trip() {
|
||||
let st = d.styles.para("Opener").unwrap();
|
||||
assert_eq!((st.para.drop_cap_chars, st.para.drop_cap_lines), (Some(1), Some(3)));
|
||||
assert_eq!(st.para.drop_cap_style.as_deref(), Some("Strong"));
|
||||
let ns = st.para.nested_styles.clone().unwrap();
|
||||
assert_eq!((ns[0].style.as_str(), &ns[0].until), ("Strong", &designcraft_doc::NestedUntil::Dropcap));
|
||||
assert_eq!(st.para.nested_styles, Some(vec![designcraft_doc::NestedStyle::drop_cap("Strong")]), "one entry, not duplicated");
|
||||
let p = &d.stories.values().find(|s| s.text.starts_with("Once")).unwrap().paras[0];
|
||||
assert_eq!((p.para.drop_cap_chars, p.para.drop_cap_lines), (Some(2), Some(2)));
|
||||
assert_eq!(p.para.drop_cap_style.as_deref(), Some(st::NO_CHAR_STYLE));
|
||||
};
|
||||
let d = import_idml_with(&bytes, &|_| None).unwrap();
|
||||
check(&d);
|
||||
// The drop cap's character style isn't written to IDML (InDesign sets it through a Dropcap
|
||||
// nested style); Align Left Edge and Scale for Descenders are, as DropcapDetail.
|
||||
let bytes = export_idml(&d);
|
||||
assert!(!zip_text(&bytes, "").contains("DropCapStyle"), "not an IDML attribute");
|
||||
check(&import_idml(&bytes).unwrap());
|
||||
// A drop cap style set without nested styles is written as one; Align Left Edge and Scale for
|
||||
// Descenders as DropcapDetail.
|
||||
let mut d = d;
|
||||
if let Some(s) = d.styles_mut().para_mut("Opener") {
|
||||
s.para.nested_styles = None;
|
||||
s.para.drop_cap_align_left = Some(false);
|
||||
s.para.drop_cap_scale_descenders = Some(true);
|
||||
}
|
||||
@@ -1432,9 +1439,124 @@ fn drop_caps_import_and_round_trip() {
|
||||
assert!(zip_text(&bytes, "Resources/Styles.xml").contains(r#"DropcapDetail="2""#));
|
||||
let back = import_idml(&bytes).unwrap();
|
||||
let st = back.styles.para("Opener").unwrap();
|
||||
assert_eq!((st.para.drop_cap_chars, st.para.drop_cap_lines, st.para.drop_cap_style.as_deref()), (Some(1), Some(3), None));
|
||||
assert_eq!((st.para.drop_cap_chars, st.para.drop_cap_lines, st.para.drop_cap_style.as_deref()), (Some(1), Some(3), Some("Strong")));
|
||||
assert_eq!((st.para.drop_cap_align_left, st.para.drop_cap_scale_descenders), (Some(false), Some(true)));
|
||||
assert_eq!(st.para.nested_styles.as_ref().map(|n| n[0].until.clone()), Some(designcraft_doc::NestedUntil::Dropcap));
|
||||
assert_eq!(st.para.nested_styles, Some(vec![designcraft_doc::NestedStyle::drop_cap("Strong")]));
|
||||
}
|
||||
|
||||
/// IDML styles and stories with an `Opener` style whose drop cap style is `Strong`, plus `styles`
|
||||
/// (more paragraph styles) and `paras` (the story's paragraph ranges). `WORDS` in them stands for
|
||||
/// a nested style list of `Strong` through 1 word.
|
||||
fn drop_cap_fixture(styles: &str, paras: &str) -> Vec<u8> {
|
||||
let nested = |style: &str, delim: &str| {
|
||||
format!(
|
||||
r#"<AllNestedStyles type="list"><ListItem type="record"><AppliedCharacterStyle type="object">CharacterStyle/{style}</AppliedCharacterStyle><Delimiter type="enumeration">{delim}</Delimiter><Repetition type="long">1</Repetition><Inclusive type="boolean">true</Inclusive></ListItem></AllNestedStyles>"#
|
||||
)
|
||||
};
|
||||
let words = nested("Strong", "AnyWord");
|
||||
let opener = format!(
|
||||
r#"<ParagraphStyle Self="ParagraphStyle/Opener" Name="Opener" DropCapCharacters="1" DropCapLines="3"><Properties>{}</Properties></ParagraphStyle>
|
||||
{}
|
||||
</RootParagraphStyleGroup>"#,
|
||||
nested("Strong", "Dropcap"),
|
||||
styles.replace("WORDS", &words)
|
||||
);
|
||||
let styles = STYLES.replace("</RootParagraphStyleGroup>", &opener);
|
||||
let story = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
|
||||
<Story Self="s1">{}</Story>
|
||||
</idPkg:Story>"#,
|
||||
paras.replace("WORDS", &words)
|
||||
);
|
||||
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),
|
||||
])
|
||||
}
|
||||
|
||||
/// A nested style list replaces the inherited one, drop cap style entry included: a style or
|
||||
/// paragraph with a list of its own and no Dropcap entry has no drop cap style.
|
||||
#[test]
|
||||
fn own_nested_styles_clear_an_inherited_drop_cap_style() {
|
||||
let styles = r#"<ParagraphStyle Self="ParagraphStyle/Child" Name="Child"><Properties><BasedOn type="object">ParagraphStyle/Opener</BasedOn>WORDS</Properties></ParagraphStyle>
|
||||
<ParagraphStyle Self="ParagraphStyle/Plain" Name="Plain"><Properties>WORDS</Properties></ParagraphStyle>"#;
|
||||
let paras = r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener"><Properties>WORDS</Properties>
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Cleared</Content><Br/></CharacterStyleRange>
|
||||
</ParagraphStyleRange>
|
||||
<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener">
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Inherited</Content></CharacterStyleRange>
|
||||
</ParagraphStyleRange>"#;
|
||||
let d = import_idml_with(&drop_cap_fixture(styles, paras), &|_| None).unwrap();
|
||||
let cap = |name: &str| d.styles.para(name).unwrap().para.drop_cap_style.clone();
|
||||
assert_eq!(cap("Opener").as_deref(), Some("Strong"));
|
||||
assert_eq!(cap("Child").as_deref(), Some(st::NO_CHAR_STYLE), "its own list replaces Opener's");
|
||||
assert_eq!(cap("Plain"), None, "nothing to clear");
|
||||
let story = d.stories.values().find(|s| s.text.starts_with("Cleared")).unwrap();
|
||||
assert_eq!(story.paras[0].para.drop_cap_style.as_deref(), Some(st::NO_CHAR_STYLE));
|
||||
assert_eq!(story.paras[1].para.drop_cap_style, None);
|
||||
assert_eq!(d.styles.resolve_para(&story.paras[1]).0.drop_cap_style, "Strong");
|
||||
}
|
||||
|
||||
/// Paragraphs in table cells and footnotes are cleared like story paragraphs.
|
||||
#[test]
|
||||
fn own_nested_styles_clear_an_inherited_drop_cap_style_in_cells_and_notes() {
|
||||
let opener = |text: &str| {
|
||||
format!(
|
||||
r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener"><Properties>WORDS</Properties><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>{text}</Content></CharacterStyleRange></ParagraphStyleRange>"#
|
||||
)
|
||||
};
|
||||
let paras = format!(
|
||||
r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle"><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]">
|
||||
<Content>Body</Content><Footnote>{}</Footnote>
|
||||
<Table HeaderRowCount="0" FooterRowCount="0"><Row Name="0" MinimumHeight="24"/><Column Name="0" SingleColumnWidth="60"/><Cell Name="0:0">{}</Cell></Table>
|
||||
</CharacterStyleRange></ParagraphStyleRange>"#,
|
||||
opener("Note"),
|
||||
opener("Cell")
|
||||
);
|
||||
let d = import_idml_with(&drop_cap_fixture("", ¶s), &|_| None).unwrap();
|
||||
let story = d.stories.values().find(|s| !s.notes.is_empty()).expect("story with a footnote");
|
||||
let note = &story.notes[0].text;
|
||||
assert_eq!(note.paras[0].para.drop_cap_style.as_deref(), Some(st::NO_CHAR_STYLE), "{:?}", note.text);
|
||||
let cell = &story.tables.values().next().expect("a table").cell(0, 0).unwrap().text;
|
||||
assert_eq!(cell.paras[0].para.drop_cap_style.as_deref(), Some(st::NO_CHAR_STYLE), "{:?}", cell.text);
|
||||
}
|
||||
|
||||
/// A paragraph that clears its style's drop cap style without nested styles of its own round-trips:
|
||||
/// it is written with the style's list, the Dropcap entry set to [None].
|
||||
#[test]
|
||||
fn a_cleared_drop_cap_style_round_trips() {
|
||||
let paras = r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Opener">
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Once upon a time</Content></CharacterStyleRange>
|
||||
</ParagraphStyleRange>"#;
|
||||
let mut d = import_idml_with(&drop_cap_fixture("", paras), &|_| None).unwrap();
|
||||
let sid = *d.stories.keys().next().unwrap();
|
||||
let clear = |d: &mut Document| d.story_mut(sid).unwrap().paras[0].para.drop_cap_style = Some(st::NO_CHAR_STYLE.into());
|
||||
let resolved = |d: &Document| {
|
||||
let st = d.stories.values().find(|s| s.text.starts_with("Once")).unwrap();
|
||||
d.styles.resolve_para(&st.paras[0]).0
|
||||
};
|
||||
clear(&mut d);
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
let pp = resolved(&back);
|
||||
assert_eq!(pp.drop_cap_style, st::NO_CHAR_STYLE);
|
||||
assert_eq!(pp.nested_styles, vec![designcraft_doc::NestedStyle::drop_cap(st::NO_CHAR_STYLE)]);
|
||||
// The style's drop cap style without nested styles: the paragraph is written with an empty
|
||||
// list, which clears it on import.
|
||||
if let Some(s) = d.styles_mut().para_mut("Opener") {
|
||||
s.para.nested_styles = None;
|
||||
}
|
||||
assert_eq!(resolved(&d).drop_cap_style, st::NO_CHAR_STYLE);
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
assert_eq!(back.styles.para("Opener").unwrap().para.drop_cap_style.as_deref(), Some("Strong"));
|
||||
let pp = resolved(&back);
|
||||
assert_eq!(pp.drop_cap_style, st::NO_CHAR_STYLE);
|
||||
assert!(pp.nested_styles.iter().all(|ns| ns.style != "Strong"), "{:?}", pp.nested_styles);
|
||||
}
|
||||
|
||||
/// Frames take the corners and Text Frame Options they don't write from their object style chain,
|
||||
|
||||
Reference in New Issue
Block a user