mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-10 18:40:27 -06:00
Merge #298: Lists: numbering formats and expressions from IDML; an empty paragraph gets no bullet or number
Conflict: crates/compose/src/lib.rs: next's list_label match with its list_label glyph flag; arms take #298's empty-paragraph rule, number_label/bullet_label and saturating counter Conflict: crates/compose/src/tests.rs: next's file with #298's appended tests
This commit is contained in:
@@ -514,13 +514,14 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
story
|
||||
.paras
|
||||
.iter()
|
||||
.map(|p| {
|
||||
.zip(¶_ranges)
|
||||
.map(|(p, r)| {
|
||||
let (pp, _) = doc.styles.resolve_para(p);
|
||||
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() {
|
||||
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() || r.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let c = counters.entry(pp.list_name.clone()).or_insert_with(|| doc.list_start(story.id, &pp.list_name));
|
||||
*c = pp.start_at.map_or(*c + 1, |s| s.max(1));
|
||||
*c = pp.start_at.map_or(c.saturating_add(1), |s| s.max(1));
|
||||
Some(*c)
|
||||
})
|
||||
.collect()
|
||||
@@ -772,16 +773,18 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
// List labels take the default super/subscript settings.
|
||||
let label_env = shape::TypeEnv { adv: Default::default(), ..env };
|
||||
let list_label = match pp.list_type {
|
||||
// An empty paragraph has no bullet or number, and the numbering carries on past it.
|
||||
designcraft_doc::ListType::Numbers | designcraft_doc::ListType::Bullets if prange.is_empty() => None,
|
||||
designcraft_doc::ListType::Numbers if !pp.list_name.is_empty() => {
|
||||
// A named list: carries on past other paragraphs (and from earlier stories).
|
||||
let n = named_numbers.get(pi).copied().flatten().unwrap_or(1);
|
||||
Some(format!("{}.{}", pp.number_style.format(n), pp.list_separator))
|
||||
Some(pp.number_label(n))
|
||||
}
|
||||
designcraft_doc::ListType::Numbers => {
|
||||
list_counter = pp.start_at.map_or(list_counter + 1, |s| s.max(1));
|
||||
Some(format!("{}.{}", pp.number_style.format(list_counter), pp.list_separator))
|
||||
list_counter = pp.start_at.map_or(list_counter.saturating_add(1), |s| s.max(1));
|
||||
Some(pp.number_label(list_counter))
|
||||
}
|
||||
designcraft_doc::ListType::Bullets => Some(format!("{}{}", pp.bullet_char, pp.list_separator)),
|
||||
designcraft_doc::ListType::Bullets => Some(pp.bullet_label()),
|
||||
designcraft_doc::ListType::None => {
|
||||
list_counter = 0;
|
||||
None
|
||||
|
||||
@@ -4285,3 +4285,74 @@ fn split_block_flows_into_the_next_column() {
|
||||
let top = ft.lines.iter().find(|l| l.column == 1).unwrap();
|
||||
assert!((s..s + 6).contains(&top.para) && top.baseline < 20.0);
|
||||
}
|
||||
|
||||
/// Each paragraph's list label as text: its label glyphs (laid before the paragraph's own text)
|
||||
/// matched back to characters of the label font; tabs and other invisible glyphs are skipped.
|
||||
fn list_labels(cs: &ComposedStory, alphabet: &str) -> Vec<String> {
|
||||
all_lines(cs)
|
||||
.iter()
|
||||
.filter(|l| l.first_in_para)
|
||||
.map(|l| {
|
||||
l.glyphs
|
||||
.iter()
|
||||
.take_while(|g| g.len == 0)
|
||||
.filter(|g| g.visible)
|
||||
.map(|g| alphabet.chars().find(|c| g.face.glyph_for(*c) == g.gid).unwrap_or('?'))
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
const LABEL_CHARS: &str = "0123456789ABCDIVXabcdivx.\u{2022} Tabel";
|
||||
|
||||
fn numbered(style: designcraft_doc::NumberStyle, expression: &str) -> ParaAttrs {
|
||||
ParaAttrs {
|
||||
list_type: Some(designcraft_doc::ListType::Numbers),
|
||||
number_style: Some(style),
|
||||
number_expression: Some(expression.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbered_lists_follow_their_format_and_expression() {
|
||||
use designcraft_doc::NumberStyle as N;
|
||||
let cases: [(N, &str, [&str; 3]); 4] = [
|
||||
(N::UpperLetters, "^#.^t", ["A.", "B.", "C."]),
|
||||
(N::LowerRoman, "^#.^t", ["i.", "ii.", "iii."]),
|
||||
(N::ArabicThreeDigits, "^#.^t", ["001.", "002.", "003."]),
|
||||
(N::Arabic, "Tabel ^#^t", ["Tabel 1", "Tabel 2", "Tabel 3"]),
|
||||
];
|
||||
for (style, expression, want) in cases {
|
||||
let (d, sid, _) = doc_with("One\nTwo\nThree", Rect::new(0.0, 0.0, 300.0, 300.0), numbered(style, expression));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
assert_eq!(list_labels(&cs, LABEL_CHARS), want, "{style:?} {expression}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbering_from_the_largest_start_number_does_not_overflow() {
|
||||
let (mut d, sid, _) = doc_with("One\nTwo", Rect::new(0.0, 0.0, 300.0, 300.0), numbered(designcraft_doc::NumberStyle::Arabic, "^#."));
|
||||
d.story_mut(sid).unwrap().paras[0].para.start_at = Some(Some(u32::MAX));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let max = format!("{}.", u32::MAX);
|
||||
assert_eq!(list_labels(&cs, LABEL_CHARS), [max.clone(), max]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_paragraphs_get_no_bullet_or_number() {
|
||||
let bullets = ParaAttrs { list_type: Some(designcraft_doc::ListType::Bullets), ..Default::default() };
|
||||
let (d, sid, _) = doc_with("One\n\nTwo\n", Rect::new(0.0, 0.0, 300.0, 300.0), bullets);
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
assert_eq!(list_labels(&cs, LABEL_CHARS), ["\u{2022}", "", "\u{2022}", ""]);
|
||||
// An empty paragraph doesn't use up a number, in a story's own list or a named one.
|
||||
for name in ["", "Steps"] {
|
||||
let attrs = ParaAttrs { list_name: Some(name.into()), ..numbered(designcraft_doc::NumberStyle::Arabic, "^#.^t") };
|
||||
let (mut d, sid, _) = doc_with("One\n\nTwo\n", Rect::new(0.0, 0.0, 300.0, 300.0), attrs);
|
||||
if !name.is_empty() {
|
||||
d.settings.lists.push(designcraft_doc::NumberedList { name: name.into(), continue_across_stories: false });
|
||||
}
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
assert_eq!(list_labels(&cs, LABEL_CHARS), ["1.", "", "2.", ""], "list `{name}`");
|
||||
}
|
||||
}
|
||||
|
||||
+105
-1
@@ -577,15 +577,33 @@ pub enum NumberStyle {
|
||||
LowerLetters,
|
||||
/// 01, 02, …
|
||||
ArabicLeadingZero,
|
||||
/// 001, 002, …
|
||||
ArabicThreeDigits,
|
||||
/// 0001, 0002, …
|
||||
ArabicFourDigits,
|
||||
/// *, †, ‡, §, ¶, ‖, then doubled (**, ††, …): footnotes.
|
||||
Symbols,
|
||||
}
|
||||
|
||||
/// Numbers past this are written in Arabic digits by the styles that repeat characters (roman
|
||||
/// numerals, letters, symbols), so a hostile start number can't build a huge label.
|
||||
const MAX_REPEATED_FORM: u32 = 3999;
|
||||
|
||||
impl NumberStyle {
|
||||
pub fn format(self, n: u32) -> String {
|
||||
if n > MAX_REPEATED_FORM
|
||||
&& matches!(
|
||||
self,
|
||||
NumberStyle::UpperRoman | NumberStyle::LowerRoman | NumberStyle::UpperLetters | NumberStyle::LowerLetters | NumberStyle::Symbols
|
||||
)
|
||||
{
|
||||
return n.to_string();
|
||||
}
|
||||
match self {
|
||||
NumberStyle::Arabic => n.to_string(),
|
||||
NumberStyle::ArabicLeadingZero => format!("{n:02}"),
|
||||
NumberStyle::ArabicThreeDigits => format!("{n:03}"),
|
||||
NumberStyle::ArabicFourDigits => format!("{n:04}"),
|
||||
NumberStyle::UpperRoman => roman(n),
|
||||
NumberStyle::LowerRoman => roman(n).to_lowercase(),
|
||||
NumberStyle::UpperLetters => letters(n),
|
||||
@@ -599,6 +617,58 @@ impl NumberStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand the metacharacters of a list's label text (`^t` tab, `^>` en space, `^^` caret, …),
|
||||
/// putting `number` in for `^#`. Level placeholders (`^1`…`^9`) and metacharacters with no
|
||||
/// meaning in a label are dropped; a label never holds a paragraph break.
|
||||
pub fn expand_list_text(s: &str, number: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + number.len());
|
||||
let mut it = s.chars();
|
||||
while let Some(c) = it.next() {
|
||||
if c != '^' {
|
||||
if c != '\n' && c != '\r' {
|
||||
out.push(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let Some(m) = it.next() else {
|
||||
out.push('^');
|
||||
break;
|
||||
};
|
||||
let r = match m {
|
||||
'#' => {
|
||||
out.push_str(number);
|
||||
continue;
|
||||
}
|
||||
'^' => '^',
|
||||
't' => '\t',
|
||||
'y' => crate::story::RIGHT_INDENT_TAB,
|
||||
'i' => crate::story::INDENT_HERE,
|
||||
'n' => crate::story::FORCED_LINE_BREAK,
|
||||
'm' => '\u{2003}',
|
||||
'>' => '\u{2002}',
|
||||
'f' => '\u{2001}',
|
||||
'|' => '\u{200A}',
|
||||
'<' => '\u{2009}',
|
||||
'/' => '\u{2007}',
|
||||
'.' => '\u{2008}',
|
||||
's' => '\u{A0}',
|
||||
'S' => '\u{202F}',
|
||||
'_' => '\u{2014}',
|
||||
'=' => '\u{2013}',
|
||||
'~' => '\u{2011}',
|
||||
'{' => '\u{201C}',
|
||||
'}' => '\u{201D}',
|
||||
'[' => '\u{2018}',
|
||||
']' => '\u{2019}',
|
||||
'"' => '"',
|
||||
'\'' => '\'',
|
||||
_ => continue,
|
||||
};
|
||||
out.push(r);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn roman(mut n: u32) -> String {
|
||||
if n == 0 {
|
||||
return "0".into();
|
||||
@@ -807,7 +877,10 @@ attr_set! {
|
||||
start_at: Option<u32> = None,
|
||||
bullet_char: String = "\u{2022}".into(),
|
||||
number_style: NumberStyle = NumberStyle::Arabic,
|
||||
/// Text after the number/bullet (`^t` = tab).
|
||||
/// A numbered paragraph's label: `^#` is the number, other metacharacters expand as in
|
||||
/// [`expand_list_text`].
|
||||
number_expression: String = "^#.^t".into(),
|
||||
/// Text after the bullet (metacharacters expand as in [`expand_list_text`]).
|
||||
list_separator: String = "\t".into(),
|
||||
balance_ragged: bool = false,
|
||||
optical_margin: bool = false,
|
||||
@@ -859,6 +932,20 @@ impl ParaAttrs {
|
||||
}
|
||||
}
|
||||
|
||||
impl ParaProps {
|
||||
/// The label of a numbered paragraph with number `n`.
|
||||
pub fn number_label(&self, n: u32) -> String {
|
||||
expand_list_text(&self.number_expression, &self.number_style.format(n))
|
||||
}
|
||||
|
||||
/// The label of a bulleted paragraph.
|
||||
pub fn bullet_label(&self) -> String {
|
||||
let mut s: String = self.bullet_char.chars().filter(|c| *c != '\n' && *c != '\r').collect();
|
||||
s.push_str(&expand_list_text(&self.list_separator, ""));
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1042,5 +1129,22 @@ mod tests {
|
||||
assert_eq!(NumberStyle::UpperLetters.format(27), "AA");
|
||||
assert_eq!(NumberStyle::LowerLetters.format(28), "bb");
|
||||
assert_eq!(NumberStyle::ArabicLeadingZero.format(3), "03");
|
||||
assert_eq!(NumberStyle::ArabicThreeDigits.format(7), "007");
|
||||
assert_eq!(NumberStyle::ArabicFourDigits.format(12), "0012");
|
||||
// Repeating forms stay small for huge numbers.
|
||||
assert_eq!(NumberStyle::UpperRoman.format(u32::MAX), u32::MAX.to_string());
|
||||
assert_eq!(NumberStyle::LowerLetters.format(u32::MAX), u32::MAX.to_string());
|
||||
assert_eq!(NumberStyle::Symbols.format(u32::MAX), u32::MAX.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_labels_expand_metacharacters() {
|
||||
let p = ParaProps { number_style: NumberStyle::UpperLetters, ..Default::default() };
|
||||
assert_eq!(p.number_label(2), "B.\t");
|
||||
let p = ParaProps { number_expression: "Tabel ^#^t".into(), ..Default::default() };
|
||||
assert_eq!(p.number_label(3), "Tabel 3\t");
|
||||
assert_eq!(expand_list_text("^2.^#.^>x^^^", "4"), ".4.\u{2002}x^^");
|
||||
assert_eq!(expand_list_text("a\nb^p", "1"), "ab");
|
||||
assert_eq!(ParaProps::default().bullet_label(), "\u{2022}\t");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,10 +235,10 @@ impl crate::Document {
|
||||
before.sort_by_key(|st| key(st));
|
||||
let mut n = 0u32;
|
||||
for st in before {
|
||||
for p in &st.paras {
|
||||
for (p, r) in st.paras.iter().zip(st.para_ranges()) {
|
||||
let (pp, _) = self.styles.resolve_para(p);
|
||||
if pp.list_type == crate::ListType::Numbers && pp.list_name == name {
|
||||
n = pp.start_at.map_or(n + 1, |s| s.max(1));
|
||||
if pp.list_type == crate::ListType::Numbers && pp.list_name == name && !r.is_empty() {
|
||||
n = pp.start_at.map_or(n.saturating_add(1), |s| s.max(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -347,13 +347,18 @@ impl Document {
|
||||
let (sid, pos) = self.find_anchor(id)?;
|
||||
let st = self.story(sid)?;
|
||||
let pi = st.para_at(pos);
|
||||
let r = st.para_ranges()[pi].clone();
|
||||
let ranges = st.para_ranges();
|
||||
let r = ranges[pi].clone();
|
||||
let (pp, _) = self.styles.resolve_para(&st.paras[pi]);
|
||||
let para_num = match pp.list_type {
|
||||
crate::ListType::Numbers => {
|
||||
// Count the run of numbered paragraphs before this one.
|
||||
let n = (0..=pi).rev().take_while(|&k| self.styles.resolve_para(&st.paras[k]).0.list_type == crate::ListType::Numbers).count();
|
||||
format!("{}.", pp.number_style.format(n as u32))
|
||||
crate::ListType::Numbers if !r.is_empty() => {
|
||||
// Count the run of numbered paragraphs before this one (empty ones have no number).
|
||||
let n = (0..=pi)
|
||||
.rev()
|
||||
.take_while(|&k| self.styles.resolve_para(&st.paras[k]).0.list_type == crate::ListType::Numbers)
|
||||
.filter(|&k| ranges.get(k).is_some_and(|r| !r.is_empty()))
|
||||
.count();
|
||||
pp.number_label(u32::try_from(n).unwrap_or(u32::MAX)).trim_end().to_string()
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
@@ -1461,6 +1461,18 @@ impl<'a> Ex<'a> {
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(n) = a.number_style {
|
||||
props.push(p("NumberingFormat", "string", names::numbering_format_out(n)));
|
||||
}
|
||||
if let Some(x) = &a.number_expression {
|
||||
el.set("NumberingExpression", names::list_text_out(x));
|
||||
}
|
||||
if let Some(x) = &a.list_separator {
|
||||
el.set("BulletsTextAfter", names::list_text_out(x));
|
||||
}
|
||||
if let Some(c) = a.bullet_char.as_deref().and_then(|s| s.chars().next()) {
|
||||
props.push(El::new("BulletChar").attr("BulletCharacterType", "UnicodeOnly").attr("BulletCharacterValue", u32::from(c).to_string()));
|
||||
}
|
||||
if let Some(b) = a.balance_ragged {
|
||||
props.push(p("BalanceRaggedLines", "enumeration", if b { "FullyBalanced" } else { "NoBalancing" }));
|
||||
}
|
||||
|
||||
@@ -1390,6 +1390,17 @@ impl<'r> Importer<'r> {
|
||||
"NumberedList" => ListType::Numbers,
|
||||
_ => ListType::None,
|
||||
});
|
||||
// A format with no counterpart (CJK, Arabic, Hebrew numbering) is left unset: decimal.
|
||||
a.number_style = e.prop("NumberingFormat").and_then(|v| names::numbering_format_in(&v));
|
||||
a.number_expression = e.prop("NumberingExpression");
|
||||
a.list_separator = e.prop("BulletsTextAfter");
|
||||
if let Some(b) = e.prop_el("BulletChar") {
|
||||
// A glyph of a named font (`GlyphWithFont`) has no character to map to.
|
||||
let unicode = matches!(b.get("BulletCharacterType"), Some("UnicodeOnly" | "UnicodeWithFont"));
|
||||
if let Some(c) = b.get("BulletCharacterValue").and_then(|v| v.trim().parse::<u32>().ok()).and_then(char::from_u32).filter(|_| unicode) {
|
||||
a.bullet_char = Some(c.to_string());
|
||||
}
|
||||
}
|
||||
a.balance_ragged = e.prop("BalanceRaggedLines").map(|v| v.trim() != "NoBalancing" && v.trim() != "false");
|
||||
a.shading_on = e.boolean("ParagraphShadingOn");
|
||||
if let Some(c) = e.prop("ParagraphShadingColor") {
|
||||
|
||||
@@ -463,7 +463,7 @@ pub const REF_POINTS: [&str; 9] = [
|
||||
|
||||
pub fn number_style_out(n: NumberStyle) -> &'static str {
|
||||
match n {
|
||||
NumberStyle::Arabic | NumberStyle::ArabicLeadingZero => "Arabic",
|
||||
NumberStyle::Arabic | NumberStyle::ArabicLeadingZero | NumberStyle::ArabicThreeDigits | NumberStyle::ArabicFourDigits => "Arabic",
|
||||
NumberStyle::UpperRoman => "UpperRoman",
|
||||
NumberStyle::LowerRoman => "LowerRoman",
|
||||
NumberStyle::UpperLetters => "UpperLetters",
|
||||
@@ -542,17 +542,55 @@ pub fn pt(x: f64, y: f64) -> String {
|
||||
format!("{} {}", num(x), num(y))
|
||||
}
|
||||
|
||||
// ---------- list numbering ----------
|
||||
|
||||
/// A paragraph's `NumberingFormat` ("A, B, C, D...", "001, 002, 003...", …), told apart by its
|
||||
/// first item. None for formats with no counterpart (CJK, Arabic, Hebrew, …).
|
||||
pub fn numbering_format_in(s: &str) -> Option<NumberStyle> {
|
||||
Some(match s.split(',').next().unwrap_or("").trim() {
|
||||
"1" => NumberStyle::Arabic,
|
||||
"01" => NumberStyle::ArabicLeadingZero,
|
||||
"001" => NumberStyle::ArabicThreeDigits,
|
||||
"0001" => NumberStyle::ArabicFourDigits,
|
||||
"I" => NumberStyle::UpperRoman,
|
||||
"i" => NumberStyle::LowerRoman,
|
||||
"A" => NumberStyle::UpperLetters,
|
||||
"a" => NumberStyle::LowerLetters,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
pub fn numbering_format_out(n: NumberStyle) -> &'static str {
|
||||
match n {
|
||||
NumberStyle::Arabic | NumberStyle::Symbols => "1, 2, 3, 4...",
|
||||
NumberStyle::ArabicLeadingZero => "01, 02, 03...",
|
||||
NumberStyle::ArabicThreeDigits => "001, 002, 003...",
|
||||
NumberStyle::ArabicFourDigits => "0001, 0002, 0003...",
|
||||
NumberStyle::UpperRoman => "I, II, III, IV...",
|
||||
NumberStyle::LowerRoman => "i, ii, iii, iv...",
|
||||
NumberStyle::UpperLetters => "A, B, C, D...",
|
||||
NumberStyle::LowerLetters => "a, b, c, d...",
|
||||
}
|
||||
}
|
||||
/// Label text in IDML metacharacters (a tab is `^t`).
|
||||
pub fn list_text_out(s: &str) -> String {
|
||||
s.replace('\t', "^t")
|
||||
}
|
||||
|
||||
// ---------- footnote options ----------
|
||||
|
||||
pub fn note_style_out(n: NumberStyle) -> &'static str {
|
||||
match n {
|
||||
NumberStyle::ArabicLeadingZero => "SingleLeadingZeros",
|
||||
NumberStyle::ArabicThreeDigits => "DoubleLeadingZeros",
|
||||
NumberStyle::ArabicFourDigits => "TripleLeadingZeros",
|
||||
n => number_style_out(n),
|
||||
}
|
||||
}
|
||||
pub fn note_style_in(s: &str) -> NumberStyle {
|
||||
match s {
|
||||
"SingleLeadingZeros" | "DoubleLeadingZeros" => NumberStyle::ArabicLeadingZero,
|
||||
"SingleLeadingZeros" => NumberStyle::ArabicLeadingZero,
|
||||
"DoubleLeadingZeros" => NumberStyle::ArabicThreeDigits,
|
||||
"TripleLeadingZeros" => NumberStyle::ArabicFourDigits,
|
||||
"Asterisks" => NumberStyle::Symbols,
|
||||
s => number_style_in(s),
|
||||
}
|
||||
|
||||
@@ -1967,3 +1967,46 @@ fn split_columns_import_and_round_trip() {
|
||||
let b = para(&import_idml(&export_idml(&doc)).unwrap());
|
||||
assert_eq!((b.span_columns, b.split_inside_gutter, b.split_outside_gutter), (a.span_columns, a.split_inside_gutter, a.split_outside_gutter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_numbering_format_expression_and_bullet_import_and_round_trip() {
|
||||
let styles = STYLES.replace(
|
||||
" </ParagraphStyleGroup>",
|
||||
r#" <ParagraphStyle Self="ParagraphStyle/Text%3aHead" Name="Text:Head" BulletsAndNumberingListType="NumberedList" NumberingExpression="^#.^t">
|
||||
<Properties><NumberingFormat type="string">A, B, C, D...</NumberingFormat></Properties>
|
||||
</ParagraphStyle>
|
||||
<ParagraphStyle Self="ParagraphStyle/Text%3aTable" Name="Text:Table" BulletsAndNumberingListType="NumberedList" NumberingExpression="Tabel ^#^t">
|
||||
<Properties><NumberingFormat type="string">001, 002, 003...</NumberingFormat></Properties>
|
||||
</ParagraphStyle>
|
||||
<ParagraphStyle Self="ParagraphStyle/Text%3aKanji" Name="Text:Kanji" BulletsAndNumberingListType="NumberedList">
|
||||
<Properties><NumberingFormat type="string">一, 二, 三, 四...</NumberingFormat></Properties>
|
||||
</ParagraphStyle>
|
||||
<ParagraphStyle Self="ParagraphStyle/Text%3aPoint" Name="Text:Point" BulletsAndNumberingListType="BulletList" BulletsTextAfter="^>">
|
||||
<Properties><BulletChar BulletCharacterType="UnicodeOnly" BulletCharacterValue="9632"/></Properties>
|
||||
</ParagraphStyle>
|
||||
</ParagraphStyleGroup>"#,
|
||||
);
|
||||
let bytes = 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),
|
||||
]);
|
||||
let check = |d: &Document| {
|
||||
let (head, _) = d.styles.resolve_para_style("Text/Head");
|
||||
assert_eq!(head.number_style, designcraft_doc::NumberStyle::UpperLetters);
|
||||
assert_eq!(head.number_label(2), "B.\t");
|
||||
let (table, _) = d.styles.resolve_para_style("Text/Table");
|
||||
assert_eq!(table.number_label(7), "Tabel 007\t");
|
||||
let (kanji, _) = d.styles.resolve_para_style("Text/Kanji");
|
||||
assert_eq!(kanji.number_style, designcraft_doc::NumberStyle::Arabic);
|
||||
let (point, _) = d.styles.resolve_para_style("Text/Point");
|
||||
assert_eq!(point.bullet_label(), "\u{25A0}\u{2002}");
|
||||
};
|
||||
let d = import_idml(&bytes).unwrap();
|
||||
check(&d);
|
||||
check(&import_idml(&export_idml(&d)).unwrap());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user