use std::collections::BTreeMap;
use std::path::Path;
use lo_calc::{evaluate_formula, Value};
use lo_core::{
parse_xml_document, serialize_xml_document, CellAddr, LoError, Result, Sheet, XmlItem, XmlNode,
};
use lo_zip::{normalize_zip_path, rels_path_for, resolve_part_target, ZipArchive};
pub fn docx_to_pdf_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let doc = lo_writer::from_docx_bytes("document", bytes)?;
lo_writer::save_as(&doc, "pdf")
}
pub fn doc_to_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let doc = lo_writer::from_doc_bytes("document", bytes)?;
lo_writer::save_as(&doc, "docx")
}
pub fn pptx_to_pdf_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let deck = lo_impress::from_pptx_bytes("presentation", bytes)?;
lo_impress::save_as(&deck, "pdf")
}
fn canonical_format_hint(format: &str) -> String {
let trimmed = format.trim();
let trimmed = trimmed.strip_prefix('.').unwrap_or(trimmed);
let head = trimmed.split(':').next().unwrap_or(trimmed).trim();
match head.to_ascii_lowercase().as_str() {
"text" => "txt".to_string(),
"markdown" => "md".to_string(),
"htm" => "html".to_string(),
"mml" => "mathml".to_string(),
"odfmath" | "odf-formula" => "odf".to_string(),
other => other.to_string(),
}
}
pub fn sniff_format_from_path(path: &str) -> Option<String> {
let ext = Path::new(path).extension()?.to_str()?;
Some(canonical_format_hint(ext))
}
pub fn writer_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let doc = lo_writer::load_bytes("document", input, &from)?;
lo_writer::save_as(&doc, &to)
}
pub fn calc_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let workbook = lo_calc::load_bytes("workbook", input, &from)?;
lo_calc::save_as(&workbook, &to)
}
pub fn impress_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let deck = lo_impress::load_bytes("presentation", input, &from)?;
lo_impress::save_as(&deck, &to)
}
pub fn draw_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let drawing = lo_draw::load_bytes("drawing", input, &from)?;
lo_draw::save_as(&drawing, &to)
}
pub fn math_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let document = lo_math::load_bytes("formula", input, &from)?;
lo_math::save_as(&document, &to)
}
pub fn base_convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
let database = lo_base::load_bytes("database", input, &from, None)?;
lo_base::save_as(&database, &to)
}
pub fn convert_bytes(input: &[u8], from: &str, to: &str) -> Result<Vec<u8>> {
let from = canonical_format_hint(from);
let to = canonical_format_hint(to);
match (from.as_str(), to.as_str()) {
("txt", "odp" | "pptx") => impress_convert_bytes(input, &from, &to),
("csv", "odb") => base_convert_bytes(input, &from, &to),
("txt" | "md" | "html" | "docx" | "doc" | "odt", _) => {
writer_convert_bytes(input, &from, &to)
}
("csv" | "xlsx" | "ods", _) => calc_convert_bytes(input, &from, &to),
("pptx" | "odp", _) => impress_convert_bytes(input, &from, &to),
("svg" | "odg", _) => draw_convert_bytes(input, &from, &to),
("latex" | "mathml" | "xml" | "odf", _) => math_convert_bytes(input, &from, &to),
("odb", _) => base_convert_bytes(input, &from, &to),
(other, _) => Err(LoError::Unsupported(format!(
"generic conversion source format not supported: {other}"
))),
}
}
pub fn convert_path_bytes(path: &str, input: &[u8], to: &str) -> Result<Vec<u8>> {
let from = sniff_format_from_path(path).ok_or_else(|| {
LoError::InvalidInput(format!(
"could not infer input format from path: {path}"
))
})?;
convert_bytes(input, &from, to)
}
pub fn docx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "html")
}
pub fn docx_to_txt_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "txt")
}
pub fn docx_to_odt_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "docx", "odt")
}
pub fn odt_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "pdf")
}
pub fn odt_to_docx_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "docx")
}
pub fn odt_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
writer_convert_bytes(input, "odt", "html")
}
pub fn xlsx_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "pdf")
}
pub fn xlsx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "html")
}
pub fn xlsx_to_csv_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "csv")
}
pub fn xlsx_to_ods_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "xlsx", "ods")
}
pub fn ods_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "pdf")
}
pub fn ods_to_xlsx_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "xlsx")
}
pub fn ods_to_csv_bytes(input: &[u8]) -> Result<Vec<u8>> {
calc_convert_bytes(input, "ods", "csv")
}
pub fn pptx_to_html_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "html")
}
pub fn pptx_to_svg_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "svg")
}
pub fn pptx_to_odp_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "pptx", "odp")
}
pub fn odp_to_pdf_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "odp", "pdf")
}
pub fn odp_to_pptx_bytes(input: &[u8]) -> Result<Vec<u8>> {
impress_convert_bytes(input, "odp", "pptx")
}
pub fn xlsx_recalc_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let zip = ZipArchive::new(bytes)?;
let workbook = lo_calc::from_xlsx_bytes("workbook", bytes)?;
let sheet_targets = parse_xlsx_sheet_targets(&zip)?;
let mut entries: Vec<lo_zip::ZipEntry> = Vec::new();
for entry_name in zip.entries() {
let path = normalize_zip_path(entry_name);
if path == "xl/calcChain.xml" {
continue;
}
if path == "[Content_Types].xml" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
remove_content_type_override(&mut root, "/xl/calcChain.xml");
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if path == "xl/_rels/workbook.xml.rels" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
remove_calc_chain_relationships(&mut root);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if path == "xl/workbook.xml" {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
mark_workbook_recalculated(&mut root);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
if let Some(sheet_index) = sheet_targets.iter().position(|(target, _)| target == &path) {
if let Some(sheet) = workbook.sheets.get(sheet_index) {
let xml = zip.read_string(&path)?;
let mut root = parse_xml_document(&xml)?;
patch_xlsx_sheet_formula_cache(&mut root, sheet)?;
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&root).into_bytes(),
));
continue;
}
}
entries.push(lo_zip::ZipEntry::new(path, zip.read(entry_name)?));
}
lo_zip::ooxml_package(&entries)
}
fn parse_xlsx_sheet_targets(zip: &ZipArchive) -> Result<Vec<(String, String)>> {
let workbook_root = parse_xml_document(&zip.read_string("xl/workbook.xml")?)?;
let rels = parse_relationships(zip, "xl/workbook.xml")?;
let mut out = Vec::new();
if let Some(sheets) = workbook_root.child("sheets") {
for (index, sheet) in sheets.children_named("sheet").enumerate() {
let name = sheet.attr("name").unwrap_or("Sheet").to_string();
let target = sheet
.attr("id")
.or_else(|| sheet.attr("r:id"))
.and_then(|id| rels.get(id))
.cloned()
.unwrap_or_else(|| format!("xl/worksheets/sheet{}.xml", index + 1));
out.push((normalize_zip_path(&target), name));
}
}
Ok(out)
}
fn parse_relationships(zip: &ZipArchive, part: &str) -> Result<BTreeMap<String, String>> {
let rels_path = rels_path_for(part);
if !zip.contains(&rels_path) {
return Ok(BTreeMap::new());
}
let root = parse_xml_document(&zip.read_string(&rels_path)?)?;
let mut map = BTreeMap::new();
for rel in root.children_named("Relationship") {
if let (Some(id), Some(target)) = (rel.attr("Id"), rel.attr("Target")) {
map.insert(id.to_string(), resolve_part_target(part, target));
}
}
Ok(map)
}
fn remove_content_type_override(root: &mut XmlNode, part_name: &str) {
root.items.retain(|item| match item {
XmlItem::Node(node) if node.local_name() == "Override" => {
node.attr("PartName") != Some(part_name)
}
_ => true,
});
sync_node_children(root);
}
fn remove_calc_chain_relationships(root: &mut XmlNode) {
root.items.retain(|item| match item {
XmlItem::Node(node) if node.local_name() == "Relationship" => {
let target = node.attr("Target").unwrap_or("");
let rel_type = node.attr("Type").unwrap_or("");
!target.ends_with("calcChain.xml")
&& !rel_type.to_ascii_lowercase().contains("calcchain")
}
_ => true,
});
sync_node_children(root);
}
fn mark_workbook_recalculated(root: &mut XmlNode) {
let mut found = false;
for item in &mut root.items {
if let XmlItem::Node(node) = item {
if node.local_name() == "calcPr" {
node.attributes
.insert("calcCompleted".to_string(), "1".to_string());
node.attributes
.insert("fullCalcOnLoad".to_string(), "0".to_string());
node.attributes.remove("calcMode");
found = true;
}
}
}
if !found {
let mut attrs = BTreeMap::new();
attrs.insert("calcCompleted".to_string(), "1".to_string());
attrs.insert("fullCalcOnLoad".to_string(), "0".to_string());
root.items.push(XmlItem::Node(XmlNode {
name: "calcPr".to_string(),
attributes: attrs,
children: Vec::new(),
items: Vec::new(),
text: String::new(),
}));
}
sync_node_children(root);
}
fn patch_xlsx_sheet_formula_cache(root: &mut XmlNode, sheet: &Sheet) -> Result<()> {
let Some(sheet_data) = child_mut(root, "sheetData") else {
return Ok(());
};
let mut shared_formulas: BTreeMap<String, (CellAddr, String)> = BTreeMap::new();
for row in &mut sheet_data.children {
if row.local_name() != "row" {
continue;
}
let row_number = row
.attr("r")
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(1);
for cell in &mut row.children {
if cell.local_name() == "c" {
patch_formula_cell(cell, row_number, sheet, &mut shared_formulas)?;
}
}
sync_node_items_from_children(row);
}
sync_node_items_from_children(sheet_data);
sync_node_items_from_children(root);
Ok(())
}
fn patch_formula_cell(
cell: &mut XmlNode,
fallback_row: usize,
sheet: &Sheet,
shared_formulas: &mut BTreeMap<String, (CellAddr, String)>,
) -> Result<()> {
let (row_1, col_1) = cell
.attr("r")
.and_then(parse_a1_cell_ref)
.unwrap_or((fallback_row, 1));
let addr = CellAddr::new(
row_1.saturating_sub(1) as u32,
col_1.saturating_sub(1) as u32,
);
let Some(formula) = resolve_formula_for_cell(cell, addr, shared_formulas) else {
return Ok(());
};
if formula.trim().is_empty() {
return Ok(());
}
let value = evaluate_formula_in_cell(&formula, sheet, addr)?;
let mut new_items = Vec::new();
for item in &cell.items {
match item {
XmlItem::Text(text) => new_items.push(XmlItem::Text(text.clone())),
XmlItem::Node(node) if matches!(node.local_name(), "v" | "is") => {}
XmlItem::Node(node) => new_items.push(XmlItem::Node(node.clone())),
}
}
new_items.push(XmlItem::Node(make_value_node(&value)));
cell.items = new_items;
sync_node_children(cell);
apply_formula_cache_type(cell, &value);
Ok(())
}
fn resolve_formula_for_cell(
cell: &XmlNode,
addr: CellAddr,
shared_formulas: &mut BTreeMap<String, (CellAddr, String)>,
) -> Option<String> {
let mut formula_text = None;
let mut formula_kind = None;
let mut shared_index = None;
for child in &cell.children {
if child.local_name() == "f" {
formula_text = Some(text_content(child));
formula_kind = child.attr("t").map(str::to_string);
shared_index = child.attr("si").map(str::to_string);
break;
}
}
let text = formula_text.unwrap_or_default();
if !text.trim().is_empty() {
if formula_kind.as_deref() == Some("shared") {
if let Some(si) = shared_index.clone() {
shared_formulas.insert(si, (addr, text.clone()));
}
}
return Some(text);
}
if formula_kind.as_deref() == Some("shared") {
if let Some(si) = shared_index {
if let Some((base_addr, base_formula)) = shared_formulas.get(&si) {
return Some(translate_shared_formula(base_formula, *base_addr, addr));
}
}
}
None
}
fn evaluate_formula_in_cell(formula: &str, sheet: &Sheet, _addr: CellAddr) -> Result<Value> {
let normalized = normalize_formula_for_eval(formula);
evaluate_formula(&normalized, sheet)
}
fn normalize_formula_for_eval(formula: &str) -> String {
formula.replace('$', "")
}
fn translate_shared_formula(formula: &str, from: CellAddr, to: CellAddr) -> String {
let row_delta = to.row as i32 - from.row as i32;
let col_delta = to.col as i32 - from.col as i32;
let chars: Vec<char> = formula.chars().collect();
let mut out = String::new();
let mut index = 0usize;
let mut in_string = false;
while index < chars.len() {
let ch = chars[index];
if ch == '"' {
in_string = !in_string;
out.push(ch);
index += 1;
continue;
}
if !in_string && is_reference_start(&chars, index) {
if let Some((len, shifted)) =
translate_reference_token(&chars, index, row_delta, col_delta)
{
out.push_str(&shifted);
index += len;
continue;
}
}
out.push(ch);
index += 1;
}
out
}
fn is_reference_start(chars: &[char], index: usize) -> bool {
let ch = chars[index];
if !(ch == '$' || ch.is_ascii_alphabetic()) {
return false;
}
if index == 0 {
return true;
}
let prev = chars[index - 1];
!(prev.is_ascii_alphanumeric() || matches!(prev, '_' | '.'))
}
fn translate_reference_token(
chars: &[char],
start: usize,
row_delta: i32,
col_delta: i32,
) -> Option<(usize, String)> {
let mut index = start;
let col_abs = if chars.get(index) == Some(&'$') {
index += 1;
true
} else {
false
};
let letters_start = index;
while index < chars.len() && chars[index].is_ascii_alphabetic() {
index += 1;
}
if index == letters_start {
return None;
}
let letters_end = index;
let row_abs = if chars.get(index) == Some(&'$') {
index += 1;
true
} else {
false
};
let digits_start = index;
while index < chars.len() && chars[index].is_ascii_digit() {
index += 1;
}
if index == digits_start {
return None;
}
if matches!(chars.get(index), Some(&'!')) {
return None;
}
if matches!(chars.get(index), Some(&ch) if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.')) {
return None;
}
let letters: String = chars[letters_start..letters_end].iter().collect();
let digits: String = chars[digits_start..index].iter().collect();
let mut col = column_letters_to_index(&letters)? as i32;
let mut row = digits.parse::<i32>().ok()? - 1;
if !col_abs {
col += col_delta;
}
if !row_abs {
row += row_delta;
}
let col = col.max(0) as u32;
let row = row.max(0) as u32;
let shifted = format!(
"{}{}{}{}",
if col_abs { "$" } else { "" },
column_index_to_letters(col),
if row_abs { "$" } else { "" },
row + 1
);
Some((index - start, shifted))
}
fn column_letters_to_index(letters: &str) -> Option<u32> {
let mut col = 0u32;
for ch in letters.chars() {
if !ch.is_ascii_alphabetic() {
return None;
}
col = col.checked_mul(26)? + ((ch.to_ascii_uppercase() as u8 - b'A') as u32 + 1);
}
col.checked_sub(1)
}
fn column_index_to_letters(mut col: u32) -> String {
col += 1;
let mut letters = String::new();
while col > 0 {
let remainder = ((col - 1) % 26) as u8;
letters.insert(0, (b'A' + remainder) as char);
col = (col - 1) / 26;
}
letters
}
fn text_content(node: &XmlNode) -> String {
let mut out = String::new();
if !node.text.is_empty() {
out.push_str(&node.text);
}
for child in &node.children {
out.push_str(&text_content(child));
}
out
}
fn apply_formula_cache_type(cell: &mut XmlNode, value: &Value) {
match value {
Value::Number(_) | Value::Blank => {
cell.attributes.remove("t");
}
Value::Text(_) => {
cell.attributes.insert("t".to_string(), "str".to_string());
}
Value::Bool(_) => {
cell.attributes.insert("t".to_string(), "b".to_string());
}
Value::Error(_) => {
cell.attributes.insert("t".to_string(), "e".to_string());
}
}
}
fn make_value_node(value: &Value) -> XmlNode {
let text = match value {
Value::Blank => String::new(),
Value::Number(number) => {
if number.fract() == 0.0 && number.is_finite() {
format!("{}", *number as i64)
} else {
number.to_string()
}
}
Value::Text(text) => text.clone(),
Value::Bool(value) => {
if *value {
"1".to_string()
} else {
"0".to_string()
}
}
Value::Error(text) => text.clone(),
};
XmlNode {
name: "v".to_string(),
attributes: BTreeMap::new(),
children: Vec::new(),
items: if text.is_empty() {
Vec::new()
} else {
vec![XmlItem::Text(text.clone())]
},
text,
}
}
fn parse_a1_cell_ref(input: &str) -> Option<(usize, usize)> {
let mut letters = String::new();
let mut digits = String::new();
for ch in input.chars() {
if ch == '$' {
continue;
}
if ch.is_ascii_alphabetic() && digits.is_empty() {
letters.push(ch);
} else if ch.is_ascii_digit() {
digits.push(ch);
} else {
return None;
}
}
if letters.is_empty() || digits.is_empty() {
return None;
}
let row = digits.parse().ok()?;
let mut col = 0usize;
for ch in letters.chars() {
col = col * 26 + ((ch.to_ascii_uppercase() as u8 - b'A' + 1) as usize);
}
Some((row, col))
}
pub fn accept_all_tracked_changes_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
let zip = ZipArchive::new(bytes)?;
let mut entries: Vec<lo_zip::ZipEntry> = Vec::new();
for entry_name in zip.entries() {
let path = normalize_zip_path(entry_name);
if is_wordprocessing_xml(&path) {
let xml = zip.read_string(&path)?;
let root = parse_xml_document(&xml)?;
let accepted = accept_revision_root(&root, &path);
entries.push(lo_zip::ZipEntry::new(
path,
serialize_xml_document(&accepted).into_bytes(),
));
} else {
entries.push(lo_zip::ZipEntry::new(path, zip.read(entry_name)?));
}
}
lo_zip::ooxml_package(&entries)
}
#[deprecated(note = "use accept_all_tracked_changes_docx_bytes")]
pub fn accept_tracked_changes_docx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
accept_all_tracked_changes_docx_bytes(bytes)
}
#[deprecated(note = "use xlsx_recalc_bytes")]
pub fn recalc_existing_xlsx_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
xlsx_recalc_bytes(bytes)
}
fn is_wordprocessing_xml(path: &str) -> bool {
path.starts_with("word/")
&& path.ends_with(".xml")
&& !path.contains("_rels/")
&& !path.ends_with("fontTable.xml")
}
fn accept_revision_root(root: &XmlNode, path: &str) -> XmlNode {
let items = accept_revision_items(&root.items);
let mut node = rebuild_node(root, items, root.attributes.clone());
if path.ends_with("settings.xml") {
node.items.retain(
|item| !matches!(item, XmlItem::Node(child) if child.local_name() == "trackRevisions"),
);
sync_node_children(&mut node);
}
node
}
fn accept_revision_items(items: &[XmlItem]) -> Vec<XmlItem> {
let mut out = Vec::new();
for item in items {
match item {
XmlItem::Text(text) => out.push(XmlItem::Text(text.clone())),
XmlItem::Node(node) => out.extend(accept_revision_node(node)),
}
}
out
}
fn accept_revision_node(node: &XmlNode) -> Vec<XmlItem> {
let local = node.local_name();
if matches!(
local,
"del"
| "delText"
| "delInstrText"
| "cellDel"
| "moveFrom"
| "moveFromRangeStart"
| "moveFromRangeEnd"
| "moveToRangeStart"
| "moveToRangeEnd"
| "customXmlDelRangeStart"
| "customXmlDelRangeEnd"
| "customXmlMoveFromRangeStart"
| "customXmlMoveFromRangeEnd"
| "customXmlMoveToRangeStart"
| "customXmlMoveToRangeEnd"
| "trackRevisions"
) {
return Vec::new();
}
if matches!(
local,
"ins" | "moveTo" | "customXmlInsRangeStart" | "customXmlInsRangeEnd"
) {
return accept_revision_items(&node.items);
}
if local.ends_with("Change") {
return Vec::new();
}
if row_deleted(node) {
return Vec::new();
}
let items = accept_revision_items(&node.items);
vec![XmlItem::Node(rebuild_node(
node,
items,
node.attributes.clone(),
))]
}
fn row_deleted(node: &XmlNode) -> bool {
if node.local_name() != "tr" {
return false;
}
node.child("trPr")
.map(|trpr| {
trpr.children
.iter()
.any(|child| child.local_name() == "del")
})
.unwrap_or(false)
}
fn rebuild_node(
template: &XmlNode,
items: Vec<XmlItem>,
attributes: BTreeMap<String, String>,
) -> XmlNode {
let mut node = XmlNode {
name: template.name.clone(),
attributes,
children: Vec::new(),
items,
text: String::new(),
};
sync_node_children(&mut node);
node
}
fn sync_node_children(node: &mut XmlNode) {
node.children = node
.items
.iter()
.filter_map(|item| match item {
XmlItem::Node(child) => Some(child.clone()),
_ => None,
})
.collect();
node.text = node
.items
.iter()
.filter_map(|item| match item {
XmlItem::Text(text) => Some(text.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
}
fn sync_node_items_from_children(node: &mut XmlNode) {
let mut child_index = 0usize;
let mut new_items = Vec::with_capacity(node.items.len().max(node.children.len()));
for item in &node.items {
match item {
XmlItem::Text(text) => new_items.push(XmlItem::Text(text.clone())),
XmlItem::Node(_) => {
if let Some(updated) = node.children.get(child_index) {
new_items.push(XmlItem::Node(updated.clone()));
child_index += 1;
}
}
}
}
while let Some(updated) = node.children.get(child_index) {
new_items.push(XmlItem::Node(updated.clone()));
child_index += 1;
}
node.items = new_items;
sync_node_children(node);
}
fn child_mut<'a>(node: &'a mut XmlNode, name: &str) -> Option<&'a mut XmlNode> {
node.children
.iter_mut()
.find(|child| child.local_name() == name || child.name == name)
}
#[allow(dead_code)]
fn _assert_send_sync() {
fn assert<T: Send + Sync>() {}
assert::<Result<Vec<u8>>>();
let _ = LoError::Parse(String::new());
}