diff --git a/README.md b/README.md index f69f897..2e03c80 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,12 @@ default. `--words-from ` lifts the words a repo already has back into the words map by structure, so a rebuild keeps hand-edited wording and -adds only the new slots. The lift is by position, so it holds for a -rebuild of the same shape and not across a restructure: on uhhm.no it -put the old front door's three headings on three new cards. `--words +adds only the new slots. Cards are matched by what they are (the +bucket they record into, the page they lead to, a field's name, the +group they invite into), not by position, so a rebuild that reorders +or reshapes a page still hands each card its own words; on uhhm.no +the live front door's three headings follow their buckets onto the +new page. `--words ` overrides slots by page and may repeat, each file layered over the last (the words a build used, then a round of hand changes); `words.json` in the output is the words as used. See diff --git a/src/build.rs b/src/build.rs index 5b6af8d..b12d299 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1693,6 +1693,39 @@ pub fn lift_words(plan: &Plan, repo: &Path) -> anyhow::Result Vec<(&'static str, String)> { + const KEYS: &[&[&str]] = &[ + &["record_as"], + &["action"], + &["name"], + &["invite", "group"], + &["resource", "source", "bucket"], + &["self_transition", "bucket"], + ]; + let mut id = Vec::new(); + for path in KEYS { + let mut at = v; + for key in path.iter() { + match at.get(*key) { + Some(next) => at = next, + None => { + at = &serde_yaml::Value::Null; + break; + } + } + } + if let Some(s) = at.as_str() { + if !s.starts_with('\u{1}') && !s.is_empty() { + id.push((path[0], s.to_string())); + } + } + } + id +} + fn token(path: &str, pointer: &str) -> String { format!("\u{1}{path}\u{2}{pointer}\u{3}") } @@ -1725,9 +1758,27 @@ fn collect_words(a: &serde_yaml::Value, b: &serde_yaml::Value, found: &mut std:: } } } + // Items are matched by what they are, not where they sit: the + // bucket an alternative records into, the page it leads to, a + // field's name, the group it invites into. A rebuild that + // reorders or reshapes a page still hands each card its own + // words; on uhhm.no a positional match once put the old front + // door's three headings on three unrelated new cards. Items + // with no identity (an information feature) fall back to + // position. (serde_yaml::Value::Sequence(x), serde_yaml::Value::Sequence(y)) => { - for (va, vb) in x.iter().zip(y) { - collect_words(va, vb, found); + let mut used = vec![false; y.len()]; + for (i, va) in x.iter().enumerate() { + let ida = identity(va); + let hit = if ida.is_empty() { + (i < y.len() && !used[i] && identity(&y[i]).is_empty()).then_some(i) + } else { + (0..y.len()).find(|&j| !used[j] && identity(&y[j]).iter().any(|k| ida.contains(k))) + }; + if let Some(j) = hit { + used[j] = true; + collect_words(va, &y[j], found); + } } } (serde_yaml::Value::String(t), serde_yaml::Value::String(live)) if t.starts_with('\u{1}') => { @@ -1786,4 +1837,25 @@ mod invite_tests { assert!(lifted2["questions/trafikk/index.yaml"]["invites"]["styret"]["heading"].as_str().unwrap().starts_with("Invite someone")); let _ = std::fs::remove_dir_all(&dir); } + + #[test] + fn words_follow_their_card_when_the_live_page_is_ordered_differently() { + let plan: Plan = serde_yaml::from_str(PLAN).unwrap(); + let mut words = fallback_words(&plan); + words.get_mut("questions/index.yaml").unwrap()["needs"]["melde"]["heading"] = json!("Meld fra om en fare"); + let dir = std::env::temp_dir().join(format!("lift-order-{}", std::process::id())); + write_repo(&dir, render(&plan, &words).unwrap()).unwrap(); + // The live front door has an extra first card the plan knows + // nothing about, so the hazard form is no longer first. + let front = dir.join("questions/index.yaml"); + let mut page: serde_yaml::Value = serde_yaml::from_str(&std::fs::read_to_string(&front).unwrap()).unwrap(); + let alternatives = page["alternatives"].as_sequence_mut().unwrap(); + let stranger: serde_yaml::Value = serde_yaml::from_str("name: About us\ndescription: Founded in 1947.\nrecord_as: newsletter\nfeatures: []\n").unwrap(); + alternatives.insert(0, stranger); + std::fs::write(&front, serde_yaml::to_string(&page).unwrap()).unwrap(); + + let lifted = lift_words(&plan, &dir).unwrap(); + assert_eq!(lifted["questions/index.yaml"]["needs"]["melde"]["heading"], "Meld fra om en fare"); + let _ = std::fs::remove_dir_all(&dir); + } }