mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
Merge pull request #1325 from storytold/codex/docx-level-presence
docx: retain explicitly defined numbering levels
This commit is contained in:
3 files changed
+83
-3
No files matched your search
@@ -331,9 +331,11 @@ impl Reader<'_> {
|
||||
for a in root.children("w:abstractNum").take(10_000) {
|
||||
let Some(id) = a.attr("w:abstractNumId").and_then(u32_of) else { continue };
|
||||
let mut levels: Vec<Level> = (0..9).map(default_level).collect();
|
||||
let mut explicit_levels = [false; 9];
|
||||
for l in a.children("w:lvl") {
|
||||
let Some(i) = numbering_level_index(l.attr("w:ilvl")) else { continue };
|
||||
levels[i] = self.level(l, i);
|
||||
explicit_levels[i] = true;
|
||||
}
|
||||
if let Some(link) = a.child_val("w:numStyleLink") {
|
||||
links.push((id, link.to_string()));
|
||||
@@ -343,6 +345,7 @@ impl Reader<'_> {
|
||||
id,
|
||||
name: a.child_val("w:name").map(str::to_string),
|
||||
levels,
|
||||
explicit_levels: Some(explicit_levels),
|
||||
nsid: a.child_val("w:nsid").filter(|v| hex(v)).map(str::to_ascii_uppercase),
|
||||
tmpl: a.child_val("w:tmpl").filter(|v| hex(v)).map(str::to_ascii_uppercase),
|
||||
multi_level_type: a
|
||||
@@ -376,9 +379,13 @@ impl Reader<'_> {
|
||||
for (aid, style) in links {
|
||||
let num = self.doc.styles.get(&self.pc.style_id(&style)).and_then(|s| s.para.numbering).map(|n| n.num);
|
||||
let src = num.and_then(|n| nums.iter().find(|x: &&Num| x.id == n)).map(|x| x.abstract_id);
|
||||
let levels = src.filter(|s| *s != aid).and_then(|s| abstracts.iter().find(|a: &&AbstractNum| a.id == s)).map(|a| a.levels.clone());
|
||||
if let (Some(levels), Some(a)) = (levels, abstracts.iter_mut().find(|a| a.id == aid)) {
|
||||
let source = src
|
||||
.filter(|s| *s != aid)
|
||||
.and_then(|s| abstracts.iter().find(|a: &&AbstractNum| a.id == s))
|
||||
.map(|a| (a.levels.clone(), a.explicit_levels));
|
||||
if let (Some((levels, explicit_levels)), Some(a)) = (source, abstracts.iter_mut().find(|a| a.id == aid)) {
|
||||
a.levels = levels;
|
||||
a.explicit_levels = explicit_levels;
|
||||
}
|
||||
}
|
||||
Arc::make_mut(&mut self.doc.numbering).abstracts = abstracts;
|
||||
|
||||
@@ -335,6 +335,9 @@ fn malformed_numbering_levels_are_skipped_instead_of_becoming_level_zero() {
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(paragraphs.len(), 5);
|
||||
assert!(doc.numbering.level_is_defined(1, 0));
|
||||
assert!(!doc.numbering.level_is_defined(1, 2), "effective defaults remain distinguishable from source definitions");
|
||||
assert!(doc.numbering.level(1, 2).is_some(), "Word's effective default level remains available");
|
||||
assert_eq!(paragraphs[0].props.numbering.map(|n| (n.num, n.level)), Some((1, 0)), "an omitted level defaults to zero");
|
||||
assert_eq!(paragraphs[1].props.numbering.map(|n| (n.num, n.level)), Some((1, 2)), "a valid level is preserved");
|
||||
for p in ¶graphs[2..] {
|
||||
|
||||
@@ -70,12 +70,18 @@ impl Default for Level {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct AbstractNum {
|
||||
pub id: u32,
|
||||
pub name: Option<String>,
|
||||
pub levels: Vec<Level>,
|
||||
/// Which levels were defined in the source file. `None` is used by model-created definitions,
|
||||
/// where the populated `levels` vector is authoritative. DOCX readers set this when they fill
|
||||
/// unspecified Word levels with their effective defaults. This reader-only provenance is not
|
||||
/// serialized as part of the document model.
|
||||
#[serde(skip)]
|
||||
pub explicit_levels: Option<[bool; 9]>,
|
||||
/// Word's list identity (`w:nsid`, 8 hex digits), which it uses to match lists across
|
||||
/// documents (paste, compare); kept from the file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -89,6 +95,20 @@ pub struct AbstractNum {
|
||||
pub multi_level_type: Option<String>,
|
||||
}
|
||||
|
||||
// Explicit source-level presence is reader provenance used to distinguish an omitted Word level
|
||||
// from an effective default. It is intentionally not part of the document model's structural
|
||||
// equality, just as it is not serialized.
|
||||
impl PartialEq for AbstractNum {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
&& self.name == other.name
|
||||
&& self.levels == other.levels
|
||||
&& self.nsid == other.nsid
|
||||
&& self.tmpl == other.tmpl
|
||||
&& self.multi_level_type == other.multi_level_type
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Num {
|
||||
@@ -236,6 +256,20 @@ impl Numbering {
|
||||
}
|
||||
self.abstract_of(num)?.levels.get(level as usize)
|
||||
}
|
||||
/// Whether the source definition supplied this level, or a per-numbering override defines it.
|
||||
///
|
||||
/// [`Self::level`] also exposes Word's effective default for omitted levels. This method lets
|
||||
/// importers preserve formats whose historical behavior distinguishes an explicit level from
|
||||
/// that effective fallback. Older serialized and model-created definitions have no source
|
||||
/// presence metadata, so their populated levels count as defined.
|
||||
pub fn level_is_defined(&self, num: u32, level: u8) -> bool {
|
||||
let Some(n) = self.num(num) else { return false };
|
||||
if n.level_overrides.iter().any(|(i, _)| *i == level) {
|
||||
return true;
|
||||
}
|
||||
let Some(a) = self.abstract_of(num) else { return false };
|
||||
a.explicit_levels.map_or_else(|| a.levels.get(level as usize).is_some(), |levels| levels.get(level as usize).copied().unwrap_or(false))
|
||||
}
|
||||
/// Create a new list of `kind`; returns the `num` id. List ids run out only past `u32::MAX`
|
||||
/// lists, more than memory holds; then nothing is added and the id is 0, "no list".
|
||||
pub fn add_list(&mut self, kind: ListKind) -> u32 {
|
||||
@@ -485,6 +519,42 @@ const MAX_LABEL_BYTES: usize = 256;
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn explicit_level_metadata_preserves_effective_defaults_and_old_models() {
|
||||
let mut numbering = Numbering::default();
|
||||
let id = numbering.add_list(ListKind::Numbered);
|
||||
assert!(numbering.level_is_defined(id, 0));
|
||||
assert!(numbering.level_is_defined(id, 8));
|
||||
|
||||
let abstract_id = numbering.num(id).unwrap().abstract_id;
|
||||
let mut explicit = [false; 9];
|
||||
explicit[0] = true;
|
||||
numbering.abstracts.iter_mut().find(|a| a.id == abstract_id).unwrap().explicit_levels = Some(explicit);
|
||||
|
||||
// Effective Word defaults remain available to existing model callers, while importers can
|
||||
// tell that this source file supplied only level zero.
|
||||
assert!(numbering.level(id, 1).is_some());
|
||||
assert!(!numbering.level_is_defined(id, 1));
|
||||
assert!(numbering.level_is_defined(id, 0));
|
||||
|
||||
// Old serialized definitions have no presence marker; populated levels remain valid.
|
||||
numbering.abstracts.iter_mut().find(|a| a.id == abstract_id).unwrap().explicit_levels = None;
|
||||
assert!(numbering.level_is_defined(id, 8));
|
||||
|
||||
// A per-numbering level definition takes precedence over the abstract's missing level.
|
||||
numbering.abstracts.iter_mut().find(|a| a.id == abstract_id).unwrap().explicit_levels = Some(explicit);
|
||||
numbering.nums.iter_mut().find(|n| n.id == id).unwrap().level_overrides.push((1, Level::default()));
|
||||
assert!(numbering.level_is_defined(id, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_level_metadata_does_not_change_model_equality() {
|
||||
let mut with_metadata = AbstractNum::default();
|
||||
let without_metadata = with_metadata.clone();
|
||||
with_metadata.explicit_levels = Some([true; 9]);
|
||||
assert_eq!(with_metadata, without_metadata);
|
||||
}
|
||||
|
||||
/// Read-only counter APIs retain the renderer's bounds and never advance a list.
|
||||
#[test]
|
||||
fn reference_labels_are_bounded_and_do_not_advance_counters() {
|
||||
|
||||
Reference in new issue
Block a user