mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 10:03:51 +00:00
drawingml: what WordCraft and GridCraft read that DeckCraft's copy did not
Every ST_PresetColorVal name has its colour, in any case and with the dk/lt/med abbreviations; a sysClr without lastClr takes the usual value of its name; a Strict percentage (50%) reads anywhere a percentage does; a chart series keeps its c:f formulas and the writer uses them.
This commit is contained in:
@@ -245,6 +245,36 @@ pub struct Series {
|
||||
/// `c:errBars` on the values (y bars; a bar chart's run along its value axis).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_bars: Option<ErrorBars>,
|
||||
/// Where the series' data lives (`c:f` formulas), as the file gave them.
|
||||
#[serde(skip_serializing_if = "SeriesRefs::is_empty")]
|
||||
pub refs: SeriesRefs,
|
||||
}
|
||||
|
||||
/// The formulas (`c:f`) a series' data comes from, such as `Sheet1!$B$2:$B$5`. A writer uses the
|
||||
/// ones given and makes up the rest for an embedded workbook laid out one series per column
|
||||
/// (names in row 1, categories in column A). A caller that changes the data so that a given
|
||||
/// range no longer covers it clears that formula.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct SeriesRefs {
|
||||
/// `c:tx`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// `c:cat`, or a scatter or bubble series' `c:xVal`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub categories: Option<String>,
|
||||
/// `c:val` or `c:yVal`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<String>,
|
||||
/// `c:bubbleSize`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sizes: Option<String>,
|
||||
}
|
||||
|
||||
impl SeriesRefs {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
*self == SeriesRefs::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Error bars (`c:errBars`): how far each point's bar reaches below and above it.
|
||||
|
||||
@@ -387,9 +387,25 @@ fn read_series(s: &El, g: &El, group: (ChartType, bool, Option<bool>), categorie
|
||||
if let Some(b) = s.child(Local("bubbleSize")) {
|
||||
ser.sizes = numbers(b, budget);
|
||||
}
|
||||
let f = |e: Option<&El>| data_ref(e?);
|
||||
ser.refs = crate::chart::SeriesRefs {
|
||||
name: f(s.child(Local("tx"))),
|
||||
categories: f(s.child(Local("cat")).or_else(|| s.child(Local("xVal")))),
|
||||
values: f(s.child(Local("val")).or_else(|| s.child(Local("yVal")))),
|
||||
sizes: f(s.child(Local("bubbleSize"))),
|
||||
};
|
||||
ser
|
||||
}
|
||||
|
||||
/// The formula of a data source (`c:numRef`, `c:strRef` or `c:multiLvlStrRef`), if it has one.
|
||||
fn data_ref(e: &El) -> Option<String> {
|
||||
let r = e.elements().find(|r| matches!(r.local(), "numRef" | "strRef" | "multiLvlStrRef"))?;
|
||||
let f = r.child(Local("f"))?.text();
|
||||
let f = f.trim();
|
||||
// Formulas are short; a huge one is not kept.
|
||||
(!f.is_empty() && f.len() <= 8192).then(|| f.to_string())
|
||||
}
|
||||
|
||||
/// Axis scaling, titles, number formats and visibility. Returns the primary value axis and the
|
||||
/// category axis.
|
||||
fn read_axes<'a>(c: &mut Chart, pa: &'a El, primary_val: &Option<String>) -> (Option<&'a El>, Option<&'a El>) {
|
||||
|
||||
@@ -272,7 +272,9 @@ fn series(w: &mut Writer, c: &Chart, s: &Series, i: usize, kind: ChartType, g: G
|
||||
w.tag("c:idx").attr("val", i).empty();
|
||||
w.tag("c:order").attr("val", i).empty();
|
||||
w.open("c:tx", &[]);
|
||||
str_cache(w, &format!("Sheet1!${}$1", col(i + 1)), std::slice::from_ref(&s.name));
|
||||
let r = &s.refs;
|
||||
let or = |given: &Option<String>, made: String| given.clone().unwrap_or(made);
|
||||
str_cache(w, &or(&r.name, format!("Sheet1!${}$1", col(i + 1))), std::slice::from_ref(&s.name));
|
||||
w.close();
|
||||
if g == Group::Stock && s.line.is_none() {
|
||||
// A stock chart's series are points joined by the high-low lines, not lines of their own.
|
||||
@@ -334,15 +336,15 @@ fn series(w: &mut Writer, c: &Chart, s: &Series, i: usize, kind: ChartType, g: G
|
||||
if matches!(g, Group::Scatter | Group::Bubble) {
|
||||
let xs: Vec<Option<f64>> = if s.x_values.is_empty() { (1..=s.values.len()).map(|v| Some(v as f64)).collect() } else { s.x_values.clone() };
|
||||
w.open("c:xVal", &[]);
|
||||
num_cache(w, &format!("Sheet1!$A$2:$A${last}"), &xs);
|
||||
num_cache(w, &or(&r.categories, format!("Sheet1!$A$2:$A${last}")), &xs);
|
||||
w.close();
|
||||
w.open("c:yVal", &[]);
|
||||
num_cache(w, &format!("Sheet1!${0}$2:${0}${last}", col(i + 1)), &s.values);
|
||||
num_cache(w, &or(&r.values, format!("Sheet1!${0}$2:${0}${last}", col(i + 1))), &s.values);
|
||||
w.close();
|
||||
if g == Group::Bubble {
|
||||
let sizes: Vec<Option<f64>> = if s.sizes.is_empty() { s.values.iter().map(|_| Some(1.0)).collect() } else { s.sizes.clone() };
|
||||
w.open("c:bubbleSize", &[]);
|
||||
num_cache(w, &format!("Sheet1!${0}$2:${0}${last}", col(i + 2)), &sizes);
|
||||
num_cache(w, &or(&r.sizes, format!("Sheet1!${0}$2:${0}${last}", col(i + 2))), &sizes);
|
||||
w.close();
|
||||
w.tag("c:bubble3D").attr("val", 0).empty();
|
||||
} else {
|
||||
@@ -350,10 +352,10 @@ fn series(w: &mut Writer, c: &Chart, s: &Series, i: usize, kind: ChartType, g: G
|
||||
}
|
||||
} else {
|
||||
w.open("c:cat", &[]);
|
||||
str_cache(w, &format!("Sheet1!$A$2:$A${last}"), &c.categories);
|
||||
str_cache(w, &or(&r.categories, format!("Sheet1!$A$2:$A${last}")), &c.categories);
|
||||
w.close();
|
||||
w.open("c:val", &[]);
|
||||
num_cache(w, &format!("Sheet1!${0}$2:${0}${last}", col(i + 1)), &s.values);
|
||||
num_cache(w, &or(&r.values, format!("Sheet1!${0}$2:${0}${last}", col(i + 1))), &s.values);
|
||||
w.close();
|
||||
if g == Group::Line {
|
||||
w.tag("c:smooth").attr("val", if s.smooth { 1 } else { 0 }).empty();
|
||||
@@ -1144,4 +1146,23 @@ mod cache_cap_tests {
|
||||
let real = vec![(); 32_000];
|
||||
assert_eq!(capped(&real).len(), 32_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_formulas_are_read_and_written_back_and_made_up_when_absent() {
|
||||
let mut c = Chart { kind: ChartType::Bar, categories: vec!["a".into(), "b".into()], ..Default::default() };
|
||||
c.series.push(Series { name: "S".into(), values: vec![Some(1.0), Some(2.0)], ..Default::default() });
|
||||
let made = String::from_utf8(super::chart_xml(&c).0).unwrap();
|
||||
assert!(made.contains("<c:f>Sheet1!$B$2:$B$3</c:f>"), "{made}");
|
||||
let back = crate::chart::read::read_chart_xml(made.as_bytes()).unwrap();
|
||||
assert_eq!(back.series[0].refs.values.as_deref(), Some("Sheet1!$B$2:$B$3"));
|
||||
c.series[0].refs = crate::chart::SeriesRefs {
|
||||
name: Some("Data!$C$1".into()),
|
||||
categories: Some("Data!$A$2:$A$3".into()),
|
||||
values: Some("'My sheet'!$C$2:$C$3".into()),
|
||||
sizes: None,
|
||||
};
|
||||
let xml = String::from_utf8(super::chart_xml(&c).0).unwrap();
|
||||
assert!(xml.contains("<c:f>Data!$C$1</c:f>") && xml.contains("<c:f>Data!$A$2:$A$3</c:f>") && xml.contains("<c:f>'My sheet'!$C$2:$C$3</c:f>"));
|
||||
assert_eq!(crate::chart::read::read_chart_xml(xml.as_bytes()).unwrap().series[0].refs, c.series[0].refs);
|
||||
}
|
||||
}
|
||||
|
||||
+201
-25
@@ -485,34 +485,187 @@ pub const STANDARD_COLORS: [(Rgba, &str); 10] = [
|
||||
(Rgba::rgb(0x70, 0x30, 0xA0), "Purple"),
|
||||
];
|
||||
|
||||
/// A few CSS-like preset colour names (`prstClr`).
|
||||
/// A preset colour (`a:prstClr/@val`, `ST_PresetColorVal`): every name in [`PRESET_NAMES`], with
|
||||
/// the CSS colour of the same name. Case does not matter, and the `dk`, `lt` and `med`
|
||||
/// abbreviations are the `dark`, `light` and `medium` names (writers vary both).
|
||||
pub fn preset(name: &str) -> Option<Rgba> {
|
||||
let c = match name {
|
||||
"black" => (0, 0, 0),
|
||||
"white" => (255, 255, 255),
|
||||
"red" => (255, 0, 0),
|
||||
"green" => (0, 128, 0),
|
||||
"lime" => (0, 255, 0),
|
||||
"blue" => (0, 0, 255),
|
||||
"yellow" => (255, 255, 0),
|
||||
"cyan" | "aqua" => (0, 255, 255),
|
||||
"magenta" | "fuchsia" => (255, 0, 255),
|
||||
"gray" | "grey" => (128, 128, 128),
|
||||
"silver" => (192, 192, 192),
|
||||
"maroon" => (128, 0, 0),
|
||||
"navy" => (0, 0, 128),
|
||||
"olive" => (128, 128, 0),
|
||||
"purple" => (128, 0, 128),
|
||||
"teal" => (0, 128, 128),
|
||||
"orange" => (255, 165, 0),
|
||||
"pink" => (255, 192, 203),
|
||||
"brown" => (165, 42, 42),
|
||||
"gold" => (255, 215, 0),
|
||||
"ltGray" | "lightGray" => (211, 211, 211),
|
||||
"dkGray" | "darkGray" => (169, 169, 169),
|
||||
let mut lower = name.to_ascii_lowercase();
|
||||
for (short, long) in [("dk", "dark"), ("lt", "light"), ("med", "medium")] {
|
||||
if lower.starts_with(short) && !lower.starts_with(long) {
|
||||
lower.replace_range(..short.len(), long);
|
||||
}
|
||||
}
|
||||
let i = PRESET_RGB.binary_search_by(|(n, _)| (*n).cmp(lower.as_str())).ok()?;
|
||||
PRESET_RGB.get(i).map(|(_, v)| rgb24(*v))
|
||||
}
|
||||
|
||||
fn rgb24(v: u32) -> Rgba {
|
||||
Rgba::rgb((v >> 16) as u8, (v >> 8) as u8, v as u8)
|
||||
}
|
||||
|
||||
/// The preset colours by lower-case long name, sorted.
|
||||
const PRESET_RGB: [(&str, u32); 147] = [
|
||||
("aliceblue", 0xF0F8FF),
|
||||
("antiquewhite", 0xFAEBD7),
|
||||
("aqua", 0x00FFFF),
|
||||
("aquamarine", 0x7FFFD4),
|
||||
("azure", 0xF0FFFF),
|
||||
("beige", 0xF5F5DC),
|
||||
("bisque", 0xFFE4C4),
|
||||
("black", 0x000000),
|
||||
("blanchedalmond", 0xFFEBCD),
|
||||
("blue", 0x0000FF),
|
||||
("blueviolet", 0x8A2BE2),
|
||||
("brown", 0xA52A2A),
|
||||
("burlywood", 0xDEB887),
|
||||
("cadetblue", 0x5F9EA0),
|
||||
("chartreuse", 0x7FFF00),
|
||||
("chocolate", 0xD2691E),
|
||||
("coral", 0xFF7F50),
|
||||
("cornflowerblue", 0x6495ED),
|
||||
("cornsilk", 0xFFF8DC),
|
||||
("crimson", 0xDC143C),
|
||||
("cyan", 0x00FFFF),
|
||||
("darkblue", 0x00008B),
|
||||
("darkcyan", 0x008B8B),
|
||||
("darkgoldenrod", 0xB8860B),
|
||||
("darkgray", 0xA9A9A9),
|
||||
("darkgreen", 0x006400),
|
||||
("darkgrey", 0xA9A9A9),
|
||||
("darkkhaki", 0xBDB76B),
|
||||
("darkmagenta", 0x8B008B),
|
||||
("darkolivegreen", 0x556B2F),
|
||||
("darkorange", 0xFF8C00),
|
||||
("darkorchid", 0x9932CC),
|
||||
("darkred", 0x8B0000),
|
||||
("darksalmon", 0xE9967A),
|
||||
("darkseagreen", 0x8FBC8F),
|
||||
("darkslateblue", 0x483D8B),
|
||||
("darkslategray", 0x2F4F4F),
|
||||
("darkslategrey", 0x2F4F4F),
|
||||
("darkturquoise", 0x00CED1),
|
||||
("darkviolet", 0x9400D3),
|
||||
("deeppink", 0xFF1493),
|
||||
("deepskyblue", 0x00BFFF),
|
||||
("dimgray", 0x696969),
|
||||
("dimgrey", 0x696969),
|
||||
("dodgerblue", 0x1E90FF),
|
||||
("firebrick", 0xB22222),
|
||||
("floralwhite", 0xFFFAF0),
|
||||
("forestgreen", 0x228B22),
|
||||
("fuchsia", 0xFF00FF),
|
||||
("gainsboro", 0xDCDCDC),
|
||||
("ghostwhite", 0xF8F8FF),
|
||||
("gold", 0xFFD700),
|
||||
("goldenrod", 0xDAA520),
|
||||
("gray", 0x808080),
|
||||
("green", 0x008000),
|
||||
("greenyellow", 0xADFF2F),
|
||||
("grey", 0x808080),
|
||||
("honeydew", 0xF0FFF0),
|
||||
("hotpink", 0xFF69B4),
|
||||
("indianred", 0xCD5C5C),
|
||||
("indigo", 0x4B0082),
|
||||
("ivory", 0xFFFFF0),
|
||||
("khaki", 0xF0E68C),
|
||||
("lavender", 0xE6E6FA),
|
||||
("lavenderblush", 0xFFF0F5),
|
||||
("lawngreen", 0x7CFC00),
|
||||
("lemonchiffon", 0xFFFACD),
|
||||
("lightblue", 0xADD8E6),
|
||||
("lightcoral", 0xF08080),
|
||||
("lightcyan", 0xE0FFFF),
|
||||
("lightgoldenrodyellow", 0xFAFAD2),
|
||||
("lightgray", 0xD3D3D3),
|
||||
("lightgreen", 0x90EE90),
|
||||
("lightgrey", 0xD3D3D3),
|
||||
("lightpink", 0xFFB6C1),
|
||||
("lightsalmon", 0xFFA07A),
|
||||
("lightseagreen", 0x20B2AA),
|
||||
("lightskyblue", 0x87CEFA),
|
||||
("lightslategray", 0x778899),
|
||||
("lightslategrey", 0x778899),
|
||||
("lightsteelblue", 0xB0C4DE),
|
||||
("lightyellow", 0xFFFFE0),
|
||||
("lime", 0x00FF00),
|
||||
("limegreen", 0x32CD32),
|
||||
("linen", 0xFAF0E6),
|
||||
("magenta", 0xFF00FF),
|
||||
("maroon", 0x800000),
|
||||
("mediumaquamarine", 0x66CDAA),
|
||||
("mediumblue", 0x0000CD),
|
||||
("mediumorchid", 0xBA55D3),
|
||||
("mediumpurple", 0x9370DB),
|
||||
("mediumseagreen", 0x3CB371),
|
||||
("mediumslateblue", 0x7B68EE),
|
||||
("mediumspringgreen", 0x00FA9A),
|
||||
("mediumturquoise", 0x48D1CC),
|
||||
("mediumvioletred", 0xC71585),
|
||||
("midnightblue", 0x191970),
|
||||
("mintcream", 0xF5FFFA),
|
||||
("mistyrose", 0xFFE4E1),
|
||||
("moccasin", 0xFFE4B5),
|
||||
("navajowhite", 0xFFDEAD),
|
||||
("navy", 0x000080),
|
||||
("oldlace", 0xFDF5E6),
|
||||
("olive", 0x808000),
|
||||
("olivedrab", 0x6B8E23),
|
||||
("orange", 0xFFA500),
|
||||
("orangered", 0xFF4500),
|
||||
("orchid", 0xDA70D6),
|
||||
("palegoldenrod", 0xEEE8AA),
|
||||
("palegreen", 0x98FB98),
|
||||
("paleturquoise", 0xAFEEEE),
|
||||
("palevioletred", 0xDB7093),
|
||||
("papayawhip", 0xFFEFD5),
|
||||
("peachpuff", 0xFFDAB9),
|
||||
("peru", 0xCD853F),
|
||||
("pink", 0xFFC0CB),
|
||||
("plum", 0xDDA0DD),
|
||||
("powderblue", 0xB0E0E6),
|
||||
("purple", 0x800080),
|
||||
("red", 0xFF0000),
|
||||
("rosybrown", 0xBC8F8F),
|
||||
("royalblue", 0x4169E1),
|
||||
("saddlebrown", 0x8B4513),
|
||||
("salmon", 0xFA8072),
|
||||
("sandybrown", 0xF4A460),
|
||||
("seagreen", 0x2E8B57),
|
||||
("seashell", 0xFFF5EE),
|
||||
("sienna", 0xA0522D),
|
||||
("silver", 0xC0C0C0),
|
||||
("skyblue", 0x87CEEB),
|
||||
("slateblue", 0x6A5ACD),
|
||||
("slategray", 0x708090),
|
||||
("slategrey", 0x708090),
|
||||
("snow", 0xFFFAFA),
|
||||
("springgreen", 0x00FF7F),
|
||||
("steelblue", 0x4682B4),
|
||||
("tan", 0xD2B48C),
|
||||
("teal", 0x008080),
|
||||
("thistle", 0xD8BFD8),
|
||||
("tomato", 0xFF6347),
|
||||
("turquoise", 0x40E0D0),
|
||||
("violet", 0xEE82EE),
|
||||
("wheat", 0xF5DEB3),
|
||||
("white", 0xFFFFFF),
|
||||
("whitesmoke", 0xF5F5F5),
|
||||
("yellow", 0xFFFF00),
|
||||
("yellowgreen", 0x9ACD32),
|
||||
];
|
||||
|
||||
/// A system colour (`a:sysClr/@val`) for a file that gives no `lastClr`: the usual light-theme
|
||||
/// value of the common names; `None` for the others.
|
||||
pub fn system(name: &str) -> Option<Rgba> {
|
||||
let v = match name {
|
||||
"windowText" | "btnText" | "menuText" | "captionText" | "infoText" => 0x000000,
|
||||
"window" | "menu" | "highlightText" => 0xFFFFFF,
|
||||
"btnFace" | "3dLight" => 0xF0F0F0,
|
||||
"grayText" | "btnShadow" => 0xA0A0A0,
|
||||
"highlight" => 0x0078D7,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Rgba::rgb(c.0, c.1, c.2))
|
||||
Some(rgb24(v))
|
||||
}
|
||||
|
||||
/// Every preset colour name a file may write (`ST_PresetColorVal`, with the later editions' aliases).
|
||||
@@ -827,4 +980,27 @@ mod tests {
|
||||
assert_eq!(SchemeSlot::from_xml(slot.xml_name()), Some(slot));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_preset_name_has_a_colour_whatever_its_case_or_abbreviation() {
|
||||
for name in PRESET_NAMES {
|
||||
assert!(preset(name).is_some(), "{name}");
|
||||
}
|
||||
assert!(PRESET_RGB.windows(2).all(|w| w[0].0 < w[1].0));
|
||||
assert_eq!(preset("Red"), Some(Rgba::rgb(255, 0, 0)));
|
||||
assert_eq!(preset("dkBlue"), preset("darkBlue"));
|
||||
assert_eq!(preset("darkBlue"), Some(Rgba::rgb(0, 0, 0x8B)));
|
||||
assert_eq!(preset("ltGray"), Some(Rgba::rgb(0xD3, 0xD3, 0xD3)));
|
||||
assert_eq!(preset("medSeaGreen"), Some(Rgba::rgb(0x3C, 0xB3, 0x71)));
|
||||
assert_eq!(preset("green"), Some(Rgba::rgb(0, 0x80, 0)));
|
||||
assert_eq!(preset("notAColor"), None);
|
||||
assert_eq!(preset("dk"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_system_colours_have_a_value_without_last_clr() {
|
||||
assert_eq!(system("window"), Some(Rgba::WHITE));
|
||||
assert_eq!(system("btnFace"), Some(Rgba::rgb(0xF0, 0xF0, 0xF0)));
|
||||
assert_eq!(system("scrollBar"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,13 @@ pub fn pt(e: &El, name: &str) -> Option<f64> {
|
||||
e.attr_local(name).and_then(value::to_i64_lossy).map(emu_to_pt)
|
||||
}
|
||||
|
||||
/// 1/100 000 → fraction.
|
||||
/// 1/100 000 → fraction. Strict OOXML writes a percentage as `50%`.
|
||||
pub fn pct(e: &El, name: &str) -> Option<f64> {
|
||||
e.attr_local(name).and_then(value::to_f64).map(|v| v / 100_000.0)
|
||||
let v = e.attr_local(name)?.trim();
|
||||
match v.strip_suffix('%') {
|
||||
Some(p) => value::to_f64(p).map(|v| v / 100.0),
|
||||
None => value::to_f64(v).map(|v| v / 100_000.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn bullet_size_points(value: Option<&str>) -> Option<f64> {
|
||||
@@ -51,11 +55,11 @@ pub fn color_el(c: &El) -> Option<ColorRef> {
|
||||
"schemeClr" => ColorBase::Scheme { slot: c.attr_local("val").and_then(SchemeSlot::from_xml).unwrap_or(SchemeSlot::Tx1) },
|
||||
"sysClr" => ColorBase::System {
|
||||
name: c.attr_local("val").unwrap_or("windowText").to_string(),
|
||||
last: c.attr_local("lastClr").and_then(Rgba::from_hex).unwrap_or(if c.attr_local("val") == Some("window") {
|
||||
Rgba::WHITE
|
||||
} else {
|
||||
Rgba::BLACK
|
||||
}),
|
||||
last: c
|
||||
.attr_local("lastClr")
|
||||
.and_then(Rgba::from_hex)
|
||||
.or_else(|| c.attr_local("val").and_then(crate::color::system))
|
||||
.unwrap_or(Rgba::BLACK),
|
||||
},
|
||||
"prstClr" => ColorBase::Preset { name: c.attr_local("val").unwrap_or("black").to_string() },
|
||||
"scrgbClr" => {
|
||||
@@ -810,4 +814,12 @@ mod tests {
|
||||
let d = parse(br#"<a:bodyPr vert="wordArtVertRtl"/>"#).unwrap();
|
||||
assert_eq!(body_pr(&d).vert.map(|v| v.xml()), Some("wordArtVertRtl"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentages_read_in_either_spelling() {
|
||||
let d = parse(br#"<a:custDash xmlns:a="x"><a:ds d="400000" sp="300%"/><a:ds d="12.5%" sp="x"/></a:custDash>"#).unwrap();
|
||||
let ds: Vec<_> = d.children(Local("ds")).collect();
|
||||
assert_eq!((pct(ds[0], "d"), pct(ds[0], "sp")), (Some(4.0), Some(3.0)));
|
||||
assert_eq!((pct(ds[1], "d"), pct(ds[1], "sp")), (Some(0.125), None));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user