use zenith_core::ast::document::{Fold, SafeZone};
use zenith_core::{Diagnostic, Document, Node};
use super::super::{
find_node_shared, node_id_of, node_kind_str, record_affected, subtree_contains,
};
fn node_is_container(node: &Node) -> bool {
matches!(
node,
Node::Frame(_) | Node::Group(_) | Node::Instance(_) | Node::Table(_)
)
}
fn node_set_id(node: &mut Node, new_id: String) -> bool {
match node {
Node::Rect(r) => {
r.id = new_id;
true
}
Node::Ellipse(e) => {
e.id = new_id;
true
}
Node::Line(l) => {
l.id = new_id;
true
}
Node::Text(t) => {
t.id = new_id;
true
}
Node::Code(c) => {
c.id = new_id;
true
}
Node::Image(i) => {
i.id = new_id;
true
}
Node::Polygon(p) => {
p.id = new_id;
true
}
Node::Polyline(p) => {
p.id = new_id;
true
}
Node::Field(f) => {
f.id = new_id;
true
}
Node::Toc(t) => {
t.id = new_id;
true
}
Node::Footnote(f) => {
f.id = new_id;
true
}
Node::Shape(s) => {
s.id = new_id;
true
}
Node::Connector(c) => {
c.id = new_id;
true
}
Node::Pattern(p) => {
p.id = new_id;
true
}
Node::Chart(c) => {
c.id = new_id;
true
}
Node::Frame(_) | Node::Group(_) | Node::Instance(_) | Node::Table(_) | Node::Unknown(_) => {
false
}
}
}
fn duplicate_in_children(children: &mut Vec<Node>, id: &str, new_id: &str) -> bool {
let direct = children.iter().position(|n| node_id_of(n) == Some(id));
if let Some(i) = direct {
if let Some(src) = children.get(i) {
let mut clone = src.clone();
node_set_id(&mut clone, new_id.to_owned());
children.insert(i + 1, clone);
return true;
}
}
for child in children.iter_mut() {
let lists: Vec<&mut Vec<Node>> = match child {
Node::Frame(f) => vec![&mut f.children],
Node::Group(g) => vec![&mut g.children],
Node::Table(t) => t
.rows
.iter_mut()
.flat_map(|row| row.cells.iter_mut().map(|cell| &mut cell.children))
.collect(),
Node::Unknown(u) => vec![&mut u.children],
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_) => Vec::new(),
};
for list in lists {
if duplicate_in_children(list, id, new_id) {
return true;
}
}
}
false
}
pub(in crate::engine) fn apply_duplicate_node(
node_id: &str,
new_id: &str,
doc: &mut Document,
diagnostics: &mut Vec<Diagnostic>,
affected: &mut Vec<String>,
) {
let page_index = doc.body.pages.iter().enumerate().find_map(|(pi, page)| {
let found = page.children.iter().any(|n| subtree_contains(n, node_id));
if found { Some(pi) } else { None }
});
let pi = match page_index {
Some(pi) => pi,
None => {
diagnostics.push(Diagnostic::error(
"tx.unknown_node",
format!("node {:?} not found in document", node_id),
None,
Some(node_id.to_owned()),
));
return;
}
};
{
let Some(page) = doc.body.pages.get(pi) else {
return; };
if let Some(src) = find_node_shared(&page.children, node_id)
&& node_is_container(src)
{
let kind = node_kind_str(src);
diagnostics.push(Diagnostic::error(
"tx.unsupported_property",
format!(
"duplicating a {} is not supported in v0; re-id'ing a subtree \
is deferred — only leaf nodes may be duplicated",
kind
),
None,
Some(node_id.to_owned()),
));
return;
}
}
let Some(page) = doc.body.pages.get_mut(pi) else {
return; };
duplicate_in_children(&mut page.children, node_id, new_id);
record_affected(new_id, affected);
}
pub(crate) fn suffix_ids_in_children(children: &mut [Node], id_suffix: &str) {
for child in children.iter_mut() {
if let Some(old_id) = node_id_of(child) {
let new_id = format!("{old_id}{id_suffix}");
node_set_id_any(child, new_id);
}
match child {
Node::Frame(f) => suffix_ids_in_children(&mut f.children, id_suffix),
Node::Group(g) => suffix_ids_in_children(&mut g.children, id_suffix),
Node::Table(t) => {
for row in &mut t.rows {
for cell in &mut row.cells {
suffix_ids_in_children(&mut cell.children, id_suffix);
}
}
}
Node::Unknown(u) => suffix_ids_in_children(&mut u.children, id_suffix),
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_) => {}
}
}
}
pub(crate) fn suffix_zone_and_fold_ids(
safe_zones: &mut [SafeZone],
folds: &mut [Fold],
id_suffix: &str,
) {
for zone in safe_zones.iter_mut() {
zone.id.push_str(id_suffix);
zone.source_span = None;
}
for fold in folds.iter_mut() {
fold.id.push_str(id_suffix);
fold.source_span = None;
}
}
pub(in crate::engine) fn node_set_id_any(node: &mut Node, new_id: String) {
match node {
Node::Frame(f) => f.id = new_id,
Node::Group(g) => g.id = new_id,
Node::Instance(i) => i.id = new_id,
Node::Table(t) => t.id = new_id,
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Field(_)
| Node::Toc(_)
| Node::Footnote(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_) => {
node_set_id(node, new_id);
}
Node::Unknown(u) => {
if u.id.is_some() {
u.id = Some(new_id);
}
}
}
}
pub(in crate::engine) fn apply_duplicate_page(
page_id: &str,
new_id: &str,
id_suffix: &str,
doc: &mut Document,
diagnostics: &mut Vec<Diagnostic>,
affected: &mut Vec<String>,
) {
let Some(position) = doc.body.pages.iter().position(|p| p.id == page_id) else {
diagnostics.push(Diagnostic::error(
"tx.unknown_node",
format!("duplicate_page: page {:?} not found", page_id),
None,
Some(page_id.to_owned()),
));
return;
};
if id_suffix.is_empty() {
diagnostics.push(Diagnostic::advisory(
"tx.noop",
format!(
"duplicate_page: empty id_suffix will not keep cloned node ids \
unique for page {:?}; the transaction will be rejected",
page_id
),
None,
Some(page_id.to_owned()),
));
}
let Some(source) = doc.body.pages.get(position) else {
return; };
let mut clone = source.clone();
clone.id = new_id.to_owned();
clone.source_span = None;
suffix_ids_in_children(&mut clone.children, id_suffix);
suffix_zone_and_fold_ids(&mut clone.safe_zones, &mut clone.folds, id_suffix);
doc.body.pages.insert(position + 1, clone);
record_affected(new_id, affected);
}