karo 0.1.2

Spreadsheet export
Documentation
#[cfg(test)]
mod tests;

use crate::{Result, XmlWritable, XmlWriter};
use indexmap::{indexmap, IndexMap, IndexSet};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Default)]
struct Inner {
    count: usize,
    strings: IndexSet<SharedString>,
}

#[derive(Clone, Default)]
pub(crate) struct SharedStrings {
    inner: Rc<RefCell<Inner>>,
}

impl SharedStrings {
    pub fn create_or_get_index<S: AsRef<str>>(
        &mut self,
        string: S,
        rich: bool,
    ) -> usize {
        let mut inner = self.inner.borrow_mut();
        inner.count += 1;
        inner
            .strings
            .insert_full(SharedString {
                string: string.as_ref().to_string(),
                rich,
            })
            .0
    }

    pub fn is_empty(&self) -> bool {
        self.inner.borrow().strings.is_empty()
    }
}

#[derive(PartialEq, Eq, Hash)]
struct SharedString {
    pub string: String,
    pub rich: bool,
}

impl XmlWritable for SharedStrings {
    fn write_xml<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
        self.inner.borrow().write_xml(w)
    }
}

impl XmlWritable for Inner {
    fn write_xml<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
        let tag = "sst";
        let attrs = indexmap! {
            "xmlns" =>
                "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
            "count" => format!("{}", self.count),
            "uniqueCount" => format!("{}", self.strings.len())
        };
        w.start_tag_with_attrs(tag, attrs)?;
        for string in self.strings.iter() {
            string.write_xml(w)?;
        }
        w.end_tag(tag)?;
        Ok(())
    }
}

impl XmlWritable for SharedString {
    fn write_xml<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
        let tag = "si";
        w.start_tag(tag)?;
        if self.rich {
            w.text(&self.string)?;
        } else {
            let tag = "t";
            let string = Self::escape_control_chars(&self.string);
            let mut attrs = IndexMap::new();
            match (string.chars().next(), string.chars().rev().next()) {
                (Some(c), _) | (_, Some(c)) if c.is_ascii_whitespace() => {
                    attrs.insert("xml:space", "preserve");
                }
                _ => {}
            }
            w.tag_with_attrs_and_text(tag, attrs, &string)?;
        }
        w.end_tag(tag)?;
        Ok(())
    }
}

impl SharedString {
    fn escape_control_chars(s: &str) -> String {
        let mut result = String::new();
        for c in s.chars() {
            match c {
                '\x01' | '\x02' | '\x03' | '\x04' | '\x05' | '\x06'
                | '\x07' | '\x08' | '\x0B' | '\x0C' | '\x0D' | '\x0E'
                | '\x0F' | '\x10' | '\x11' | '\x12' | '\x13' | '\x14'
                | '\x15' | '\x16' | '\x17' | '\x18' | '\x19' | '\x1A'
                | '\x1B' | '\x1C' | '\x1D' | '\x1E' | '\x1F' => {
                    result.push_str(&format!("_x{:04X}_", c as u32));
                }
                _ => result.push(c),
            }
        }
        result
    }
}