use crate::entities::read_event_folding_entities;
use std::collections::HashMap;
use std::io::Cursor;
use quick_xml::events::Event as XmlEvent;
use quick_xml::Reader as XmlReader;
use zip::ZipArchive;
use super::common::read_zip_entry;
use super::md_blocks::BlockKind;
use super::md_slide_parse::parse_slide_for_markdown;
pub(super) fn extract_presentation_title(archive: &mut ZipArchive<Cursor<Vec<u8>>>) -> Option<String> {
use std::io::Read;
let mut entry = archive.by_name("docProps/core.xml").ok()?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf).ok()?;
let xml = std::str::from_utf8(&buf).ok()?;
let mut reader = XmlReader::from_str(xml);
let mut event_buf = Vec::new();
let mut in_title = false;
let mut title = String::new();
loop {
let mut spill = String::new();
let mut is_entity = false;
match read_event_folding_entities!(reader, &mut event_buf, &mut spill, &mut is_entity) {
Ok(XmlEvent::Start(ref e)) => {
let ename = e.name();
let ebytes = ename.as_ref();
let local: &[u8] = ebytes.rsplit(|b| *b == b':').next().unwrap_or(ebytes);
if local == b"title" {
in_title = true;
}
}
Ok(XmlEvent::Text(ref e)) if in_title => {
let s = e.decode().unwrap_or_default();
title = s.trim().to_string();
}
Ok(XmlEvent::End(ref e)) => {
let ename = e.name();
let ebytes = ename.as_ref();
let local: &[u8] = ebytes.rsplit(|b| *b == b':').next().unwrap_or(ebytes);
if local == b"title" {
in_title = false;
}
}
Ok(XmlEvent::Eof) | Err(_) => break,
_ => {}
}
event_buf.clear();
}
if title.is_empty() {
None
} else {
Some(title)
}
}
pub(super) fn parse_slide_rels(
archive: &mut ZipArchive<Cursor<Vec<u8>>>,
slide_name: &str,
) -> HashMap<String, String> {
parse_slide_rels_with_images(archive, slide_name).0
}
pub(super) fn parse_slide_rels_with_images(
archive: &mut ZipArchive<Cursor<Vec<u8>>>,
slide_name: &str,
) -> (HashMap<String, String>, HashMap<String, String>) {
let last_slash = match slide_name.rfind('/') {
Some(i) => i,
None => return (HashMap::new(), HashMap::new()),
};
let dir = &slide_name[..last_slash];
let file = &slide_name[last_slash + 1..];
let rels_path = format!("{}/_rels/{}.rels", dir, file);
let rels_bytes = match read_zip_entry(archive, &rels_path) {
Ok(b) => b,
Err(_) => return (HashMap::new(), HashMap::new()),
};
let xml = match std::str::from_utf8(&rels_bytes) {
Ok(s) => s.to_string(),
Err(_) => return (HashMap::new(), HashMap::new()),
};
let mut hyperlinks = HashMap::new();
let mut images = HashMap::new();
let mut reader = XmlReader::from_str(&xml);
let mut buf = Vec::new();
loop {
let mut spill = String::new();
let mut is_entity = false;
match read_event_folding_entities!(reader, &mut buf, &mut spill, &mut is_entity) {
Ok(XmlEvent::Empty(ref e)) | Ok(XmlEvent::Start(ref e)) => {
let ename = e.name();
let ebytes = ename.as_ref();
let local: &[u8] = ebytes.rsplit(|b| *b == b':').next().unwrap_or(ebytes);
if local == b"Relationship" {
let mut id = String::new();
let mut target = String::new();
let mut rel_type = String::new();
let mut is_external = false;
for attr in e.attributes().flatten() {
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
let val = String::from_utf8_lossy(attr.value.as_ref()).to_string();
match key.as_str() {
"Id" => id = val,
"Target" => target = val,
"Type" => rel_type = val,
"TargetMode" if val == "External" => is_external = true,
_ => {}
}
}
if rel_type.contains("hyperlink")
&& is_external
&& !id.is_empty()
&& !target.is_empty()
{
hyperlinks.insert(id, target);
} else if rel_type.ends_with("/image") && !id.is_empty() && !target.is_empty() {
let path = resolve_relative_path(dir, &target);
images.insert(id, path);
}
}
}
Ok(XmlEvent::Eof) | Err(_) => break,
_ => {}
}
buf.clear();
}
(hyperlinks, images)
}
pub(super) fn image_hash_name(bytes: &[u8], zip_path: &str) -> Option<String> {
let path = zip_path.to_ascii_lowercase();
crate::image_naming::name_for_path(bytes, &path)
}
pub(super) fn resolve_relative_path(base_dir: &str, relative: &str) -> String {
let mut parts: Vec<&str> = base_dir.split('/').collect();
for segment in relative.split('/') {
match segment {
".." => {
parts.pop();
}
"." | "" => {}
s => parts.push(s),
}
}
parts.join("/")
}
pub(super) fn extract_notes_text(
archive: &mut ZipArchive<Cursor<Vec<u8>>>,
slide_name: &str,
) -> Option<String> {
let last_slash = slide_name.rfind('/')?;
let dir = &slide_name[..last_slash];
let file = &slide_name[last_slash + 1..];
let rels_path = format!("{}/_rels/{}.rels", dir, file);
let rels_bytes = read_zip_entry(archive, &rels_path).ok()?;
let content = std::str::from_utf8(&rels_bytes).ok()?;
let notes_path = content
.split("<Relationship ")
.find(|chunk| chunk.contains("notesSlide") && !chunk.contains("notesSlideLayout"))
.and_then(|chunk| {
let start = chunk.find("Target=\"")? + 8;
let rest = &chunk[start..];
let end = rest.find('"')?;
Some(resolve_relative_path(dir, &rest[..end]))
})?;
let notes_buf = read_zip_entry(archive, ¬es_path).ok()?;
let notes_slide = parse_slide_for_markdown(¬es_buf, &HashMap::new()).ok()?;
let text: String = notes_slide
.blocks
.iter()
.filter(|b| matches!(b.kind, BlockKind::Paragraph | BlockKind::ListItem))
.map(|b| b.text.as_str())
.filter(|t| !t.trim().is_empty())
.collect::<Vec<_>>()
.join("\n");
if text.is_empty() {
None
} else {
Some(text)
}
}