rs-chunks 0.1.0

Fast, high-fidelity document chunking for RAG — a pure-Rust engine covering 36 file formats (Office, OpenDocument, PDF, email, ebooks, notebooks, and more).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StyleKind {
    Normal,
    Heading(u8),
    ListParagraph,
    Unknown,
}

pub struct StyleSheet {
    pub styles: Vec<StyleKind>,
}

impl StyleSheet {
    pub fn kind(&self, istd: usize) -> StyleKind {
        self.styles.get(istd).cloned().unwrap_or(StyleKind::Unknown)
    }
}

fn read_u16(data: &[u8], offset: usize) -> Option<u16> {
    data.get(offset..offset + 2)
        .map(|b| u16::from_le_bytes([b[0], b[1]]))
}

fn decode_utf16(bytes: &[u8]) -> String {
    let mut units = Vec::with_capacity(bytes.len() / 2);
    let mut i = 0usize;
    while i + 1 < bytes.len() {
        units.push(u16::from_le_bytes([bytes[i], bytes[i + 1]]));
        i += 2;
    }
    char::decode_utf16(units)
        .map(|r| r.unwrap_or('\u{FFFD}'))
        .collect()
}

fn heading_level_from_name(name_lc: &str) -> Option<u8> {
    for prefix in ["heading", "titre", "überschrift"] {
        if let Some(rest) = name_lc.strip_prefix(prefix) {
            let digits: String = rest.chars().skip_while(|c| !c.is_ascii_digit()).collect();
            if let Some(ch) = digits.chars().next() {
                if let Some(n) = ch.to_digit(10) {
                    if (1..=9).contains(&n) {
                        return Some(n as u8);
                    }
                }
            }
            return Some(1);
        }
    }
    None
}

fn classify_style_name(name: &str) -> StyleKind {
    let lower = name.trim().to_ascii_lowercase();
    if let Some(level) = heading_level_from_name(&lower) {
        return StyleKind::Heading(level);
    }
    if lower == "normal" || lower == "default paragraph font" {
        return StyleKind::Normal;
    }
    if lower.starts_with("list paragraph")
        || lower.starts_with("list bullet")
        || lower.starts_with("list number")
    {
        return StyleKind::ListParagraph;
    }
    StyleKind::Unknown
}

pub fn parse_stylesheet(
    table_stream: &[u8],
    fc_stshf: u32,
    lcb_stshf: u32,
) -> Result<StyleSheet, String> {
    let start = fc_stshf as usize;
    let len = lcb_stshf as usize;
    let end = start
        .checked_add(len)
        .ok_or_else(|| "Stylesheet offset overflow".to_string())?;
    let data = table_stream
        .get(start..end)
        .ok_or_else(|| "Stylesheet points outside table stream".to_string())?;

    if data.len() < 14 {
        return Ok(StyleSheet { styles: Vec::new() });
    }

    let cstd = read_u16(data, 0).unwrap_or(0) as usize;
    let cb_std_base_in_file = read_u16(data, 2).unwrap_or(0) as usize;

    let mut pos = 14usize;
    let mut styles = Vec::with_capacity(cstd);

    for _ in 0..cstd {
        let cb_std = match read_u16(data, pos) {
            Some(v) => v as usize,
            None => break,
        };
        pos += 2;

        if cb_std == 0 {
            styles.push(StyleKind::Unknown);
            continue;
        }

        let std_end = match pos.checked_add(cb_std) {
            Some(v) => v,
            None => {
                styles.push(StyleKind::Unknown);
                break;
            }
        };
        let std = match data.get(pos..std_end) {
            Some(v) => v,
            None => {
                styles.push(StyleKind::Unknown);
                break;
            }
        };

        let mut kind = StyleKind::Unknown;
        if cb_std_base_in_file + 2 <= std.len() {
            let cch_name = read_u16(std, cb_std_base_in_file).unwrap_or(0) as usize;
            let name_bytes_start = cb_std_base_in_file + 2;
            let name_bytes_len = cch_name.saturating_mul(2);
            let name_bytes_end = name_bytes_start.saturating_add(name_bytes_len);
            if let Some(name_bytes) = std.get(name_bytes_start..name_bytes_end) {
                let name = decode_utf16(name_bytes);
                kind = classify_style_name(&name);
            }
        }

        styles.push(kind);
        pos = std_end;
    }

    while styles.len() < cstd {
        styles.push(StyleKind::Unknown);
    }

    Ok(StyleSheet { styles })
}