use std::{borrow::Borrow, cell::RefCell};
use roxmltree::Node;
use rust_office_art::smartart::{SmartArtData, SmartArtDrawingKind, SmartArtDrawingNode};
use super::context::{ParseContext, ParsedStyleSheet};
use super::util::escape_xml_text;
fn child<'a, 'b>(node: Node<'a, 'b>, name: &str) -> Option<Node<'a, 'b>> {
node.children()
.find(|node| node.is_element() && node.tag_name().name() == name)
}
fn wrapped(name: &str, node: Node<'_, '_>, xml: &str) -> String {
let raw = &xml[node.range()];
let Some(end) = raw.find('>') else {
return String::new();
};
let opening = &raw[..end];
let attributes = opening
.find(char::is_whitespace)
.map(|offset| &opening[offset..])
.unwrap_or("")
.trim_end_matches('/');
let body = if opening.ends_with('/') {
""
} else {
&raw[end + 1..raw.rfind("</").unwrap_or(end + 1)]
};
format!("<{name}{attributes}>{body}</{name}>")
}
fn text_box(
xml: &str,
context: &ParseContext<'_, impl Borrow<ParsedStyleSheet>>,
) -> Option<String> {
let wrapped_xml = format!(
r#"<root xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram">{xml}</root>"#
);
let document = roxmltree::Document::parse(&wrapped_xml).ok()?;
let mut output = String::new();
if let Some(body) = document
.descendants()
.find(|node| node.is_element() && node.tag_name().name() == "bodyPr")
{
output.push_str(&wrapped("wps:bodyPr", body, &wrapped_xml));
}
output.push_str("<wps:txbx><w:txbxContent>");
for paragraph in document
.descendants()
.filter(|node| node.is_element() && node.tag_name().name() == "p")
{
output.push_str("<w:p>");
let properties = child(paragraph, "pPr");
if let Some(align) = properties.and_then(|node| node.attribute("algn")) {
let value = match align {
"ctr" => "center",
"r" => "right",
"just" | "dist" => "both",
_ => "left",
};
output.push_str(&format!(r#"<w:pPr><w:jc w:val="{value}"/></w:pPr>"#));
}
let defaults = properties.and_then(|node| child(node, "defRPr"));
for run in paragraph.children().filter(|node| node.is_element()) {
match run.tag_name().name() {
"br" => output.push_str("<w:r><w:br/></w:r>"),
"r" | "fld" => {
output.push_str("<w:r><w:rPr>");
let direct_style = child(run, "rPr");
let style = direct_style.or(defaults);
if let Some(style) = style {
let attribute_value = |name| {
direct_style
.and_then(|node| node.attribute(name))
.or_else(|| defaults.and_then(|node| node.attribute(name)))
};
if let Some(size) =
attribute_value("sz").and_then(|value| value.parse::<u32>().ok())
{
output.push_str(&format!(
r#"<w:sz w:val="{}"/>"#,
(size as f64 / 50.0).round().max(1.0)
));
}
for (attribute, tag) in [("b", "w:b"), ("i", "w:i")] {
if attribute_value(attribute)
.is_some_and(|value| matches!(value, "1" | "true"))
{
output.push_str(&format!("<{tag}/>"));
}
}
if let Some(font) = child(style, "latin")
.or_else(|| defaults.and_then(|node| child(node, "latin")))
.and_then(|node| node.attribute("typeface"))
{
let family = match font {
"+mj-lt" => context
.styles()
.theme_fonts
.major_latin
.as_deref()
.unwrap_or("Calibri"),
"+mn-lt" => context
.styles()
.theme_fonts
.minor_latin
.as_deref()
.unwrap_or("Calibri"),
_ => font,
};
output.push_str(&format!(
r#"<w:rFonts w:ascii="{}" w:hAnsi="{}"/>"#,
escape_xml_text(family),
escape_xml_text(family)
));
}
if let Some(color) = super::colors::resolve_drawing_color_from_xml(
Some(&wrapped_xml[style.range()]),
&context.styles().theme_colors,
)
.or_else(|| {
defaults.and_then(|node| {
super::colors::resolve_drawing_color_from_xml(
Some(&wrapped_xml[node.range()]),
&context.styles().theme_colors,
)
})
}) {
output.push_str(&format!(
r#"<w:color w:val="{}"/>"#,
color.color.trim_start_matches('#')
));
}
}
output.push_str("</w:rPr>");
for text in run
.children()
.filter(|node| node.is_element() && node.tag_name().name() == "t")
{
output.push_str(&format!(
r#"<w:t xml:space="preserve">{}</w:t>"#,
escape_xml_text(text.text().unwrap_or_default())
));
}
output.push_str("</w:r>");
}
_ => {}
}
}
output.push_str("</w:p>");
}
output.push_str("</w:txbxContent></wps:txbx>");
Some(output)
}
fn word_node(
node: &SmartArtDrawingNode,
data: &SmartArtData,
context: &ParseContext<'_, impl Borrow<ParsedStyleSheet>>,
remaining: &mut usize,
depth: usize,
) -> Option<String> {
if *remaining == 0 || depth > 64 {
return None;
}
*remaining -= 1;
let xml = &node.source_xml;
let document = roxmltree::Document::parse(xml).ok()?;
let root = document.root_element();
match node.kind {
SmartArtDrawingKind::Unknown => None,
SmartArtDrawingKind::Group => {
let properties = child(root, "grpSpPr")
.map(|node| wrapped("wpg:grpSpPr", node, xml))
.unwrap_or_default();
let mut output = format!("<wpg:grpSp>{properties}");
for nested in &node.children {
output.push_str(&word_node(nested, data, context, remaining, depth + 1)?);
}
output.push_str("</wpg:grpSp>");
Some(output)
}
SmartArtDrawingKind::Picture => {
let mut output = String::from("<pic:pic>");
for name in ["nvPicPr", "blipFill", "spPr"] {
if let Some(node) = child(root, name) {
output.push_str(&wrapped(&format!("pic:{name}"), node, xml));
}
}
output.push_str("</pic:pic>");
Some(output)
}
SmartArtDrawingKind::Shape | SmartArtDrawingKind::Connector => {
if child(root, "txXfrm").is_some() {
context.push_warning("SmartArt independent text-frame transforms are preserved but not applied to text layout");
}
let properties = wrapped("wps:spPr", child(root, "spPr")?, xml);
let style = child(root, "style")
.map(|node| wrapped("wps:style", node, xml))
.unwrap_or_default();
let semantic_text = node
.model_id
.as_deref()
.and_then(|id| data.semantic_point(id))
.and_then(|point| point.text_body_xml.as_deref());
let saved_text = child(root, "txBody").map(|_| xml.as_str());
let text = saved_text
.filter(|xml| xml.contains(":t>"))
.or(semantic_text)
.or(saved_text)
.and_then(|xml| text_box(xml, context))
.unwrap_or_default();
Some(format!("<wps:wsp>{properties}{style}{text}</wps:wsp>"))
}
}
}
pub(super) fn render_diagram(
run: &str,
width: f64,
height: f64,
context: &ParseContext<'_, impl Borrow<ParsedStyleSheet>>,
) -> Option<String> {
let references = crate::xml::extract_balanced_tag_slices(run, "dgm:relIds")
.into_iter()
.next()?;
let relationship = crate::xml::get_attribute(references, "r:dm")?;
let data_part = context.relationships.get(&relationship)?;
let data =
rust_office_art::smartart::parse_smartart_data(&context.parts.get(data_part)?.content)?;
let related = super::relationships::parse_relationships_from_parts(context.parts, data_part);
let drawing_part = data
.drawing_relationship_id
.as_deref()
.and_then(|id| related.get(id))
.cloned()
.or_else(|| {
crate::package::relationship_target_from_parts(
context.parts,
data_part,
"diagramDrawing",
)
});
if let Some(drawing_part) = drawing_part {
if let Some(drawing) = context
.parts
.get(&drawing_part)
.and_then(|part| rust_office_art::smartart::parse_smartart_drawing(&part.content))
{
let child_context = ParseContext {
relationships: super::relationships::parse_relationships_from_parts(
context.parts,
&drawing_part,
),
content_types: context.content_types.clone(),
parts: context.parts,
binary_assets: context.binary_assets,
style_sheet: context.styles(),
warnings: RefCell::new(Vec::new()),
};
let properties = drawing.group_properties_xml.as_deref().and_then(|xml| {
let document = roxmltree::Document::parse(xml).ok()?;
let transform = child(document.root_element(), "xfrm")?;
child(transform, "chOff")?;
child(transform, "chExt")?;
Some(wrapped("wpg:grpSpPr", document.root_element(), xml))
}).unwrap_or_else(|| format!(r#"<wpg:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{}" cy="{}"/><a:chOff x="0" y="0"/><a:chExt cx="{}" cy="{}"/></a:xfrm></wpg:grpSpPr>"#, width * 9525.0, height * 9525.0, width * 9525.0, height * 9525.0));
let mut remaining = 2048;
let nodes = drawing
.nodes
.iter()
.map(|node| word_node(node, &data, &child_context, &mut remaining, 0))
.collect::<Option<Vec<_>>>();
if let Some(nodes) = nodes {
let group = format!("<wpg:wgp>{properties}{}</wpg:wgp>", nodes.join(""));
if let Some(svg) = super::shapes::render_standalone_word_shape_svg(
&group,
Some(width),
Some(height),
&child_context,
) {
context
.warnings
.borrow_mut()
.extend(child_context.warnings.into_inner());
return Some(svg.replacen("<svg ", r#"<svg data-office-art="true" "#, 1));
}
}
}
}
context.push_warning("SmartArt cached drawing is unavailable or unsupported; displaying semantic text without native layout");
let paragraphs = data
.points
.iter()
.filter(|point| point.kind == "node" && !point.text.is_empty())
.take(512)
.map(|point| super::shapes::ParsedTextBoxParagraph {
text: point.text.clone(),
style: None,
align: None,
images: Vec::new(),
})
.collect::<Vec<_>>();
if paragraphs.is_empty() {
return None;
}
Some(
super::shapes::render_bounded_text_box_svg(¶graphs, width, height, None).replacen(
"<svg ",
r#"<svg data-office-art="true" "#,
1,
),
)
}