use std::io::Cursor;
use zip::ZipArchive;
use super::common::read_zip_entry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum BlockKind {
Paragraph,
ListItem,
Table,
Image,
}
#[derive(Debug, Clone)]
pub(super) struct SlideBlock {
pub(super) kind: BlockKind,
pub(super) text: String,
pub(super) level: u8,
pub(super) is_numbered: bool,
pub(super) table_rows: Vec<Vec<String>>,
pub(super) table_has_header: bool,
pub(super) image_rid: Option<String>,
}
impl SlideBlock {
pub(super) fn paragraph(text: String) -> Self {
Self {
kind: BlockKind::Paragraph,
text,
level: 0,
is_numbered: false,
table_rows: Vec::new(),
table_has_header: false,
image_rid: None,
}
}
pub(super) fn list_item(text: String, level: u8, is_numbered: bool) -> Self {
Self {
kind: BlockKind::ListItem,
text,
level,
is_numbered,
table_rows: Vec::new(),
table_has_header: false,
image_rid: None,
}
}
pub(super) fn table(rows: Vec<Vec<String>>, has_header: bool) -> Self {
Self {
kind: BlockKind::Table,
text: String::new(),
level: 0,
is_numbered: false,
table_rows: rows,
table_has_header: has_header,
image_rid: None,
}
}
pub(super) fn image(alt: Option<String>, rid: Option<String>) -> Self {
let text = match alt.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
Some(a) => format!("[Image: {a}]"),
None => "[Image]".to_string(),
};
Self {
kind: BlockKind::Image,
text,
level: 0,
is_numbered: false,
table_rows: Vec::new(),
table_has_header: false,
image_rid: rid,
}
}
}
#[derive(Debug, Default)]
pub(super) struct SlideMarkdownContent {
pub title: Option<String>,
pub blocks: Vec<SlideBlock>,
pub notes: Option<String>,
pub diagram_rids: Vec<String>,
pub chart_rids: Vec<String>,
}
pub(super) fn append_diagram_blocks(
archive: &mut ZipArchive<Cursor<Vec<u8>>>,
slide_name: &str,
slide: &mut SlideMarkdownContent,
) {
let parts = super::diagram::resolve_diagram_parts(archive, slide_name, &slide.diagram_rids);
for part in parts {
if let Ok(bytes) = read_zip_entry(archive, &part) {
for text in super::diagram::parse_diagram_xml(&bytes) {
slide.blocks.push(SlideBlock::paragraph(text));
}
}
}
}
pub(super) fn append_chart_blocks(
archive: &mut ZipArchive<Cursor<Vec<u8>>>,
slide_name: &str,
slide: &mut SlideMarkdownContent,
) {
let parts = super::chart::resolve_chart_parts(archive, slide_name, &slide.chart_rids);
for part in parts {
if let Ok(bytes) = read_zip_entry(archive, &part) {
let rows = super::chart::parse_chart_xml(&bytes);
if !rows.is_empty() {
slide.blocks.push(SlideBlock::paragraph("Chart".to_string()));
slide.blocks.push(SlideBlock::table(rows, true));
}
}
}
}
pub(super) fn push_text(dst: &mut String, text: &str) {
let trimmed = text.trim();
if trimmed.is_empty() {
return;
}
if !dst.is_empty() {
dst.push(' ');
}
dst.push_str(trimmed);
}
pub(super) fn attr_local_name(key: &[u8]) -> &[u8] {
key.rsplit(|b| *b == b':').next().unwrap_or(key)
}
pub(super) fn decode_attr(attr: &quick_xml::events::attributes::Attribute<'_>) -> String {
match attr.unescape_value() {
Ok(v) => v.into_owned(),
Err(_) => String::from_utf8_lossy(attr.value.as_ref()).into_owned(),
}
}
pub(super) fn collapse_ws(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}