rwml 0.1.1

Native Rust toolkit for Microsoft Word — read, write, edit, and render legacy .doc (Word 97-2003, [MS-DOC]) and modern .docx (OOXML): one document model, package-preserving edits, field evaluation, Markdown/HTML export, PDF preview
Documentation
use super::*;

#[derive(Debug, Clone, Default)]
pub(crate) struct LegacyFormContext {
    results: Vec<Option<String>>,
    preserve_cached: bool,
}

impl LegacyFormContext {
    fn field_result(&self, index: usize) -> Option<String> {
        self.results.get(index).cloned().flatten()
    }

    fn preserves_cached(&self) -> bool {
        self.preserve_cached
    }
}

#[derive(Debug, Clone, Default)]
struct LegacyFormData {
    checkbox: Option<bool>,
    dropdown_entries: Vec<String>,
    dropdown_default: Option<usize>,
    dropdown_result: Option<usize>,
    text_default: Option<String>,
}

#[derive(Debug, Clone)]
struct LegacyFormScanField {
    instruction: String,
    form_data: Option<LegacyFormData>,
    phase: FieldPhase,
}

pub(crate) fn legacy_form_context(xml: &str, preserve_cached: bool) -> LegacyFormContext {
    let mut r = Reader::from_str(xml);
    let mut results = Vec::new();
    let mut current: Option<LegacyFormScanField> = None;
    let mut xml_depth = 0usize;
    let mut alternate_content_stack = Vec::new();
    loop {
        match r.read_event() {
            Ok(Event::Start(e)) => {
                let qname = e.name();
                let name = local(qname.as_ref());
                if should_skip_alternate_branch(&mut alternate_content_stack, xml_depth, name) {
                    skip_subtree(&mut r);
                    continue;
                }
                if matches!(name, b"del" | b"moveFrom") {
                    skip_subtree(&mut r);
                    continue;
                }
                let mut consumed_element = false;
                match name {
                    b"AlternateContent" => {
                        alternate_content_stack.push(AlternateContentBranchState {
                            branch_depth: xml_depth + 1,
                            took_branch: false,
                        });
                    }
                    b"fldSimple" => {
                        let instruction = attr_local(&e, b"instr").unwrap_or_default();
                        let form_data = read_legacy_form_data_until(&mut r);
                        record_simple_legacy_form_result(&instruction, form_data, &mut results);
                        consumed_element = true;
                    }
                    b"fldChar" => {
                        let kind = field_char_type(&e);
                        let form_data = read_legacy_form_data_until(&mut r);
                        apply_legacy_form_scan_fld_char(
                            kind.as_deref(),
                            form_data,
                            &mut current,
                            &mut results,
                        );
                        consumed_element = true;
                    }
                    b"instrText" => {
                        let text = read_text(&mut r);
                        consumed_element = true;
                        if let Some(field) = current.as_mut() {
                            if field.phase == FieldPhase::Instruction {
                                field.instruction.push_str(&text);
                            }
                        }
                    }
                    _ => {}
                }
                if !consumed_element {
                    xml_depth = xml_depth.saturating_add(1);
                }
            }
            Ok(Event::Empty(e)) => {
                let qname = e.name();
                let name = local(qname.as_ref());
                if should_skip_alternate_branch(&mut alternate_content_stack, xml_depth, name) {
                    continue;
                }
                match name {
                    b"fldSimple" => {
                        record_simple_legacy_form_result(
                            attr_local(&e, b"instr").as_deref().unwrap_or_default(),
                            None,
                            &mut results,
                        );
                    }
                    b"fldChar" => apply_legacy_form_scan_fld_char(
                        field_char_type(&e).as_deref(),
                        None,
                        &mut current,
                        &mut results,
                    ),
                    _ => {}
                }
            }
            Ok(Event::End(e)) => {
                if local(e.name().as_ref()) == b"AlternateContent" {
                    alternate_content_stack.pop();
                }
                xml_depth = xml_depth.saturating_sub(1);
            }
            Ok(Event::Eof) | Err(_) => break,
            _ => {}
        }
    }
    LegacyFormContext {
        results,
        preserve_cached,
    }
}

fn record_simple_legacy_form_result(
    instruction: &str,
    form_data: Option<LegacyFormData>,
    results: &mut Vec<Option<String>>,
) {
    let instruction = normalize_instruction(instruction);
    if matches!(field_kind(&instruction), FieldKind::FormField(_)) {
        results.push(form_data.and_then(|data| legacy_form_field_result(&instruction, &data)));
    }
}

fn apply_legacy_form_scan_fld_char(
    kind: Option<&str>,
    form_data: Option<LegacyFormData>,
    current: &mut Option<LegacyFormScanField>,
    results: &mut Vec<Option<String>>,
) {
    match kind {
        Some("begin") => {
            *current = Some(LegacyFormScanField {
                instruction: String::new(),
                form_data,
                phase: FieldPhase::Instruction,
            });
        }
        Some("separate") => {
            if let Some(field) = current.as_mut() {
                field.phase = FieldPhase::Result;
            }
        }
        Some("end") => {
            if let Some(field) = current.take() {
                let instruction = normalize_instruction(&field.instruction);
                if matches!(field_kind(&instruction), FieldKind::FormField(_)) {
                    results.push(
                        field
                            .form_data
                            .and_then(|data| legacy_form_field_result(&instruction, &data)),
                    );
                }
            }
        }
        _ => {}
    }
}

