use super::cascade::{NodeFacts, Stylesheet};
use super::ir::{AttrMap, MarkerKind, Markers, ResolvedInst, ResolvedValue, ShapeKind, VarTable};
use super::merge::{collapse, resolve_markers};
use super::types::Types;
use super::value::resolve_groups;
use crate::error::Error;
use crate::span::Span;
use crate::syntax::ast::{Child, Node, Wire};
use std::collections::HashMap;
pub(super) const INHERITED_TEXT: &[&str] = &[
"font-family",
"font-size",
"font-weight",
"font-style",
"text-align",
"line-height",
"letter-spacing",
"color",
];
pub struct LiftedWire {
pub wire: Wire,
pub prefix: Vec<String>,
}
pub struct SceneCtx<'a> {
pub types: &'a Types<'a>,
pub sheet: &'a Stylesheet,
pub vars: &'a VarTable,
}
pub fn resolve_instances(
instances: &[Child],
ctx: &SceneCtx,
root_attrs: &AttrMap,
text_ctx: &AttrMap,
id_seen: &mut HashMap<String, Span>,
lifted: &mut Vec<LiftedWire>,
) -> Result<Vec<ResolvedInst>, Error> {
let mut ancestors = Vec::new();
let mut nodes = Vec::with_capacity(instances.len());
for child in instances {
nodes.push(resolve_child(
child,
ctx,
&mut ancestors,
&[],
text_ctx,
id_seen,
lifted,
)?);
}
drop_blank_text(&mut nodes, root_attrs);
Ok(nodes)
}
#[allow(clippy::too_many_arguments)]
fn resolve_child(
child: &Child,
ctx: &SceneCtx,
ancestors: &mut Vec<NodeFacts>,
path_prefix: &[String],
text_ctx: &AttrMap,
id_seen: &mut HashMap<String, Span>,
lifted: &mut Vec<LiftedWire>,
) -> Result<ResolvedInst, Error> {
match child {
Child::Box(n) => resolve_node(n, ctx, ancestors, path_prefix, text_ctx, id_seen, lifted),
Child::Text(t) => Ok(text_inst(&t.text, text_ctx, t.span)),
}
}
#[allow(clippy::too_many_arguments)]
pub fn resolve_node(
node: &Node,
ctx: &SceneCtx,
ancestors: &mut Vec<NodeFacts>,
path_prefix: &[String],
text_ctx: &AttrMap,
id_seen: &mut HashMap<String, Span>,
lifted: &mut Vec<LiftedWire>,
) -> Result<ResolvedInst, Error> {
let type_name = node.ty.as_deref().unwrap_or("box");
let rt = ctx.types.resolve(type_name, node.span)?;
for class in &node.classes {
if !ctx.sheet.defines_class(class) {
return Err(Error::at(node.span, format!("unknown class '.{}'", class)));
}
}
if let Some(id) = &node.id {
if is_reserved_id(id, ctx.types) {
return Err(reserved_error(node.span, id));
}
let full = join_path(path_prefix, id);
if let Some(prev) = id_seen.get(&full) {
return Err(Error::at(node.span, format!("duplicate id '{}'", id)).with_related(*prev));
}
id_seen.insert(full, node.span);
}
let mut facts_types = rt.type_chain.clone();
facts_types.push(rt.kind.as_str().to_string());
let facts = NodeFacts {
types: facts_types,
classes: node.classes.clone(),
};
let mut ordered: Vec<(String, ResolvedValue)> = rt.defaults.clone();
ordered.extend(ctx.sheet.node_layers(ancestors, &facts));
if let Some(block) = &node.block {
for d in &block.decls {
ordered.push((d.name.clone(), resolve_groups(&d.groups, d.span, ctx.vars)?));
}
}
let markers = resolve_markers(&ordered, MarkerKind::None, MarkerKind::None, node.span)?;
let attrs = collapse(&ordered);
if rt.kind == ShapeKind::Slant
&& let Some(skew) = attrs.number("skew")
&& (skew <= -89.0 || skew >= 89.0)
{
return Err(Error::at(
node.span,
format!("skew: {} must be in (-89, 89)", skew),
));
}
let mut child_text_ctx = text_ctx.clone();
for name in INHERITED_TEXT {
if let Some(v) = attrs.get(name) {
child_text_ctx.insert(*name, v.clone());
}
}
let mut child_prefix = path_prefix.to_vec();
if let Some(id) = &node.id {
child_prefix.push(id.clone());
}
for w in rt
.body_wires
.iter()
.chain(node.block.iter().flat_map(|b| &b.wires))
{
lifted.push(LiftedWire {
wire: w.clone(),
prefix: child_prefix.clone(),
});
}
let block_children = node
.block
.as_ref()
.map(|b| b.children.as_slice())
.unwrap_or(&[]);
let body: Vec<&Child> = rt
.body_children
.iter()
.chain(block_children.iter())
.collect();
let is_icon = rt.kind == ShapeKind::Icon;
let own_label = if is_icon {
first_text(&body)
.map(str::to_string)
.or_else(|| node.id.clone())
} else {
None
};
ancestors.push(facts);
let mut children = Vec::new();
if !is_icon {
for child in &body {
children.push(resolve_child(
child,
ctx,
ancestors,
&child_prefix,
&child_text_ctx,
id_seen,
lifted,
)?);
}
let text_capable = !matches!(
rt.kind,
ShapeKind::Line | ShapeKind::Poly | ShapeKind::Path | ShapeKind::Image
);
if text_capable
&& children.is_empty()
&& let Some(id) = &node.id
{
children.push(text_inst(id, &child_text_ctx, node.span));
}
}
ancestors.pop();
drop_blank_text(&mut children, &attrs);
Ok(ResolvedInst {
id: node.id.clone(),
shape: rt.kind,
type_chain: rt.type_chain,
applied_styles: node.classes.clone(),
label: own_label,
attrs,
markers,
children,
span: node.span,
})
}
fn text_inst(text: &str, text_ctx: &AttrMap, span: Span) -> ResolvedInst {
let mut attrs = AttrMap::new();
for name in INHERITED_TEXT {
if let Some(v) = text_ctx.get(name) {
attrs.insert(*name, v.clone());
}
}
ResolvedInst {
id: None,
shape: ShapeKind::Text,
type_chain: Vec::new(),
applied_styles: Vec::new(),
label: Some(text.to_string()),
attrs,
markers: Markers::default(),
children: Vec::new(),
span,
}
}
fn first_text<'a>(children: &[&'a Child]) -> Option<&'a str> {
children.iter().find_map(|c| match c {
Child::Text(t) => Some(t.text.as_str()),
Child::Box(_) => None,
})
}
fn drop_blank_text(children: &mut Vec<ResolvedInst>, container: &AttrMap) {
let is_grid = matches!(container.get("layout"), Some(ResolvedValue::Ident(s)) if s == "grid");
if !is_grid {
children.retain(|c| !is_blank_anon_text(c));
}
}
fn is_blank_anon_text(r: &ResolvedInst) -> bool {
r.id.is_none() && r.shape == ShapeKind::Text && r.label.as_deref().is_none_or(str::is_empty)
}
fn is_reserved_id(id: &str, types: &Types) -> bool {
types.is_known(id)
|| matches!(
id,
"wire" | "rect" | "circle" | "top" | "bottom" | "left" | "right"
)
}
pub(super) fn reserved_error(span: Span, name: &str) -> Error {
let mut cap = name.to_string();
if let Some(first) = cap.get_mut(0..1) {
first.make_ascii_uppercase();
}
Error::at(
span,
format!(
"'{}' is reserved (ids are case-sensitive — '{}' is free)",
name, cap
),
)
}
fn join_path(prefix: &[String], id: &str) -> String {
if prefix.is_empty() {
id.to_string()
} else {
format!("{}.{}", prefix.join("."), id)
}
}
pub struct PathIndex {
paths: Vec<String>,
}
impl PathIndex {
pub fn build(nodes: &[ResolvedInst]) -> Self {
let mut paths = Vec::new();
for n in nodes {
walk_paths(n, &mut Vec::new(), &mut paths);
}
Self { paths }
}
pub fn contains(&self, path: &str) -> bool {
self.paths.iter().any(|p| p == path)
}
pub fn resolve(&self, query: &[String]) -> Option<String> {
let joined = query.join(".");
self.contains(&joined).then_some(joined)
}
pub fn has_final_segment(&self, seg: &str) -> bool {
self.paths.iter().any(|p| final_segment(p) == seg)
}
pub fn suggest(&self, seg: &str, scope: &[String]) -> Vec<String> {
let prefix = if scope.is_empty() {
String::new()
} else {
format!("{}.", scope.join("."))
};
let mut hits: Vec<String> = self
.paths
.iter()
.filter(|p| final_segment(p) == seg)
.filter_map(|p| {
if prefix.is_empty() {
Some(p.clone())
} else {
p.strip_prefix(&prefix).map(str::to_string)
}
})
.collect();
hits.sort();
hits.dedup();
hits.truncate(3);
hits
}
}
fn final_segment(path: &str) -> &str {
path.rsplit('.').next().unwrap_or(path)
}
fn walk_paths(n: &ResolvedInst, stack: &mut Vec<String>, out: &mut Vec<String>) {
if let Some(id) = &n.id {
stack.push(id.clone());
out.push(stack.join("."));
}
for c in &n.children {
walk_paths(c, stack, out);
}
if n.id.is_some() {
stack.pop();
}
}