use zenith_core::ast::document::Page;
use zenith_core::ast::token::TokenBlock;
use crate::engine::structure::{suffix_ids_in_children, suffix_zone_and_fold_ids};
pub fn reconcile_candidate_tokens(candidate_tokens: &TokenBlock, target_tokens: &mut TokenBlock) {
for cand_token in &candidate_tokens.tokens {
match target_tokens
.tokens
.iter_mut()
.find(|t| t.id == cand_token.id)
{
Some(existing) => {
*existing = cand_token.clone();
}
None => {
target_tokens.tokens.push(cand_token.clone());
}
}
}
}
pub fn merge_candidate_page(source: &Page, target: &mut Page, id_suffix: &str) {
let mut children = source.children.clone();
let mut safe_zones = source.safe_zones.clone();
let mut folds = source.folds.clone();
suffix_ids_in_children(&mut children, id_suffix);
suffix_zone_and_fold_ids(&mut safe_zones, &mut folds, id_suffix);
target.children = children;
target.safe_zones = safe_zones;
target.folds = folds;
}
#[cfg(test)]
mod tests {
use zenith_core::ast::token::{Token, TokenBlock, TokenLiteral, TokenType, TokenValue};
use zenith_core::{KdlAdapter, KdlSource};
use super::{merge_candidate_page, reconcile_candidate_tokens};
fn make_color_token(id: &str, value: &str) -> Token {
Token {
id: id.to_owned(),
token_type: TokenType::Color,
value: TokenValue::Literal(TokenLiteral::String(value.to_owned())),
source_span: None,
}
}
fn make_token_block(tokens: Vec<Token>) -> TokenBlock {
TokenBlock {
format: "zenith-token-v1".to_owned(),
tokens,
}
}
#[test]
fn reconcile_shared_id_replaced_in_place() {
let mut target = make_token_block(vec![
make_color_token("color.shared", "#ff0000"),
make_color_token("color.del-only", "#00ff00"),
]);
let candidate = make_token_block(vec![
make_color_token("color.shared", "#0000ff"),
make_color_token("color.cand-only", "#aabbcc"),
]);
reconcile_candidate_tokens(&candidate, &mut target);
assert_eq!(
target.tokens.len(),
3,
"reconciled block must have 3 tokens; got {:?}",
target.tokens.iter().map(|t| &t.id).collect::<Vec<_>>()
);
let shared = target
.tokens
.iter()
.find(|t| t.id == "color.shared")
.unwrap();
assert_eq!(
shared.value,
TokenValue::Literal(TokenLiteral::String("#0000ff".to_owned())),
"shared id must carry the candidate's value"
);
let del_only = target
.tokens
.iter()
.find(|t| t.id == "color.del-only")
.unwrap();
assert_eq!(
del_only.value,
TokenValue::Literal(TokenLiteral::String("#00ff00".to_owned())),
"deliverable-only token must be retained unchanged"
);
assert!(
target.tokens.iter().any(|t| t.id == "color.cand-only"),
"candidate-only token must be appended"
);
}
#[test]
fn reconcile_preserves_deliverable_order() {
let mut target = make_token_block(vec![
make_color_token("tok.a", "#aaa"),
make_color_token("tok.b", "#bbb"),
make_color_token("tok.c", "#ccc"),
]);
let candidate = make_token_block(vec![
make_color_token("tok.b", "#BBB"),
make_color_token("tok.d", "#ddd"),
]);
reconcile_candidate_tokens(&candidate, &mut target);
let ids: Vec<&str> = target.tokens.iter().map(|t| t.id.as_str()).collect();
assert_eq!(
ids,
["tok.a", "tok.b", "tok.c", "tok.d"],
"order must be deliverable order then appended candidate-only; got {ids:?}"
);
let tok_b = target.tokens.iter().find(|t| t.id == "tok.b").unwrap();
assert_eq!(
tok_b.value,
TokenValue::Literal(TokenLiteral::String("#BBB".to_owned()))
);
}
#[test]
fn reconcile_empty_candidate_leaves_target_unchanged() {
let mut target = make_token_block(vec![make_color_token("tok.a", "#aaa")]);
let candidate = make_token_block(vec![]);
reconcile_candidate_tokens(&candidate, &mut target);
assert_eq!(target.tokens.len(), 1);
assert_eq!(target.tokens[0].id, "tok.a");
}
#[test]
fn reconcile_empty_target_appends_all_candidate_tokens() {
let mut target = make_token_block(vec![]);
let candidate = make_token_block(vec![
make_color_token("tok.x", "#x"),
make_color_token("tok.y", "#y"),
]);
reconcile_candidate_tokens(&candidate, &mut target);
let ids: Vec<&str> = target.tokens.iter().map(|t| t.id.as_str()).collect();
assert_eq!(
ids,
["tok.x", "tok.y"],
"all candidate tokens must be appended in order"
);
}
fn parse_first_page(src: &str) -> zenith_core::ast::document::Page {
KdlAdapter
.parse(src.as_bytes())
.expect("test doc must parse")
.body
.pages
.into_iter()
.next()
.expect("test doc must have at least one page")
}
const SOURCE_DOC: &str = r##"zenith version=1 {
project id="proj.src" name="Src"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.src" title="Src" {
page id="page.source" w=(px)400 h=(px)300 {
rect id="rect.a" x=(px)0 y=(px)0 w=(px)100 h=(px)100
rect id="rect.b" x=(px)100 y=(px)0 w=(px)100 h=(px)100
}
}
}
"##;
const TARGET_DOC: &str = r##"zenith version=1 {
project id="proj.tgt" name="Tgt"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.tgt" title="Tgt" {
page id="page.target" w=(px)400 h=(px)300 {
rect id="old.rect" x=(px)0 y=(px)0 w=(px)50 h=(px)50
}
}
}
"##;
fn rect_id(node: &zenith_core::Node) -> Option<&str> {
match node {
zenith_core::Node::Rect(r) => Some(r.id.as_str()),
_ => None,
}
}
#[test]
fn children_copied_with_suffixed_ids() {
let source = parse_first_page(SOURCE_DOC);
let mut target = parse_first_page(TARGET_DOC);
merge_candidate_page(&source, &mut target, ".promoted");
assert_eq!(target.children.len(), 2, "target must have 2 children");
let ids: Vec<&str> = target.children.iter().filter_map(rect_id).collect();
assert!(
ids.contains(&"rect.a.promoted"),
"rect.a must be suffixed; got {ids:?}"
);
assert!(
ids.contains(&"rect.b.promoted"),
"rect.b must be suffixed; got {ids:?}"
);
}
#[test]
fn source_unchanged_after_merge() {
let source = parse_first_page(SOURCE_DOC);
let source_children_len = source.children.len();
let first_id = source
.children
.first()
.and_then(rect_id)
.unwrap()
.to_owned();
let mut target = parse_first_page(TARGET_DOC);
merge_candidate_page(&source, &mut target, ".p");
assert_eq!(
source.children.len(),
source_children_len,
"source must not be mutated"
);
assert_eq!(
source.children.first().and_then(rect_id),
Some(first_id.as_str()),
);
}
#[test]
fn empty_source_replaces_target_children() {
const EMPTY_SOURCE: &str = r##"zenith version=1 {
project id="proj.empty" name="E"
tokens format="zenith-token-v1" {}
styles {}
document id="doc.empty" title="E" {
page id="page.source" w=(px)100 h=(px)100 {}
}
}
"##;
let source = parse_first_page(EMPTY_SOURCE);
let mut target = parse_first_page(TARGET_DOC);
assert!(
!target.children.is_empty(),
"target must start with children"
);
merge_candidate_page(&source, &mut target, ".p");
assert!(
target.children.is_empty(),
"empty source must produce empty target children"
);
}
}