use crate::io::{read_file, ApiResult};
use crate::prelude::PathBuf;
use crate::util::{Label, StringConversion};
use bon::Builder;
use color_eyre::eyre::eyre;
use core::fmt;
use core::iter::once;
use core::str::from_utf8;
use quick_xml::events::{BytesStart, Event};
use quick_xml::{Reader, Writer};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use tracing::{debug, error};
const XML_DECLARATION: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
pub trait XmlRels {
fn largest_revision_identifier(&self) -> Option<u32> {
None
}
fn revision_identifiers(&self) -> Vec<u32> {
vec![]
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Capitalization {
#[default]
#[serde(rename = "none")]
NoCap,
All,
Small,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Effect {
#[serde(rename(serialize = "a:blur", deserialize = "blur"))]
Blur {
#[serde(rename = "@rad")]
radius: Option<String>,
#[serde(rename = "@grow")]
grow_bounds: Option<String>,
},
#[serde(rename(serialize = "a:glow", deserialize = "glow"))]
Glow,
#[serde(rename(serialize = "a:reflection", deserialize = "reflection"))]
Reflection,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum LineFill {
#[default]
#[serde(rename(serialize = "a:noFill", deserialize = "noFill"))]
NoFill,
#[serde(rename(serialize = "a:solidFill", deserialize = "solidFill"))]
Solid,
#[serde(rename(serialize = "a:gradFill", deserialize = "gradFill"))]
Gradient,
#[serde(rename(serialize = "a:pattFill", deserialize = "pattFill"))]
Pattern,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Strikethrough {
#[default]
#[serde(rename = "noStrike")]
NoStrike,
#[serde(rename = "sngStrike")]
Single,
#[serde(rename = "dblStrike")]
Double,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TextUnderline {
#[serde(rename(serialize = "a:uLnTx", deserialize = "uLnTx"))]
FollowsText,
#[serde(rename(serialize = "a:uLn", deserialize = "uLn"))]
Stroke,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BulletCharacter {
#[serde(rename = "@char")]
pub character: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BulletColor {
#[serde(rename(serialize = "a:srgbClr", deserialize = "srgbClr"))]
pub color: Color,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BulletFont {
#[serde(rename = "@typeface")]
pub typeface: String,
#[serde(rename = "@panose")]
pub panose: String,
#[serde(rename = "@pitchFamily")]
pub similar_font_family: String,
#[serde(rename = "@charset")]
pub charset: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Color {
#[serde(rename = "@val")]
pub value: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EffectContainer {
#[serde(rename = "$value")]
pub effect: Option<Vec<Effect>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FontComplexScript {
#[serde(rename = "@typeface")]
pub typeface: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FontEastAsian {
#[serde(rename = "@typeface")]
pub typeface: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FontSymbol {
#[serde(rename = "@typeface")]
pub typeface: String,
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Line {
#[serde(rename = "$value")]
line_fill: LineFill,
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
#[builder(start_fn = init)]
pub struct Relationships {
#[builder(default = vec![])]
pub relationship: Vec<Relationship>,
#[builder(default = "http://schemas.openxmlformats.org/package/2006/relationships".to_string())]
#[serde(rename = "@xmlns")]
pub namespace: String,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
#[builder(start_fn = init)]
pub struct Relationship {
#[serde(rename = "@Id")]
pub id: String,
#[builder(default = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide".to_string())]
#[serde(rename = "@Type")]
pub relationship_type: String,
#[serde(rename = "@Target")]
pub target: String,
#[serde(rename = "@TargetMode")]
pub target_mode: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename = "p:presentation")]
pub struct Presentation {
#[serde(rename = "p:sldIdLst")]
pub slide_id_list: Option<SlideIdList>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SlideIdList {
#[serde(rename = "p:sldId")]
pub slide_ids: Vec<SlideId>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SlideId {
#[serde(rename = "@id")]
pub id: u32,
#[serde(rename = "@r:id")]
pub relationship_id: String,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
pub struct TextCharacterProperties {
#[serde(rename = "@baseline")]
pub baseline: Option<String>,
#[serde(rename = "@b")]
pub bold: Option<String>,
#[serde(rename = "@cap")]
pub capitalization: Option<Capitalization>,
#[builder(default = "0".to_string())]
#[serde(rename = "@dirty")]
pub dirty: String,
#[serde(rename = "@i")]
pub italic: Option<String>,
#[builder(default = "0".to_string())]
#[serde(rename = "@kern")]
pub kerning: String,
#[serde(rename = "@kumimoji")]
pub kumimoji: Option<String>,
#[serde(rename = "@lang")]
pub language: Option<String>,
#[serde(rename = "@noProof")]
pub no_proofing: Option<String>,
#[serde(rename = "@normalizeH")]
pub normalize_heights: Option<String>,
#[serde(rename = "@spc")]
pub spacing: Option<String>,
#[serde(rename = "@sz")]
pub size: Option<String>,
#[serde(rename = "@strike")]
pub strikethrough: Option<Strikethrough>,
#[serde(rename = "@u")]
pub underline: Option<String>,
#[serde(rename(serialize = "a:ln", deserialize = "ln"))]
pub line: Option<Line>,
#[serde(rename(serialize = "a:effectlst", deserialize = "effectLst"))]
pub effect_list: Option<EffectContainer>,
#[serde(rename(serialize = "a:uLnTx", deserialize = "uLnTx"))]
pub underline_follows_text: Option<UnderlineFollowsText>,
#[serde(rename(serialize = "a:uLn", deserialize = "uLn"))]
pub underline_stroke: Option<UnderlineStroke>,
#[serde(rename(serialize = "a:uFillTx", deserialize = "uFillTx"))]
pub underline_fill_properties_follow_text: Option<UnderlineFillPropertiesFollowText>,
#[serde(rename(serialize = "a:uFill", deserialize = "uFill"))]
pub underline_fill: Option<UnderlineFill>,
#[serde(rename(serialize = "a:cs", deserialize = "cs"))]
pub font_complex_script: Option<FontComplexScript>,
#[serde(rename(serialize = "a:ea", deserialize = "ea"))]
pub font_east_asian: Option<FontEastAsian>,
#[serde(rename(serialize = "a:sym", deserialize = "sym"))]
pub font_symbol: Option<FontSymbol>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
#[serde(rename = "a:p")]
pub struct TextParagraph {
#[builder(default = Vec::new())]
#[serde(rename(serialize = "a:pPr", deserialize = "pPr"))]
pub text_paragraph_properties: Vec<TextParagraphProperties>,
#[builder(default = Vec::new())]
#[serde(rename(serialize = "a:r", deserialize = "r"))]
pub text_run: Vec<TextRun>,
#[serde(rename(serialize = "a:endParaRPr", deserialize = "endParaRPr"))]
pub end_paragraph_run_properties: Option<TextCharacterProperties>,
}
#[skip_serializing_none]
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
pub struct TextParagraphProperties {
#[serde(rename = "@indent")]
#[builder(default = "0".to_string())]
pub indent: String,
#[serde(rename = "@marL")]
#[builder(default = "0".to_string())]
pub margin_left: String,
#[serde(rename(serialize = "a:buClr", deserialize = "buClr"))]
pub bullet_color: Option<BulletColor>,
#[serde(rename(serialize = "a:buFont", deserialize = "buFont"))]
pub bullet_font: Option<BulletFont>,
#[serde(rename(serialize = "a:buChar", deserialize = "buChar"))]
pub bullet_character: Option<BulletCharacter>,
#[serde(rename(serialize = "a:defRPr", deserialize = "defRPr"))]
pub default_text_run_properties: Option<TextRunPropertiesDefault>,
}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
pub struct TextRun {
#[builder(default = Vec::new())]
#[serde(rename(serialize = "a:rPr", deserialize = "rPr"))]
pub text_run_properties: Vec<TextCharacterProperties>,
#[builder(default = TextString::init().build())]
#[serde(rename(serialize = "a:t", deserialize = "t"))]
pub text: TextString,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TextRunPropertiesDefault {}
#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
#[builder(start_fn = init)]
pub struct TextString {
#[builder(default = "".to_string())]
#[serde(rename = "$text")]
pub value: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnderlineFill {}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnderlineFillPropertiesFollowText {}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnderlineFollowsText {}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnderlineStroke {}
impl XmlRels for Vec<Relationship> {
fn largest_revision_identifier(&self) -> Option<u32> {
self.revision_identifiers().iter().max().cloned()
}
fn revision_identifiers(&self) -> Vec<u32> {
self.clone()
.iter()
.filter_map(|x| x.id.clone().trim_start_matches("rId").to_string().parse::<u32>().ok())
.collect::<Vec<u32>>()
}
}
impl Default for Relationships {
fn default() -> Self {
Self::init().build()
}
}
impl fmt::Display for Relationships {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.to_string() {
| Ok(xml) => write!(f, "{}", xml),
| Err(e) => write!(f, "Error serializing Relationships: {}", e),
}
}
}
impl Relationships {
pub fn add_relationship(&self, value: Relationship) -> Relationships {
let Relationships { relationship, .. } = self;
let updated = relationship.clone().into_iter().chain(once(value)).collect::<Vec<_>>();
Relationships::init().relationship(updated).build()
}
pub fn largest_revision_identifier(&self) -> Option<u32> {
self.relationship.largest_revision_identifier()
}
pub fn to_string(&self) -> Result<String, quick_xml::de::DeError> {
let xml = quick_xml::se::to_string(self).map_err(|e| quick_xml::de::DeError::Custom(e.to_string()))?;
Ok(format!("{}{}", XML_DECLARATION, xml))
}
}
pub fn add_slide_to_presentation_xml(xml: &str, slide_identifier: u32, revision_identifier: u32) -> ApiResult<String> {
let mut reader = Reader::from_str(xml);
let mut writer = Writer::new(Vec::new());
let mut found_slide_list = false;
loop {
match reader.read_event() {
| Ok(Event::End(end)) if end.name().as_ref() == b"p:sldIdLst" => {
found_slide_list = true;
let mut slide = BytesStart::new("p:sldId");
let id = slide_identifier.to_string();
let relationship_id = format!("rId{revision_identifier}");
slide.push_attribute(("id", id.as_str()));
slide.push_attribute(("r:id", relationship_id.as_str()));
match writer.write_event(Event::Empty(slide)).and_then(|_| writer.write_event(Event::End(end))) {
| Ok(_) => {}
| Err(why) => break Err(eyre!("Failed to write presentation slide identifier — {why}")),
}
}
| Ok(Event::Eof) if found_slide_list => {
let output = writer.into_inner();
match String::from_utf8(output) {
| Ok(value) => break Ok(value),
| Err(why) => break Err(eyre!("Failed to decode presentation.xml as UTF-8 — {why}")),
}
}
| Ok(Event::Eof) => break Err(eyre!("Missing p:sldIdLst in presentation.xml")),
| Ok(event) => match writer.write_event(event) {
| Ok(_) => {}
| Err(why) => break Err(eyre!("Failed to write presentation.xml event — {why}")),
},
| Err(why) => break Err(eyre!("Failed to parse presentation.xml at byte {} — {why}", reader.buffer_position())),
}
}
}
pub fn update_relationship_target(content: &str, current_target: &str, target: &str) -> ApiResult<String> {
quick_xml::de::from_str::<Relationships>(content)
.map_err(|why| eyre!("Failed to parse slide relationships — {why}"))
.and_then(|rels| {
let updated = rels
.relationship
.into_iter()
.map(|rel| match rel.target == current_target {
| true => Relationship::init()
.id(rel.id)
.relationship_type(rel.relationship_type)
.target(target.to_string())
.maybe_target_mode(rel.target_mode)
.build(),
| false => rel,
})
.collect::<Vec<_>>();
Relationships::init()
.relationship(updated)
.build()
.to_string()
.map_err(|why| eyre!("Failed to serialize slide relationships — {why}"))
})
}
pub fn validate_xml_well_formed(content: &str) -> bool {
let mut reader = Reader::from_str(content);
match content.trim().is_empty() {
| true => false,
| false => {
let mut open_elements = Vec::new();
loop {
match reader.read_event() {
| Ok(Event::Start(event)) => open_elements.push(event.name().as_ref().to_vec()),
| Ok(Event::End(event)) => match open_elements.pop() {
| Some(name) if name == event.name().as_ref() => {}
| _ => break false,
},
| Ok(Event::Eof) => break open_elements.is_empty(),
| Ok(_) => {}
| Err(_) => break false,
}
}
}
}
}
pub fn prettify_xml(xml: &str) -> String {
let mut reader = Reader::from_str(xml);
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2);
loop {
match reader.read_event() {
| Ok(Event::Eof) => break,
| Ok(event) => match writer.write_event(event) {
| Ok(_) => {}
| Err(why) => {
error!("=> {} Cannot write XML event — {why}", Label::fail());
break;
}
},
| Err(why) => {
error!("=> {} Error at XML position {} — {why}", Label::fail(), reader.buffer_position());
break;
}
}
}
let output = writer.into_inner();
match from_utf8(&output) {
| Ok(value) => value.to_string(),
| Err(why) => {
error!("=> {} Cannot decode prettified XML as UTF-8 — {why}", Label::fail());
String::new()
}
}
}
pub fn read_xml_rel(path: PathBuf) -> Option<Relationships> {
match read_file(path.clone()) {
| Ok(content) => {
let parsed = quick_xml::de::from_str::<Relationships>(&content);
debug!("=> {} Relationships = {:#?}", Label::using(), parsed);
match parsed {
| Ok(value) => Some(value),
| Err(why) => {
error!(
path = path.to_absolute_string(),
"=> {} Cannot parse relationships - {why}",
Label::fail()
);
None
}
}
}
| Err(why) => {
error!(path = path.to_absolute_string(), "=> {} Cannot read xml.rels file - {why}", Label::fail());
None
}
}
}