mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 03:49:54 -06:00
insert.checkbox existed but had no ribbon button, replaced existing values with FALSE and was written to XLSX as an invalid number format. Now: Insert > Checkbox (Cell Controls); empty cells start FALSE, other values stay and display as General; toggling an emptied box checks it and only checkbox cells toggle; Delete unchecks first, then removes the boxes; XLSX reads and writes the checkbox as Excel's feature-property-bag cell control ([MS-XLSX] 2.1.22, 2.3.9, 2.4.99). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
4.8 KiB
Rust
84 lines
4.8 KiB
Rust
//! Feature property bags ([MS-XLSX] 2.1.22, 2.3.9): how Excel stores cell checkboxes.
|
|
//!
|
|
//! A checkbox is a cell format (`xf`) whose `extLst` carries an `xfComplement i="n"` element
|
|
//! ([MS-XLSX] 2.4.99, styles extension `{C7286773-470A-42A8-94C5-96B5CB345126}`). `n` indexes the
|
|
//! `MappedFeaturePropertyBags` array of the `XFComplements` bag; that entry is an `XFComplement`
|
|
//! bag whose `XFControls` key names an `XFControls` bag, whose `CellControl` key names a
|
|
//! `Checkbox` bag. Bag references are zero-based indexes into the part's list of `bag` elements.
|
|
//!
|
|
//! GridCraft keeps a checkbox as the `checkbox` number format in its model, so reading maps each
|
|
//! `xfComplement` that resolves to a `Checkbox` bag onto that format, and writing emits one chain
|
|
//! that every checkbox `xf` points at.
|
|
|
|
use crate::xml::El;
|
|
|
|
/// Namespace of the part and of the `xfComplement` element.
|
|
pub const NS: &str = "http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag";
|
|
/// Relationship type from the workbook part.
|
|
pub const REL: &str = "http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag";
|
|
/// Content type of the part.
|
|
pub const CONTENT_TYPE: &str = "application/vnd.ms-excel.featurepropertybag+xml";
|
|
/// Part name we write.
|
|
pub const PART: &str = "xl/featurePropertyBag/featurePropertyBag.xml";
|
|
/// `ext` URI of the `xfComplement` extension on an `xf`.
|
|
pub const XF_EXT_URI: &str = "{C7286773-470A-42A8-94C5-96B5CB345126}";
|
|
|
|
/// The bags for one checkbox cell control, referenced as `xfComplement i="0"`: Checkbox (0),
|
|
/// XFControls → 0 (1), XFComplement → 1 (2), XFComplements mapping index 0 → 2 (3). Every
|
|
/// reference points to an earlier bag, as [MS-XLSX] 2.6.230 requires.
|
|
pub fn checkbox_part() -> String {
|
|
format!(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<FeaturePropertyBags xmlns=\"{NS}\" count=\"4\"><bag type=\"Checkbox\"/><bag type=\"XFControls\"><bagId k=\"CellControl\">0</bagId></bag><bag type=\"XFComplement\"><bagId k=\"XFControls\">1</bagId></bag><bag type=\"XFComplements\"><a k=\"MappedFeaturePropertyBags\"><bagId>2</bagId></a></bag></FeaturePropertyBags>"
|
|
)
|
|
}
|
|
|
|
/// The `extLst` that marks an `xf` as a checkbox (complement index 0 of [`checkbox_part`]).
|
|
pub fn checkbox_xf_ext() -> String {
|
|
format!("<extLst><ext uri=\"{XF_EXT_URI}\" xmlns:xfpb=\"{NS}\"><xfpb:xfComplement i=\"0\"/></ext></extLst>")
|
|
}
|
|
|
|
/// For each `xfComplement` index, whether it resolves to a Checkbox cell control. Unknown,
|
|
/// malformed or out-of-range references simply resolve to `false`.
|
|
pub fn checkbox_complements(root: &El) -> Vec<bool> {
|
|
let bags: Vec<&El> = root.kids("bag").collect();
|
|
let bag = |i: Option<u32>, ty: &str| i.and_then(|i| bags.get(i as usize).copied()).filter(|b| b.attr("type") == Some(ty));
|
|
let key = |b: &El, k: &str| b.kids("bagId").find(|e| e.attr("k") == Some(k)).and_then(|e| e.text.trim().parse::<u32>().ok());
|
|
let Some(map) = bags.iter().find(|b| b.attr("type") == Some("XFComplements")) else { return Vec::new() };
|
|
let Some(arr) = map.kids("a").find(|a| a.attr("k") == Some("MappedFeaturePropertyBags")) else { return Vec::new() };
|
|
arr.kids("bagId")
|
|
.take(100_000)
|
|
.map(|e| {
|
|
let complement = bag(e.text.trim().parse::<u32>().ok(), "XFComplement");
|
|
let controls = complement.and_then(|c| bag(key(c, "XFControls"), "XFControls"));
|
|
controls.and_then(|c| bag(key(c, "CellControl"), "Checkbox")).is_some()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Whether an `xf` element carries an `xfComplement` that resolves to a checkbox.
|
|
pub fn xf_is_checkbox(xf: &El, complements: &[bool]) -> bool {
|
|
let Some(ext) = xf.child("extLst") else { return false };
|
|
ext.kids("ext")
|
|
.filter(|e| e.attr("uri").is_some_and(|u| u.eq_ignore_ascii_case(XF_EXT_URI)))
|
|
.filter_map(|e| e.child("xfComplement"))
|
|
.filter_map(|c| c.attr_u32("i"))
|
|
.any(|i| complements.get(i as usize).copied().unwrap_or(false))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn written_chain_resolves_and_hostile_chains_do_not() {
|
|
let root = crate::xml::parse(checkbox_part().as_bytes()).unwrap_or_default();
|
|
assert_eq!(checkbox_complements(&root), vec![true]);
|
|
// Out-of-range, wrong-type and self references resolve to false without panicking.
|
|
let bad = format!(
|
|
"<FeaturePropertyBags xmlns=\"{NS}\"><bag type=\"XFControls\"><bagId k=\"CellControl\">99</bagId></bag><bag type=\"XFComplement\"><bagId k=\"XFControls\">1</bagId></bag><bag type=\"XFComplements\"><a k=\"MappedFeaturePropertyBags\"><bagId>1</bagId><bagId>4294967295</bagId><bagId>x</bagId></a></bag></FeaturePropertyBags>"
|
|
);
|
|
let root = crate::xml::parse(bad.as_bytes()).unwrap_or_default();
|
|
assert_eq!(checkbox_complements(&root), vec![false, false, false]);
|
|
}
|
|
}
|