use std::io::{Seek, Write};
use std::path::Path;
use crate::Result;
use crate::format::DocumentFormat;
use crate::ir::*;
pub fn create_from_markdown(
markdown: &str,
format: DocumentFormat,
path: impl AsRef<Path>,
) -> Result<()> {
let ir = DocumentIR::from_markdown(markdown, format);
create_from_ir(&ir, format, path)
}
pub fn create_from_markdown_to_writer<W: Write + Seek>(
markdown: &str,
format: DocumentFormat,
writer: W,
) -> Result<()> {
let ir = DocumentIR::from_markdown(markdown, format);
create_from_ir_to_writer(&ir, format, writer)
}
pub fn create_from_ir(
ir: &DocumentIR,
format: DocumentFormat,
path: impl AsRef<Path>,
) -> Result<()> {
match format {
DocumentFormat::Docx => {
let writer = ir_to_docx(ir);
writer.save(path)?;
},
DocumentFormat::Xlsx => {
let writer = ir_to_xlsx(ir);
writer.save(path)?;
},
DocumentFormat::Pptx => {
let writer = ir_to_pptx(ir);
writer.save(path)?;
},
_ => return Err(crate::OfficeError::UnsupportedFormat(format!("{format:?}"))),
}
Ok(())
}
pub fn create_from_ir_to_writer<W: Write + Seek>(
ir: &DocumentIR,
format: DocumentFormat,
writer: W,
) -> Result<()> {
match format {
DocumentFormat::Docx => {
let w = ir_to_docx(ir);
w.write_to(writer)?;
},
DocumentFormat::Xlsx => {
let w = ir_to_xlsx(ir);
w.write_to(writer)?;
},
DocumentFormat::Pptx => {
let w = ir_to_pptx(ir);
w.write_to(writer)?;
},
_ => return Err(crate::OfficeError::UnsupportedFormat(format!("{format:?}"))),
}
Ok(())
}
pub fn ir_to_docx(ir: &DocumentIR) -> crate::docx::write::DocxWriter {
use crate::docx::write::{DocxWriter, IrParaProps, Run};
let mut writer = DocxWriter::new();
writer.set_metadata(&ir.metadata);
for section in &ir.sections {
if let Some(ref title) = section.title {
if !title.is_empty() {
let runs = [Run::new(title)];
let props = IrParaProps {
style: Some("Heading1".to_string()),
..Default::default()
};
writer.add_ir_paragraph(&runs, Some(props));
}
}
if let Some(ref hf) = section.header {
writer.add_section_header(
crate::docx::write::HfType::default_header(),
hf.content.clone(),
);
}
if let Some(ref hf) = section.footer {
writer.add_section_header(
crate::docx::write::HfType::default_footer(),
hf.content.clone(),
);
}
if let Some(ref hf) = section.first_page_header {
writer.add_section_header(
crate::docx::write::HfType::first_page_header(),
hf.content.clone(),
);
}
if let Some(ref hf) = section.first_page_footer {
writer.add_section_header(
crate::docx::write::HfType::first_page_footer(),
hf.content.clone(),
);
}
if let Some(ref hf) = section.even_page_header {
writer.add_section_header(
crate::docx::write::HfType::even_page_header(),
hf.content.clone(),
);
}
if let Some(ref hf) = section.even_page_footer {
writer.add_section_header(
crate::docx::write::HfType::even_page_footer(),
hf.content.clone(),
);
}
for elem in §ion.elements {
add_element_to_docx(&mut writer, elem);
}
if section.page_setup.is_some()
|| section.columns.is_some()
|| section.break_type != SectionBreakType::Continuous
{
writer.set_section_props(
section.page_setup.clone(),
section.columns.clone(),
section.break_type.clone(),
);
}
}
writer
}
fn add_element_to_docx(writer: &mut crate::docx::write::DocxWriter, elem: &Element) {
use crate::docx::write::{IrParaProps, Run};
match elem {
Element::Heading(h) => {
let level = h.level.clamp(1, 6);
let runs: Vec<Run> = ir_inline_to_runs(&h.content);
let props = IrParaProps {
style: Some(format!("Heading{level}")),
alignment: h.alignment.clone(),
..Default::default()
};
writer.add_ir_paragraph(&runs, Some(props));
},
Element::Paragraph(p) => {
let runs = ir_inline_to_runs(&p.content);
if runs
.iter()
.any(|r| !r.text.is_empty() || r.footnote_ref.is_some() || r.endnote_ref.is_some())
{
let props = IrParaProps {
alignment: p.alignment.clone(),
indent_left_twips: p.indent_left_twips,
indent_right_twips: p.indent_right_twips,
first_line_indent_twips: p.first_line_indent_twips,
space_before_twips: p.space_before_twips,
space_after_twips: p.space_after_twips,
line_spacing: p.line_spacing.clone(),
keep_with_next: p.keep_with_next,
keep_together: p.keep_together,
page_break_before: p.page_break_before,
background_color: p.background_color,
outline_level: p.outline_level,
border: p.border.clone(),
..Default::default()
};
writer.add_ir_paragraph(&runs, Some(props));
}
},
Element::Table(t) => {
writer.add_ir_table(t);
},
Element::List(l) => {
writer.add_ir_list(l);
},
Element::Image(img) => {
writer.add_ir_image(img);
},
Element::ThematicBreak => {
let border = crate::ir::ParagraphBorder {
top: None,
left: None,
right: None,
between: None,
bottom: Some(crate::ir::BorderLine {
style: crate::ir::BorderStyle::Single,
color: Some([0, 0, 0]),
size: Some(6),
space: Some(1),
}),
};
let props = IrParaProps {
border: Some(border),
..Default::default()
};
writer.add_ir_paragraph(&[], Some(props));
},
Element::PageBreak => {
writer.add_page_break();
},
Element::ColumnBreak => {
writer.add_column_break();
},
Element::TextBox(tb) => {
writer.add_text_box(tb);
},
Element::Footnote(n) => {
writer.add_footnote(n.id, &n.content);
},
Element::Endnote(n) => {
writer.add_endnote(n.id, &n.content);
},
Element::CodeBlock(cb) => {
writer.add_code_block(&cb.content);
},
Element::Shape(_) => {
},
}
}
fn ir_inline_to_runs(content: &[InlineContent]) -> Vec<crate::docx::write::Run> {
use crate::docx::write::Run;
let mut runs: Vec<Run> = Vec::new();
for item in content {
match item {
InlineContent::Text(span) => {
let mut run = Run::new(&span.text);
run.bold = span.bold;
run.italic = span.italic;
run.strikethrough = span.strikethrough;
run.font_name = span.font_name.clone();
run.font_size_half_pt = span.font_size_half_pt;
run.color_rgb = span.color;
run.underline_style = span.underline.clone();
run.highlight = span.highlight;
run.vertical_align = span.vertical_align.clone();
run.all_caps = span.all_caps;
run.small_caps = span.small_caps;
run.char_spacing_half_pt = span.char_spacing_half_pt;
runs.push(run);
},
InlineContent::LineBreak => {
runs.push(Run {
text: "\n".to_string(),
..Default::default()
});
},
InlineContent::FootnoteRef(r) => {
runs.push(Run {
footnote_ref: Some(r.note_id),
..Default::default()
});
},
InlineContent::EndnoteRef(r) => {
runs.push(Run {
endnote_ref: Some(r.note_id),
..Default::default()
});
},
}
}
coalesce_runs(runs)
}
fn coalesce_runs(runs: Vec<crate::docx::write::Run>) -> Vec<crate::docx::write::Run> {
use crate::docx::write::Run;
let mut out: Vec<Run> = Vec::with_capacity(runs.len());
for r in runs {
let mergeable = r.footnote_ref.is_none() && r.endnote_ref.is_none() && r.text != "\n";
if mergeable {
if let Some(last) = out.last_mut() {
if last.footnote_ref.is_none()
&& last.endnote_ref.is_none()
&& last.text != "\n"
&& run_props_equal(last, &r)
{
last.text.push_str(&r.text);
continue;
}
}
}
out.push(r);
}
out
}
fn run_props_equal(a: &crate::docx::write::Run, b: &crate::docx::write::Run) -> bool {
a.bold == b.bold
&& a.italic == b.italic
&& a.underline == b.underline
&& a.underline_style == b.underline_style
&& a.strikethrough == b.strikethrough
&& a.color == b.color
&& a.color_rgb == b.color_rgb
&& a.font_size_pt == b.font_size_pt
&& a.font_size_half_pt == b.font_size_half_pt
&& a.font_name == b.font_name
&& a.highlight == b.highlight
&& a.vertical_align == b.vertical_align
&& a.all_caps == b.all_caps
&& a.small_caps == b.small_caps
&& a.char_spacing_half_pt == b.char_spacing_half_pt
}
fn unique_sheet_name(raw: &str, idx: usize, used: &std::collections::HashSet<String>) -> String {
fn sanitise(s: &str) -> String {
let mut out = String::with_capacity(s.len().min(31));
for ch in s.chars() {
if matches!(ch, ':' | '\\' | '/' | '?' | '*' | '[' | ']') {
out.push('_');
} else {
out.push(ch);
}
if out.chars().count() >= 31 {
break;
}
}
out.trim().to_string()
}
let candidate = sanitise(raw);
if !candidate.is_empty()
&& !candidate.eq_ignore_ascii_case("history")
&& !used.contains(&candidate)
{
return candidate;
}
let mut fallback = format!("Sheet{idx}");
let mut bump = idx;
while used.contains(&fallback) {
bump += 1;
fallback = format!("Sheet{bump}");
}
fallback
}
pub fn ir_to_xlsx(ir: &DocumentIR) -> crate::xlsx::write::XlsxWriter {
use crate::xlsx::write::{CellData, CellStyle};
let mut writer = crate::xlsx::write::XlsxWriter::new();
writer.set_metadata(&ir.metadata);
let mut used_names: std::collections::HashSet<String> = std::collections::HashSet::new();
for (idx, section) in ir.sections.iter().enumerate() {
let raw_owned = section
.title
.clone()
.or_else(|| first_heading_text(§ion.elements))
.unwrap_or_default();
let raw = raw_owned.as_str();
let name = unique_sheet_name(raw, idx + 1, &used_names);
used_names.insert(name.clone());
let mut sheet = writer.add_sheet(&name);
if let Some(ps) = section.page_setup.as_ref() {
sheet.set_page_setup(crate::xlsx::write::PageSetup {
width_twips: ps.width_twips,
height_twips: ps.height_twips,
margin_top_twips: ps.margin_top_twips,
margin_bottom_twips: ps.margin_bottom_twips,
margin_left_twips: ps.margin_left_twips,
margin_right_twips: ps.margin_right_twips,
header_distance_twips: ps.header_distance_twips,
footer_distance_twips: ps.footer_distance_twips,
landscape: ps.landscape,
});
}
let mut row_cursor = 0usize;
let mut body_paragraphs_seen = false;
for elem in §ion.elements {
match elem {
Element::Table(t) => {
for (ci, &twips) in t.column_widths_twips.iter().enumerate() {
if twips > 0 {
let w = (twips as f64) * 96.0 / (1440.0 * 7.0);
sheet.set_column_width(ci, w.clamp(3.0, 80.0));
}
}
for row in &t.rows {
let mut col = 0usize;
for cell in &row.cells {
let text = cell_text(cell);
let data = text_to_cell_data(&text);
if let Some(style) =
xlsx_cell_style(row.is_header, cell.background_color)
{
sheet.set_cell_styled(row_cursor, col, data, style);
} else {
sheet.set_cell(row_cursor, col, data);
}
let cs = cell.col_span.max(1) as usize;
let rs = cell.row_span.max(1) as usize;
if cs > 1 || rs > 1 {
sheet.merge_cells(row_cursor, col, rs, cs);
}
col += cs;
}
row_cursor += 1;
}
},
Element::Paragraph(p) => {
let text = inline_to_text(&p.content);
if !text.is_empty() {
body_paragraphs_seen = true;
let mut style = CellStyle::new();
if let Some(size_pt) = crate::ir::first_inline_font_size_pt(&p.content) {
style = style.font_size(size_pt);
}
if let Some(name) = first_inline_font_name(&p.content) {
style = style.font_name(name);
}
for line in split_paragraph_for_xlsx(&text) {
sheet.set_cell_styled(
row_cursor,
0,
CellData::String(line),
style.clone(),
);
row_cursor += 1;
}
}
},
Element::Image(img) => {
if let (Some(data), Some(fmt)) = (&img.data, &img.format) {
let cx = img.display_width_emu.unwrap_or(3_000_000) as i64;
let cy = img.display_height_emu.unwrap_or(2_000_000) as i64;
sheet.add_image(data.clone(), fmt.extension(), 0, 0, cx, cy);
}
},
Element::TextBox(tb) => {
let x = tb.x_emu.unwrap_or(0);
let y = tb.y_emu.unwrap_or(0);
let cx = tb.width_emu.unwrap_or(0) as i64;
let cy = tb.height_emu.unwrap_or(0) as i64;
for inner in &tb.content {
if let Element::Image(img) = inner {
if let (Some(data), Some(fmt)) = (&img.data, &img.format) {
let icx = if cx > 0 {
cx
} else {
img.display_width_emu.unwrap_or(3_000_000) as i64
};
let icy = if cy > 0 {
cy
} else {
img.display_height_emu.unwrap_or(2_000_000) as i64
};
sheet.add_image(data.clone(), fmt.extension(), x, y, icx, icy);
}
}
}
},
Element::Heading(h) => {
let text = inline_to_text(&h.content);
if !text.is_empty() {
let data = CellData::String(text);
let mut style = CellStyle::new().bold();
if let Some(size_pt) = crate::ir::first_inline_font_size_pt(&h.content) {
style = style.font_size(size_pt);
}
if let Some(name) = first_inline_font_name(&h.content) {
style = style.font_name(name);
}
sheet.set_cell_styled(row_cursor, 0, data, style);
row_cursor += 1;
}
},
_ => {},
}
}
if body_paragraphs_seen {
sheet.set_column_width(0, 80.0);
}
}
writer
}
fn split_paragraph_for_xlsx(text: &str) -> Vec<String> {
const SHORT_THRESHOLD: usize = 80;
const TARGET_LINE_LEN: usize = 120;
const SCAN_BACK_CHARS: usize = 60;
if text.chars().count() <= SHORT_THRESHOLD {
return vec![text.to_string()];
}
let chars: Vec<(usize, char)> = text.char_indices().collect();
let total_chars = chars.len();
let total_bytes = text.len();
let mut chunks: Vec<String> = Vec::new();
let mut char_start: usize = 0;
while char_start < total_chars {
let remaining_chars = total_chars - char_start;
if remaining_chars <= TARGET_LINE_LEN {
let head_byte = chars[char_start].0;
let tail = text[head_byte..].trim();
if !tail.is_empty() {
chunks.push(tail.to_string());
}
break;
}
let min_break_char = char_start + TARGET_LINE_LEN;
let scan_back_char = min_break_char
.saturating_sub(SCAN_BACK_CHARS)
.max(char_start);
let mut break_char: Option<usize> = None;
for i in min_break_char..total_chars.saturating_sub(2) {
if chars[i].1 == '.' && chars[i + 1].1 == ' ' && chars[i + 2].1.is_ascii_uppercase() {
break_char = Some(i + 2); break;
}
}
if break_char.is_none() {
for i in scan_back_char..min_break_char.saturating_sub(2).max(scan_back_char) {
if i + 2 >= total_chars {
break;
}
if chars[i].1 == '.' && chars[i + 1].1 == ' ' && chars[i + 2].1.is_ascii_uppercase()
{
break_char = Some(i + 2);
}
}
}
if break_char.is_none() {
for i in min_break_char..total_chars {
if chars[i].1 == ' ' {
break_char = Some(i + 1);
break;
}
}
}
let next_char = break_char.unwrap_or(total_chars);
let head_byte = chars[char_start].0;
let tail_byte = if next_char >= total_chars {
total_bytes
} else {
chars[next_char].0
};
let head = text[head_byte..tail_byte].trim();
if !head.is_empty() {
chunks.push(head.to_string());
}
let mut cs = next_char;
while cs < total_chars && chars[cs].1 == ' ' {
cs += 1;
}
if cs <= char_start {
cs = char_start + 1;
}
char_start = cs;
}
if chunks.is_empty() {
chunks.push(text.to_string());
}
chunks
}
pub fn ir_to_pptx(ir: &DocumentIR) -> crate::pptx::write::PptxWriter {
let mut writer = crate::pptx::write::PptxWriter::new();
writer.set_metadata(&ir.metadata);
if let Some(ps) = ir.sections.iter().find_map(|s| s.page_setup.as_ref()) {
let cx = ps.width_twips as u64 * 914_400 / 1440;
let cy = ps.height_twips as u64 * 914_400 / 1440;
writer.set_presentation_size(cx, cy);
}
const MAX_SLIDES: usize = 250;
const MAX_PARAGRAPHS_PER_SLIDE: usize = 12;
if ir.sections.len() <= MAX_SLIDES {
for section in &ir.sections {
emit_pptx_slide_from_section(&mut writer, section);
}
} else {
emit_pptx_slides_compacted(&mut writer, ir, MAX_SLIDES, MAX_PARAGRAPHS_PER_SLIDE);
}
writer
}
fn emit_pptx_slide_from_section(writer: &mut crate::pptx::write::PptxWriter, section: &Section) {
let slide = writer.add_slide();
if let Some(ref title) = section.title {
if !title.is_empty() {
slide.set_title(title);
}
}
for elem in §ion.elements {
emit_pptx_element(slide, elem);
}
}
pub(crate) const PPTX_THEMATIC_BREAK_MARKER: &str = "\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}";
fn emit_pptx_element(slide: &mut crate::pptx::write::SlideData, elem: &Element) {
match elem {
Element::ThematicBreak => {
let runs = vec![crate::pptx::write::Run::new(PPTX_THEMATIC_BREAK_MARKER)];
slide.add_rich_text_aligned(&runs, Some(ParagraphAlignment::Center));
},
Element::Heading(h) => {
if slide.title.is_none() {
slide.set_title_aligned(&inline_to_text(&h.content), h.alignment.clone());
} else {
let runs = inline_to_pptx_runs(&h.content);
if !runs.is_empty() {
slide.add_rich_text_aligned(&runs, h.alignment.clone());
}
}
},
Element::Paragraph(p) => {
let runs = inline_to_pptx_runs(&p.content);
let space_before_hundredths_pt = p.space_before_twips.map(|t| t * 5);
let props = crate::pptx::write::ParaProps {
alignment: p.alignment.clone(),
space_before_hundredths_pt,
};
slide.add_rich_text_with_props(&runs, props);
},
Element::List(l) => {
let items: Vec<String> = l
.items
.iter()
.map(|i| {
i.content
.iter()
.map(|e| match e {
Element::Paragraph(p) => inline_to_text(&p.content),
_ => String::new(),
})
.collect::<Vec<_>>()
.join(" ")
})
.collect();
let item_refs: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
slide.add_bullet_list(&item_refs);
},
Element::Table(t) => {
let text = t
.rows
.iter()
.map(|row| {
row.cells
.iter()
.map(cell_text)
.collect::<Vec<_>>()
.join("\t")
})
.collect::<Vec<_>>()
.join("\n");
if !text.is_empty() {
slide.add_text(&text);
}
},
Element::Image(img) => {
if let (Some(data), Some(fmt)) = (&img.data, &img.format) {
let cx = img.display_width_emu.unwrap_or(3_000_000);
let cy = img.display_height_emu.unwrap_or(2_000_000);
slide.add_image(data.clone(), fmt.clone(), 0, 0, cx, cy);
}
},
Element::CodeBlock(cb) => {
let run = crate::pptx::write::Run::new(&cb.content).font("Courier New");
slide.add_rich_text(&[run]);
},
_ => {},
}
}
fn emit_pptx_slides_compacted(
writer: &mut crate::pptx::write::PptxWriter,
ir: &DocumentIR,
max_slides: usize,
max_paragraphs_per_slide: usize,
) {
type TitleWithAlgn = (String, Option<ParagraphAlignment>);
let mut groups: Vec<(Option<TitleWithAlgn>, Vec<Element>)> = Vec::new();
let mut current_title: Option<TitleWithAlgn> = None;
let mut current_elems: Vec<Element> = Vec::new();
let mut current_has_body = false;
let flush = |groups: &mut Vec<(Option<TitleWithAlgn>, Vec<Element>)>,
title: &mut Option<TitleWithAlgn>,
elems: &mut Vec<Element>| {
if !elems.is_empty() || title.is_some() {
groups.push((title.take(), std::mem::take(elems)));
}
};
fn is_body_content(elem: &Element) -> bool {
match elem {
Element::Paragraph(p) => p.content.iter().any(|ic| match ic {
InlineContent::Text(s) => !s.text.is_empty(),
_ => false,
}),
Element::List(_) | Element::CodeBlock(_) | Element::Table(_) => true,
_ => false,
}
}
for section in &ir.sections {
for elem in §ion.elements {
if let Element::Heading(h) = elem {
if h.level <= 2 {
let text = inline_to_text(&h.content);
let trimmed = text.trim();
if trimmed.is_empty() {
continue;
}
if !current_has_body {
if current_title.is_none() {
current_title = Some((trimmed.to_string(), h.alignment.clone()));
} else {
let mut span = TextSpan::plain(trimmed.to_string());
span.bold = true;
current_elems.push(Element::Paragraph(Paragraph {
content: vec![InlineContent::Text(span)],
alignment: h.alignment.clone(),
..Default::default()
}));
}
continue;
}
flush(&mut groups, &mut current_title, &mut current_elems);
current_has_body = false;
current_title = Some((trimmed.to_string(), h.alignment.clone()));
continue;
}
}
current_elems.push(elem.clone());
if is_body_content(elem) {
current_has_body = true;
}
}
}
flush(&mut groups, &mut current_title, &mut current_elems);
if groups.len() <= 1 {
let mut all_elems: Vec<Element> = Vec::new();
for section in &ir.sections {
for elem in §ion.elements {
all_elems.push(elem.clone());
}
}
groups = vec![(None, all_elems)];
}
struct PendingSlide {
title: Option<(String, Option<ParagraphAlignment>)>,
elements: Vec<Element>,
}
let mut pending: Vec<PendingSlide> = Vec::new();
for (title, elems) in groups {
let mut chunk: Vec<Element> = Vec::new();
let mut paragraph_count = 0usize;
let mut first_chunk = true;
for elem in elems {
let is_paragraph_like =
matches!(elem, Element::Paragraph(_) | Element::List(_) | Element::CodeBlock(_));
if is_paragraph_like && paragraph_count >= max_paragraphs_per_slide {
pending.push(PendingSlide {
title: if first_chunk { title.clone() } else { None },
elements: std::mem::take(&mut chunk),
});
paragraph_count = 0;
first_chunk = false;
}
if is_paragraph_like {
paragraph_count += 1;
}
chunk.push(elem);
}
if !chunk.is_empty() || (first_chunk && title.is_some()) {
pending.push(PendingSlide {
title: if first_chunk { title.clone() } else { None },
elements: chunk,
});
}
}
while pending.len() > max_slides {
let tail = pending.pop().expect("pending non-empty");
if let Some(prev) = pending.last_mut() {
prev.elements.extend(tail.elements);
} else {
pending.push(tail);
break;
}
}
for ps in pending {
let slide = writer.add_slide();
if let Some((t, algn)) = ps.title.as_ref() {
if !t.is_empty() {
slide.set_title_aligned(t, algn.clone());
}
}
for elem in &ps.elements {
emit_pptx_element(slide, elem);
}
}
}
fn inline_to_text(content: &[InlineContent]) -> String {
let mut out = String::new();
for item in content {
match item {
InlineContent::Text(span) => out.push_str(&span.text),
InlineContent::LineBreak => out.push('\n'),
InlineContent::FootnoteRef(_) | InlineContent::EndnoteRef(_) => {},
}
}
out
}
fn rgb_to_hex(rgb: [u8; 3]) -> String {
format!("{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2])
}
fn cell_text(cell: &TableCell) -> String {
cell.content
.iter()
.map(|e| match e {
Element::Paragraph(p) => inline_to_text(&p.content),
_ => String::new(),
})
.collect::<Vec<_>>()
.join(" ")
}
fn text_to_cell_data(text: &str) -> crate::xlsx::write::CellData {
use crate::xlsx::write::CellData;
if text.is_empty() {
CellData::Empty
} else if let Ok(n) = text.parse::<f64>() {
CellData::Number(n)
} else {
CellData::String(text.to_string())
}
}
fn first_heading_text(elements: &[Element]) -> Option<String> {
for el in elements {
if let Element::Heading(h) = el {
let text = inline_to_text(&h.content);
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
fn first_inline_font_name(content: &[InlineContent]) -> Option<String> {
for ic in content {
if let InlineContent::Text(span) = ic {
if let Some(name) = &span.font_name {
if !name.is_empty() {
return Some(name.clone());
}
}
}
}
None
}
fn xlsx_cell_style(is_header: bool, bg: Option<[u8; 3]>) -> Option<crate::xlsx::write::CellStyle> {
use crate::xlsx::write::CellStyle;
if is_header {
let bg_hex = bg.map(rgb_to_hex).unwrap_or_else(|| "D3D3D3".to_string());
Some(CellStyle::new().bold().background(bg_hex))
} else {
bg.map(|c| CellStyle::new().background(rgb_to_hex(c)))
}
}
fn inline_to_pptx_runs(content: &[InlineContent]) -> Vec<crate::pptx::write::Run> {
use crate::pptx::write::Run;
content
.iter()
.filter_map(|item| {
if let InlineContent::Text(span) = item {
if span.text.is_empty() {
return None;
}
let mut run = Run::new(&span.text);
if span.bold {
run = run.bold();
}
if span.italic {
run = run.italic();
}
if let Some(half_pt) = span.font_size_half_pt {
run = run.font_size(half_pt as f64 / 2.0);
}
if let Some(c) = span.color {
run = run.color(rgb_to_hex(c));
}
if let Some(ref name) = span.font_name {
run = run.font(name.clone());
}
Some(run)
} else {
None
}
})
.collect()
}