fn read_legacy_form_data_until(r: &mut Xml<'_>) -> Option<LegacyFormData> {
    let mut depth = 1usize;
    let mut xml_depth = 1usize;
    let mut alternate_content_stack = Vec::new();
    let mut checkbox_depth = 0usize;
    let mut dropdown_depth = 0usize;
    let mut text_input_depth = 0usize;
    let mut data = LegacyFormData::default();
    loop {
        match r.read_event() {
            Ok(Event::Start(e)) => {
                let qname = e.name();
                let name = local(qname.as_ref());
                if should_skip_alternate_branch(&mut alternate_content_stack, xml_depth, name) {
                    skip_subtree(r);
                    continue;
                }
                if matches!(name, b"del" | b"moveFrom") {
                    skip_subtree(r);
                    continue;
                }
                let is_checkbox = name == b"checkBox";
                let is_dropdown = name == b"ddList";
                let is_text_input = name == b"textInput";
                read_legacy_form_data_element(
                    &e,
                    checkbox_depth > 0,
                    dropdown_depth > 0,
                    text_input_depth > 0,
                    &mut data,
                );
                match name {
                    b"AlternateContent" => {
                        alternate_content_stack.push(AlternateContentBranchState {
                            branch_depth: xml_depth + 1,
                            took_branch: false,
                        });
                    }
                    _ if is_checkbox => checkbox_depth += 1,
                    _ if is_dropdown => dropdown_depth += 1,
                    _ if is_text_input => text_input_depth += 1,
                    _ => {}
                }
                depth += 1;
                xml_depth = xml_depth.saturating_add(1);
            }
            Ok(Event::Empty(e)) => {
                let qname = e.name();
                let name = local(qname.as_ref());
                if should_skip_alternate_branch(&mut alternate_content_stack, xml_depth, name) {
                    continue;
                }
                read_legacy_form_data_element(
                    &e,
                    checkbox_depth > 0,
                    dropdown_depth > 0,
                    text_input_depth > 0,
                    &mut data,
                );
            }
            Ok(Event::End(e)) => {
                let qname = e.name();
                let name = local(qname.as_ref());
                if name == b"AlternateContent" {
                    alternate_content_stack.pop();
                }
                if name == b"checkBox" {
                    checkbox_depth = checkbox_depth.saturating_sub(1);
                } else if name == b"ddList" {
                    dropdown_depth = dropdown_depth.saturating_sub(1);
                } else if name == b"textInput" {
                    text_input_depth = text_input_depth.saturating_sub(1);
                }
                depth = depth.saturating_sub(1);
                xml_depth = xml_depth.saturating_sub(1);
                if depth == 0 {
                    break;
                }
            }
            Ok(Event::Eof) | Err(_) => break,
            _ => {}
        }
    }
    (data.checkbox.is_some()
        || !data.dropdown_entries.is_empty()
        || data.dropdown_default.is_some()
        || data.dropdown_result.is_some()
        || data.text_default.is_some())
    .then_some(data)
}

fn read_legacy_form_data_element(
    e: &BytesStart<'_>,
    in_checkbox: bool,
    in_dropdown: bool,
    in_text_input: bool,
    data: &mut LegacyFormData,
) {
    match local(e.name().as_ref()) {
        b"checked" if in_checkbox => data.checkbox = Some(toggle_on(attr_local(e, b"val"))),
        b"default" if in_checkbox && data.checkbox.is_none() => {
            data.checkbox = Some(toggle_on(attr_local(e, b"val")));
        }
        b"default" if in_dropdown => data.dropdown_default = attr_usize(e, b"val"),
        b"default" if in_text_input => {
            data.text_default = attr_local(e, b"val");
        }
        b"result" if in_dropdown => data.dropdown_result = attr_usize(e, b"val"),
        b"listEntry" if in_dropdown => {
            if let Some(value) = attr_local(e, b"val") {
                data.dropdown_entries.push(value);
            }
        }
        _ => {}
    }
}

fn legacy_form_field_result(instruction: &str, data: &LegacyFormData) -> Option<String> {
    let spec = legacy_form_field_syntax(instruction)?;
    let text = match spec.kind.as_str() {
        "FORMCHECKBOX" => Some(
            if data.checkbox? {
                "\u{2612}"
            } else {
                "\u{2610}"
            }
            .to_string(),
        ),
        "FORMDROPDOWN" => {
            let result = data
                .dropdown_result
                .and_then(|index| data.dropdown_entries.get(index));
            let default = data
                .dropdown_default
                .and_then(|index| data.dropdown_entries.get(index));
            result.or(default).cloned()
        }
        "FORMTEXT" => data.text_default.clone(),
        _ => None,
    }?;
    Some(apply_field_text_format(text, spec.text_format))
}

pub(crate) fn computed_legacy_form_result(
    instruction: &str,
    current_result: &str,
    legacy_forms: &LegacyFormContext,
    field_index: usize,
) -> Option<String> {
    if legacy_forms.preserves_cached() {
        return None;
    }
    let spec = legacy_form_field_syntax(instruction)?;
    match spec.kind.as_str() {
        "FORMTEXT" if !current_result.is_empty() => Some(apply_field_text_format(
            current_result.to_string(),
            spec.text_format,
        )),
        _ => legacy_forms.field_result(field_index),
    }
}