use anyhow::Result;
use serde_json::Value;
use std::io::Write;
use crate::chart::{generate_chart_drawing_xml, Chart, ThemeColors};
use crate::editable_svg::EditableSvgGroup;
use crate::html_paragraph::{HtmlParagraph, HtmlToParagraph};
use crate::image::{generate_image_drawing_xml, Image};
use crate::markdown::Markdown;
use crate::paragraph::Paragraph;
use crate::table::Table;
#[derive(Debug)]
pub enum ContentItem {
Paragraph(Paragraph),
HtmlParagraph(HtmlParagraph), Chart(Chart),
Image(Image),
EditableSvgGroup(EditableSvgGroup),
Markdown(Markdown),
Table(Table),
}
#[derive(Debug)]
pub struct Document {
contents: Vec<ContentItem>,
locale: String,
}
impl Document {
pub fn new(locale: &String) -> Self {
Self {
contents: Vec::new(),
locale: locale.to_owned(),
}
}
pub fn add_paragraph(&mut self, paragraph: Paragraph) {
let paragraph = paragraph.with_locale(self.locale.clone());
self.contents.push(ContentItem::Paragraph(paragraph));
}
pub fn add_html_paragraph(&mut self, html: HtmlParagraph) {
self.contents.push(ContentItem::HtmlParagraph(html));
}
pub fn add_chart(&mut self, chart: Chart) {
let chart = chart.with_locale(self.locale.clone());
self.contents.push(ContentItem::Chart(chart));
}
pub fn add_image(&mut self, image: Image) {
self.contents.push(ContentItem::Image(image));
}
pub fn add_editable_svg_group(&mut self, group: EditableSvgGroup) {
self.contents.push(ContentItem::EditableSvgGroup(group));
}
pub fn add_markdown(&mut self, markdown: Markdown) {
let markdown = markdown.with_locale(self.locale.clone());
self.contents.push(ContentItem::Markdown(markdown));
}
pub fn add_table(&mut self, table: Table) {
let table = table.with_locale(self.locale.clone());
self.contents.push(ContentItem::Table(table));
}
pub fn add_table_from_json(&mut self, datas: &Value, with_headers: bool) -> Result<()> {
let table = Table::from_json(datas, with_headers)?.with_locale(self.locale.clone());
self.add_table(table);
Ok(())
}
pub fn set_chart_colors(&mut self, colors: Vec<String>) -> Result<()> {
for item in self.contents.iter_mut().rev() {
if let ContentItem::Chart(chart) = item {
chart.set_colors(colors);
return Ok(());
}
}
Err(anyhow::anyhow!("No chart to set colors for"))
}
pub fn set_chart_colors_by_index(&mut self, index: usize, colors: Vec<String>) -> Result<()> {
let chart_count = self
.contents
.iter()
.filter(|item| matches!(item, ContentItem::Chart(_)))
.count();
if index >= chart_count {
return Err(anyhow::anyhow!("No chart at index {}", index));
}
let mut current_index = 0;
for item in &mut self.contents {
if let ContentItem::Chart(chart) = item {
if current_index == index {
chart.set_colors(colors);
return Ok(());
}
current_index += 1;
}
}
Err(anyhow::anyhow!("Chart index {} not found", index))
}
pub fn set_all_charts_colors(&mut self, colors: Vec<String>) {
for item in &mut self.contents {
if let ContentItem::Chart(chart) = item {
chart.set_colors(colors.clone());
}
}
}
pub fn use_default_theme_colors(&mut self) {
let theme = ThemeColors::default();
let colors: Vec<String> = theme.static_colors.iter().map(|&c| c.to_string()).collect();
self.set_all_charts_colors(colors);
}
pub fn save(&self, path: &str) -> Result<()> {
let file = std::fs::File::create(path)?;
let mut zip = zip::ZipWriter::new(file);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
macro_rules! log {
($msg:expr) => {
let _ = $msg;
};
}
let has_charts = self
.contents
.iter()
.any(|item| matches!(item, ContentItem::Chart(_)));
log!("add_directory: word/");
zip.add_directory("word/", options)?;
log!("add_directory: word/_rels/");
zip.add_directory("word/_rels/", options)?;
log!("add_directory: word/charts/");
zip.add_directory("word/charts/", options)?;
if has_charts {
log!("add_directory: word/charts/_rels/");
zip.add_directory("word/charts/_rels/", options)?;
log!("add_directory: word/embeddings/");
zip.add_directory("word/embeddings/", options)?;
}
log!("add_directory: word/media/");
zip.add_directory("word/media/", options)?;
log!("add_directory: _rels/");
zip.add_directory("_rels/", options)?;
log!("add_directory: word/theme/");
zip.add_directory("word/theme/", options)?;
log!("add_directory: docProps/");
zip.add_directory("docProps/", options)?;
log!("write: docProps/core.xml");
zip.start_file("docProps/core.xml", options)?;
zip.write_all(Self::generate_core_xml().as_bytes())?;
log!("write: docProps/app.xml");
zip.start_file("docProps/app.xml", options)?;
zip.write_all(Self::generate_app_xml().as_bytes())?;
log!("write: [Content_Types].xml");
zip.start_file("[Content_Types].xml", options)?;
let content_types_xml = self.generate_content_types_xml()?;
zip.write_all(content_types_xml.as_bytes())?;
log!("write: _rels/.rels");
zip.start_file("_rels/.rels", options)?;
let rels_root_xml = Self::generate_root_rels();
zip.write_all(rels_root_xml.as_bytes())?;
log!("write: word/settings.xml");
zip.start_file("word/settings.xml", options)?;
zip.write_all(include_bytes!("../assets/settings.xml"))?;
log!("write: word/fontTable.xml");
zip.start_file("word/fontTable.xml", options)?;
zip.write_all(include_bytes!("../assets/fontTable.xml"))?;
log!("write: word/theme/theme1.xml");
zip.start_file("word/theme/theme1.xml", options)?;
zip.write_all(self.generate_theme_xml().as_bytes())?;
log!("write: word/webSettings.xml");
zip.start_file("word/webSettings.xml", options)?;
zip.write_all(include_bytes!("../assets/webSettings.xml"))?;
log!("write: word/document.xml");
zip.start_file("word/document.xml", options)?;
let document_xml = self.generate_document_xml()?;
zip.write_all(document_xml.as_bytes())?;
log!("write: word/_rels/document.xml.rels");
zip.start_file("word/_rels/document.xml.rels", options)?;
let rels_xml = self.generate_document_rels()?;
zip.write_all(rels_xml.as_bytes())?;
log!("write: word/styles.xml");
zip.start_file("word/styles.xml", options)?;
zip.write_all(include_bytes!("../assets/styles.xml"))?;
log!("write: word/numbering.xml");
zip.start_file("word/numbering.xml", options)?;
zip.write_all(include_bytes!("../assets/numbering.xml"))?;
let charts: Vec<&Chart> = self
.contents
.iter()
.filter_map(|item| {
if let ContentItem::Chart(chart) = item {
Some(chart)
} else {
None
}
})
.collect();
for (i, chart) in charts.iter().enumerate() {
let chart_index = i + 1;
let chart_xml = chart.generate_chart_xml()?;
let chart_path = format!("word/charts/{}", chart.chart_part_name(chart_index));
log!(format!("write: {}", chart_path));
zip.start_file(&chart_path, options)?;
zip.write_all(chart_xml.as_bytes())?;
let chart_rels_path = format!(
"word/charts/_rels/{}.rels",
chart.chart_part_name(chart_index)
);
log!(format!("write: {}", chart_rels_path));
zip.start_file(&chart_rels_path, options)?;
zip.write_all(chart.generate_chart_rels(chart_index).as_bytes())?;
if let Some(chart_user_shapes_xml) = chart.generate_chart_user_shapes_xml() {
let chart_drawing_path = format!("word/drawings/drawing{}.xml", chart_index);
log!(format!("write: {}", chart_drawing_path));
zip.start_file(&chart_drawing_path, options)?;
zip.write_all(chart_user_shapes_xml.as_bytes())?;
}
if let Some(chart_style_xml) = chart.generate_chart_style_xml() {
let chart_style_path =
format!("word/charts/{}", chart.chart_style_part_name(chart_index));
log!(format!("write: {}", chart_style_path));
zip.start_file(&chart_style_path, options)?;
zip.write_all(chart_style_xml.as_bytes())?;
}
if let Some(chart_color_xml) = chart.generate_chart_color_style_xml() {
let chart_color_path =
format!("word/charts/{}", chart.chart_color_part_name(chart_index));
log!(format!("write: {}", chart_color_path));
zip.start_file(&chart_color_path, options)?;
zip.write_all(chart_color_xml.as_bytes())?;
}
let excel_path = format!(
"word/embeddings/Microsoft_Excel_Worksheet{}.xlsx",
chart_index
);
log!(format!("write: {}", excel_path));
zip.start_file(&excel_path, options)?;
zip.write_all(&chart.generate_excel_data()?)?;
}
let mut image_index = 1;
for item in &self.contents {
let image = match item {
ContentItem::Image(image) => Some(image),
_ => None,
};
let Some(image) = image else {
continue;
};
let image_path = format!("word/media/image{}.{}", image_index, image.get_extension());
log!(format!("write: {}", image_path));
zip.start_file(&image_path, options)?;
zip.write_all(&image.data)?;
image_index += 1;
}
log!("zip.finish()");
zip.finish()?;
Ok(())
}
fn generate_document_rels(&self) -> Result<String> {
let mut xml = String::new();
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
xml.push_str(r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml" />
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml" />
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml" />"#);
let mut next_rid = 5;
let mut img_index = 1;
let mut chart_index = 1;
for item in self.contents.iter() {
match item {
ContentItem::Image(image) => {
xml.push_str(&format!(r#"
<Relationship Id="rId{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image{}.{}"/>"#,
next_rid, img_index , image.get_extension()));
next_rid += 1;
img_index += 1;
}
ContentItem::Chart(chart) => {
xml.push_str(&format!(r#"
<Relationship Id="rId{}" Type="{}" Target="charts/{}"/>"#,
next_rid,
if chart.uses_chart_ex() {
"http://schemas.microsoft.com/office/2014/relationships/chartEx"
} else {
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
},
chart.chart_part_name(chart_index)
));
next_rid += 1;
chart_index += 1;
}
_ => (),
}
}
xml.push_str(
r#"
</Relationships>"#,
);
Ok(xml)
}
fn generate_document_xml(&self) -> Result<String> {
let mut xml = String::new();
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
xml.push_str(
r#"<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
<w:body>"#,
);
let mut index = 4;
let mut editable_shape_index = 0;
for item in &self.contents {
match item {
ContentItem::Paragraph(paragraph) => {
xml.push_str(¶graph.generate_paragraph_xml(Self::escape_xml));
}
ContentItem::HtmlParagraph(html) => {
let mut converter = HtmlToParagraph::new().with_locale(html.locale.clone());
if let Some(family) = &html.font_family {
converter = converter.with_font(family.clone());
}
if let Some(size) = html.font_size {
converter = converter.with_font_size(size);
}
if let Some(weight) = html.font_weight {
converter = converter.with_font_weight(weight);
}
if let Some(color) = &html.color {
converter = converter.with_color(color.clone());
}
if let Some(max_font_size) = html.max_font_size {
converter = converter.with_max_font_size(max_font_size);
}
xml.push_str(&converter.convert(&html.content));
}
ContentItem::Chart(chart) => {
index += 1;
xml.push_str(&generate_chart_drawing_xml(chart, index));
}
ContentItem::Image(image) => {
index += 1;
xml.push_str(&generate_image_drawing_xml(
index,
image.width,
image.height,
&image.alt_text,
&image.image_type,
));
}
ContentItem::EditableSvgGroup(group) => {
xml.push_str(&group.generate_xml_with_offset(editable_shape_index));
editable_shape_index += group.shape_count();
}
ContentItem::Markdown(markdown) => {
xml.push_str(&markdown.generate_openxml(Self::escape_xml));
}
ContentItem::Table(table) => {
xml.push_str(&table.generate_table_xml());
}
}
}
xml.push_str(r#"
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/>
<w:cols w:space="708"/>
<w:docGrid w:linePitch="360"/>
</w:sectPr>
</w:body>
</w:document>"#);
Ok(xml)
}
fn generate_content_types_xml(&self) -> Result<String> {
let chart_count = self
.contents
.iter()
.filter(|item| matches!(item, ContentItem::Chart(_)))
.count();
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
xml.push_str(
"<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">",
);
xml.push_str(" <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n");
xml.push_str(" <Default Extension=\"xml\" ContentType=\"application/xml\"/>\n");
xml.push_str(" <Default Extension=\"png\" ContentType=\"image/png\"/>\n");
xml.push_str(" <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n");
xml.push_str(" <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n");
xml.push_str(" <Default Extension=\"svg\" ContentType=\"image/svg+xml\"/>\n");
xml.push_str(" <Default Extension=\"xlsx\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"/>\n");
xml.push_str(" <Override PartName=\"/word/document.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml\"/>\n");
xml.push_str(" <Override PartName=\"/word/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml\"/>\n");
xml.push_str(" <Override PartName=\"/word/numbering.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml\"/>\n");
for (index, chart) in self
.contents
.iter()
.filter_map(|item| {
if let ContentItem::Chart(chart) = item {
Some(chart)
} else {
None
}
})
.enumerate()
{
xml.push_str(&format!(
" <Override PartName=\"/word/charts/{}\" ContentType=\"{}\"/>\n",
chart.chart_part_name(index + 1),
chart.chart_content_type()
));
if chart.uses_chart_ex() {
xml.push_str(&format!(
" <Override PartName=\"/word/charts/{}\" ContentType=\"application/vnd.ms-office.chartstyle+xml\"/>\n",
chart.chart_style_part_name(index + 1)
));
xml.push_str(&format!(
" <Override PartName=\"/word/charts/{}\" ContentType=\"application/vnd.ms-office.chartcolorstyle+xml\"/>\n",
chart.chart_color_part_name(index + 1)
));
}
if chart.has_chart_user_shapes() {
xml.push_str(&format!(
" <Override PartName=\"/word/drawings/drawing{}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml\"/>\n",
index + 1
));
}
}
for i in 0..chart_count {
xml.push_str(&format!(" <Override PartName=\"/word/embeddings/Microsoft_Excel_Worksheet{}.xlsx\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\"/>\n", i+1));
}
xml.push_str(" <Override PartName=\"/word/theme/theme1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.theme+xml\"/>\n");
xml.push_str(" <Override PartName=\"/word/settings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml\"/>\n");
xml.push_str(" <Override PartName=\"/word/fontTable.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml\"/>\n");
xml.push_str(" <Override PartName=\"/word/webSettings.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml\"/>\n");
xml.push_str(" <Override PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/>\n");
xml.push_str(" <Override PartName=\"/docProps/app.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.extended-properties+xml\"/>\n");
xml.push_str("</Types>");
Ok(xml)
}
fn generate_root_rels() -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
xml.push_str("<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">");
xml.push_str(r#"<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml" />"#);
xml.push_str(r#"<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml" />"#);
xml.push_str(r#"<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml" />"#);
xml.push_str("</Relationships>");
xml
}
fn generate_core_xml() -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
xml.push_str("<cp:coreProperties xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\"");
xml.push_str(" xmlns:dc=\"http://purl.org/dc/elements/1.1/\"");
xml.push_str(" xmlns:dcterms=\"http://purl.org/dc/terms/\"");
xml.push_str(" xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\"");
xml.push_str(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">");
xml.push_str("<dc:title>Document</dc:title>");
xml.push_str("<dc:creator>AlgForceAi</dc:creator>");
xml.push_str("<cp:lastModifiedBy>AlgForceAi</cp:lastModifiedBy>");
xml.push_str(
"<dcterms:created xsi:type=\"dcterms:W3CDTF\">2024-01-01T00:00:00Z</dcterms:created>",
);
xml.push_str(
"<dcterms:modified xsi:type=\"dcterms:W3CDTF\">2024-01-01T00:00:00Z</dcterms:modified>",
);
xml.push_str("</cp:coreProperties>");
xml
}
fn generate_app_xml() -> String {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
xml.push_str("<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\"");
xml.push_str(
" xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">",
);
xml.push_str("<Application>AlgForceAi</Application>");
xml.push_str("</Properties>");
xml
}
fn generate_theme_xml(&self) -> String {
let default_font = crate::get_font_for_locale(&self.locale);
format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Office Theme">
<a:themeElements>
<a:clrScheme name="Office">
<a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1>
<a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>
<a:dk2><a:srgbClr val="1F497D"/></a:dk2>
<a:lt2><a:srgbClr val="EEECE1"/></a:lt2>
<a:accent1><a:srgbClr val="4F81BD"/></a:accent1>
<a:accent2><a:srgbClr val="C0504D"/></a:accent2>
<a:accent3><a:srgbClr val="9BBB59"/></a:accent3>
<a:accent4><a:srgbClr val="8064A2"/></a:accent4>
<a:accent5><a:srgbClr val="4BACC6"/></a:accent5>
<a:accent6><a:srgbClr val="F79646"/></a:accent6>
<a:hlink><a:srgbClr val="0000FF"/></a:hlink>
<a:folHlink><a:srgbClr val="800080"/></a:folHlink>
</a:clrScheme>
<a:fontScheme name="Office">
<a:majorFont>
<a:latin typeface="{font}"/>
<a:ea typeface="{font}"/>
<a:cs typeface="{font}"/>
</a:majorFont>
<a:minorFont>
<a:latin typeface="{font}"/>
<a:ea typeface="{font}"/>
<a:cs typeface="{font}"/>
</a:minorFont>
</a:fontScheme>
<a:fmtScheme name="Office">
<a:fillStyleLst/>
<a:lnStyleLst/>
<a:effectStyleLst/>
<a:bgFillStyleLst/>
</a:fmtScheme>
</a:themeElements>
</a:theme>"#,
font = default_font
)
}
fn escape_xml(text: &str) -> String {
text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chart::{ChartData, ChartType, Series};
fn waterfall_chart() -> Chart {
let mut chart = Chart::new("Revenue Bridge".to_string());
chart.series_list = vec![Series {
name: "Revenue".to_string(),
chart_type: ChartType::Waterfall,
data: vec![
ChartData {
category: "Start".to_string(),
value: 100.0,
x_value: None,
bubble_size: None,
role: Some("total".to_string()),
},
ChartData {
category: "Gain".to_string(),
value: 20.0,
x_value: None,
bubble_size: None,
role: Some("increase".to_string()),
},
ChartData {
category: "Loss".to_string(),
value: -10.0,
x_value: None,
bubble_size: None,
role: Some("decrease".to_string()),
},
],
use_secondary_axis: false,
is_stacked: false,
is_area: false,
}];
chart
}
fn column_chart() -> Chart {
let mut chart = Chart::new("Sales".to_string());
chart.series_list = vec![Series {
name: "Series 1".to_string(),
chart_type: ChartType::Column,
data: vec![
ChartData {
category: "A".to_string(),
value: 10.0,
x_value: None,
bubble_size: None,
role: None,
},
ChartData {
category: "B".to_string(),
value: 20.0,
x_value: None,
bubble_size: None,
role: None,
},
],
use_secondary_axis: false,
is_stacked: false,
is_area: false,
}];
chart
}
#[test]
fn document_waterfall_uses_chartex_in_document_xml() {
let mut doc = Document::new(&"en".to_string());
doc.add_chart(waterfall_chart());
let xml = doc.generate_document_xml().unwrap();
assert!(xml.contains("http://schemas.microsoft.com/office/drawing/2014/chartex"));
assert!(xml.contains("<cx:chart"));
assert!(xml.contains("r:id=\"rId5\""));
assert!(!xml.contains(
"http://schemas.openxmlformats.org/drawingml/2006/chart\">\n <c:chart"
));
}
#[test]
fn document_waterfall_uses_chartex_relationship_type() {
let mut doc = Document::new(&"en".to_string());
doc.add_chart(waterfall_chart());
let rels = doc.generate_document_rels().unwrap();
assert!(rels.contains("http://schemas.microsoft.com/office/2014/relationships/chartEx"));
assert!(rels.contains("Target=\"charts/chartEx1.xml\""));
}
#[test]
fn document_non_waterfall_keeps_standard_chart_relationships() {
let mut doc = Document::new(&"en".to_string());
doc.add_chart(column_chart());
let xml = doc.generate_document_xml().unwrap();
let rels = doc.generate_document_rels().unwrap();
assert!(xml.contains("http://schemas.openxmlformats.org/drawingml/2006/chart"));
assert!(xml.contains("<c:chart"));
assert!(rels
.contains("http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"));
assert!(rels.contains("Target=\"charts/chart1.xml\""));
}
#[test]
fn document_mark_overlays_register_chart_shape_content_type() {
let mut chart = column_chart();
chart.theme = Some(serde_json::json!({
"marks": [{ "markType": "line", "axis": "y", "preset": "average" }]
}));
let mut doc = Document::new(&"en".to_string());
doc.add_chart(chart);
let content_types = doc.generate_content_types_xml().unwrap();
assert!(content_types.contains("/word/drawings/drawing1.xml"));
assert!(content_types
.contains("application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"));
}
}