use std::collections::{HashMap, HashSet};
use roxmltree::{Document, Node as XmlNode, NodeId};
use super::keynote::{
reading_order, Geometry, Placed, Presentation, Slide, DEFAULT_SLIDE_HEIGHT, DEFAULT_SLIDE_WIDTH,
};
use super::ooxml::Package;
use super::pages::{Block, Comment, Formatting, Label, ListLabel, ListStyle, Paragraph};
use super::pages_xml::{
float_attr, int_attr, is_media, is_sf, legacy_formatting, legacy_geometry,
legacy_inherited_lists, legacy_list_styles, legacy_picture, legacy_runs, legacy_styles,
legacy_table, read_index_xml, sfa_attr, SF_NS,
};
use crate::error::ConversionError;
const KEY_NS: &str = "http://developer.apple.com/namespaces/keynote2";
fn is_key(node: XmlNode, name: &str) -> bool {
node.is_element()
&& node.tag_name().namespace() == Some(KEY_NS)
&& node.tag_name().name() == name
}
struct Styles {
characters: HashMap<String, Option<Formatting>>,
lists: HashMap<String, ListStyle>,
inherited: HashMap<String, String>,
}
pub(crate) fn read_content(
pkg: &mut Package,
member: &str,
) -> Result<Presentation, ConversionError> {
let xml = read_index_xml(pkg, member, "Keynote")?;
let dom = Document::parse(&xml)
.map_err(|e| ConversionError::Parse(format!("iwork: could not parse '{member}': {e}")))?;
let root = dom.root_element();
let styles = Styles {
characters: legacy_styles(root, "characterstyle", legacy_formatting),
lists: legacy_list_styles(root),
inherited: legacy_inherited_lists(root),
};
let slides = iter_slides(root)
.into_iter()
.map(|slide| read_slide(slide, pkg, &styles))
.collect();
let (width, height) = slide_size(root);
Ok(Presentation {
slides,
width,
height,
})
}
fn iter_slides<'a>(root: XmlNode<'a, 'a>) -> Vec<XmlNode<'a, 'a>> {
root.children()
.filter(|c| is_key(*c, "slide-list"))
.flat_map(|list| list.children().filter(|s| is_key(*s, "slide")))
.collect()
}
fn slide_size(root: XmlNode) -> (f64, f64) {
for child in root.children().filter(|c| is_key(*c, "size")) {
if let (Some(w), Some(h)) = (float_attr(child, "w"), float_attr(child, "h")) {
if w > 0.0 && h > 0.0 {
return (w, h);
}
}
}
(DEFAULT_SLIDE_WIDTH, DEFAULT_SLIDE_HEIGHT)
}
fn read_slide(slide: XmlNode, pkg: &mut Package, styles: &Styles) -> Slide {
let placed = slide_drawables(slide);
let geometries: Vec<Option<Geometry>> = placed.iter().map(|(_, _, g)| *g).collect();
let mut blocks = Vec::new();
let mut comments = Vec::new();
for position in reading_order(&geometries) {
let (element, label, geometry) = placed[position];
let (found, said) = drawable_blocks(element, label, pkg, styles);
blocks.extend(found.into_iter().map(|block| Placed { block, geometry }));
comments.extend(said);
}
Slide {
blocks,
notes: slide_notes(slide, styles),
comments,
}
}
fn slide_drawables<'a>(
slide: XmlNode<'a, 'a>,
) -> Vec<(XmlNode<'a, 'a>, Option<Label>, Option<Geometry>)> {
let placeholders = slide_placeholders(slide);
let mut placed = Vec::new();
let mut seen: HashSet<NodeId> = HashSet::new();
let mut add = |element: XmlNode<'a, 'a>, label: Option<Label>| {
if seen.insert(element.id()) {
placed.push((element, label, legacy_geometry(element)));
}
};
for drawable in iter_drawables(slide) {
if is_sf(drawable, "title-placeholder-ref") || is_sf(drawable, "body-placeholder-ref") {
if let Some((element, label)) =
placeholders.get(sfa_attr(drawable, "IDREF").unwrap_or(""))
{
add(*element, Some(*label));
}
continue;
}
add(drawable, None);
}
for (element, label) in placeholders.values() {
add(*element, Some(*label));
}
placed
}
fn slide_placeholders<'a>(slide: XmlNode<'a, 'a>) -> HashMap<String, (XmlNode<'a, 'a>, Label)> {
let mut found = HashMap::new();
for child in slide.children() {
let label = if is_key(child, "title-placeholder") {
Label::Title
} else if is_key(child, "body-placeholder") {
Label::Text
} else {
continue;
};
if let Some(identifier) = sfa_attr(child, "ID").filter(|id| !id.is_empty()) {
found.insert(identifier.to_string(), (child, label));
}
}
found
}
fn iter_drawables<'a>(slide: XmlNode<'a, 'a>) -> Vec<XmlNode<'a, 'a>> {
slide
.children()
.filter(|c| is_key(*c, "page"))
.flat_map(|page| page.children().filter(|c| is_sf(*c, "layers")))
.flat_map(|layers| layers.children().filter(|c| is_sf(*c, "layer")))
.flat_map(|layer| layer.children().filter(|c| is_sf(*c, "drawables")))
.flat_map(|group| group.children().filter(XmlNode::is_element))
.collect()
}
fn drawable_blocks(
element: XmlNode,
label: Option<Label>,
pkg: &mut Package,
styles: &Styles,
) -> (Vec<Block>, Vec<Comment>) {
if is_sf(element, "sticky-note") {
let text = sticky_note_text(element, styles);
let comments = if text.is_empty() {
Vec::new()
} else {
vec![Comment {
text,
anchor: String::new(),
}]
};
return (Vec::new(), comments);
}
if is_sf(element, "tabular-info") {
let table = element
.descendants()
.find(|n| is_sf(*n, "tabular-model"))
.and_then(legacy_table);
return (table.map(Block::Table).into_iter().collect(), Vec::new());
}
if is_media(element) {
let picture = legacy_picture(element, pkg);
return (
picture.map(Block::Picture).into_iter().collect(),
Vec::new(),
);
}
let paragraphs = element_paragraphs(element, label.unwrap_or(Label::Text), styles);
(
paragraphs.into_iter().map(Block::Paragraph).collect(),
Vec::new(),
)
}
fn element_paragraphs(element: XmlNode, label: Label, styles: &Styles) -> Vec<Paragraph> {
element
.descendants()
.filter(|n| is_sf(*n, "p"))
.filter_map(|para| {
let runs = legacy_runs(para, &styles.characters);
if runs.is_empty() {
return None;
}
Some(Paragraph {
runs,
label,
list: list_label(para, styles),
anchors: Vec::new(),
})
})
.collect()
}
fn list_label(paragraph: XmlNode, styles: &Styles) -> Option<ListLabel> {
let named = paragraph.attribute((SF_NS, "list-style")).or_else(|| {
styles
.inherited
.get(paragraph.attribute((SF_NS, "style")).unwrap_or(""))
.map(String::as_str)
})?;
let style = styles.lists.get(named)?;
let rung = int_attr(paragraph, "list-level").unwrap_or(0);
let mut label = style.label(rung)?;
label.depth = rung.saturating_sub(1);
Some(label)
}
fn sticky_note_text(element: XmlNode, styles: &Styles) -> String {
element
.descendants()
.filter(|n| is_sf(*n, "p"))
.map(|para| {
legacy_runs(para, &styles.characters)
.into_iter()
.map(|r| r.text)
.collect::<String>()
})
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string()
}
fn slide_notes(slide: XmlNode, styles: &Styles) -> Vec<Paragraph> {
slide
.children()
.filter(|c| is_key(*c, "notes"))
.flat_map(|notes| element_paragraphs(notes, Label::Text, styles))
.collect()
}