use std::collections::BTreeSet;
use zenith_core::{
AssetDecl, Dimension, Document, KdlAdapter, KdlSource, Node, Style, Token, Unit,
};
use super::registry::{EMBEDDED_PACKS, LibraryPack, PackSource};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddError {
pub message: String,
}
impl AddError {
pub(super) fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for AddError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for AddError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddOutcome {
pub pkg_id: String,
pub item: String,
pub target_component_id: String,
pub instance_id: String,
pub provenance_id: String,
pub warnings: Vec<String>,
}
pub fn parse_spec(spec: &str) -> Result<(String, String), AddError> {
let (pkg, item) = spec.split_once('#').ok_or_else(|| {
AddError::new(format!(
"malformed item spec {:?} (expected `<package>#<item>`, e.g. \
`@zenith/flowchart#decision`)",
spec
))
})?;
if pkg.is_empty() || item.is_empty() {
return Err(AddError::new(format!(
"malformed item spec {:?} (both package and item must be non-empty, \
e.g. `@zenith/flowchart#decision`)",
spec
)));
}
Ok((pkg.to_owned(), item.to_owned()))
}
pub fn load_pack_document(pack: &LibraryPack) -> Result<Document, AddError> {
let source = match &pack.source {
PackSource::Preset => EMBEDDED_PACKS
.iter()
.find(|(id, _)| *id == pack.id)
.map(|(_, src)| (*src).to_owned())
.ok_or_else(|| {
AddError::new(format!("embedded pack '{}' source not found", pack.id))
})?,
PackSource::Project(path) => std::fs::read_to_string(path).map_err(|e| {
AddError::new(format!("error reading pack '{}': {}", path.display(), e))
})?,
};
KdlAdapter
.parse(source.as_bytes())
.map_err(|e| AddError::new(format!("error parsing pack '{}': {}", pack.id, e)))
}
pub(super) fn unknown_package_error(pkg_id: &str, packs: &[LibraryPack]) -> AddError {
let mut available: Vec<&str> = packs.iter().map(|p| p.id.as_str()).collect();
available.sort_unstable();
available.dedup();
AddError::new(format!(
"unknown library package '{}' (available: {})",
pkg_id,
if available.is_empty() {
"none".to_owned()
} else {
available.join(", ")
}
))
}
pub(crate) fn sanitize_pkg(pkg_id: &str) -> String {
let mut out = String::with_capacity(pkg_id.len());
let mut prev_dot = false;
for ch in pkg_id.chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
out.push(ch);
prev_dot = ch == '.';
} else {
if !prev_dot && !out.is_empty() {
out.push('.');
prev_dot = true;
}
}
}
while out.ends_with('.') {
out.pop();
}
out
}
pub(crate) fn target_component_id(pkg_id: &str, item: &str) -> String {
format!("lib.{}.{}", sanitize_pkg(pkg_id), item)
}
fn collect_node_ids(children: &[Node], out: &mut BTreeSet<String>) {
for child in children {
match child {
Node::Rect(n) => {
out.insert(n.id.clone());
}
Node::Ellipse(n) => {
out.insert(n.id.clone());
}
Node::Line(n) => {
out.insert(n.id.clone());
}
Node::Text(n) => {
out.insert(n.id.clone());
}
Node::Code(n) => {
out.insert(n.id.clone());
}
Node::Image(n) => {
out.insert(n.id.clone());
}
Node::Polygon(n) => {
out.insert(n.id.clone());
}
Node::Polyline(n) => {
out.insert(n.id.clone());
}
Node::Frame(n) => {
out.insert(n.id.clone());
collect_node_ids(&n.children, out);
}
Node::Group(n) => {
out.insert(n.id.clone());
collect_node_ids(&n.children, out);
}
Node::Instance(n) => {
out.insert(n.id.clone());
}
Node::Field(n) => {
out.insert(n.id.clone());
}
Node::Toc(n) => {
out.insert(n.id.clone());
}
Node::Footnote(n) => {
out.insert(n.id.clone());
}
Node::Table(n) => {
out.insert(n.id.clone());
for row in &n.rows {
for cell in &row.cells {
collect_node_ids(&cell.children, out);
}
}
}
Node::Shape(n) => {
out.insert(n.id.clone());
}
Node::Connector(n) => {
out.insert(n.id.clone());
}
Node::Pattern(n) => {
out.insert(n.id.clone());
}
Node::Chart(n) => {
out.insert(n.id.clone());
}
Node::Light(n) => {
out.insert(n.id.clone());
}
Node::Mesh(n) => {
out.insert(n.id.clone());
}
Node::Unknown(n) => {
if let Some(id) = &n.id {
out.insert(id.clone());
}
collect_node_ids(&n.children, out);
}
}
}
}
pub fn collect_all_ids(doc: &Document) -> BTreeSet<String> {
let mut ids = BTreeSet::new();
if let Some(project) = &doc.project {
ids.insert(project.id.clone());
}
ids.insert(doc.body.id.clone());
for t in &doc.tokens.tokens {
ids.insert(t.id.clone());
}
for s in &doc.styles.styles {
ids.insert(s.id.clone());
}
for a in &doc.assets.assets {
ids.insert(a.id.clone());
}
for l in &doc.libraries {
ids.insert(l.id.clone());
}
for p in &doc.provenance {
ids.insert(p.id.clone());
}
for s in &doc.sections {
ids.insert(s.id.clone());
}
for comp in &doc.components {
ids.insert(comp.id.clone());
collect_node_ids(&comp.children, &mut ids);
}
for master in &doc.masters {
ids.insert(master.id.clone());
collect_node_ids(&master.children, &mut ids);
}
for page in &doc.body.pages {
ids.insert(page.id.clone());
collect_node_ids(&page.children, &mut ids);
}
ids
}
pub(super) fn unique_id(base: &str, taken: &BTreeSet<String>) -> String {
if !taken.contains(base) {
return base.to_owned();
}
let mut n = 1u64;
loop {
let candidate = format!("{}.{}", base, n);
if !taken.contains(&candidate) {
return candidate;
}
n += 1;
}
}
pub(crate) fn px(value: f64) -> Dimension {
Dimension {
value,
unit: Unit::Px,
}
}
pub(super) fn copy_tokens(pack: &[Token], target: &mut Vec<Token>, warnings: &mut Vec<String>) {
for tok in pack {
match target.iter().find(|t| t.id == tok.id) {
Some(existing)
if existing.token_type != tok.token_type || existing.value != tok.value =>
{
warnings.push(dependency_conflict("token", &tok.id));
}
Some(_) => {}
None => target.push(tok.clone()),
}
}
}
pub(super) fn copy_styles(pack: &[Style], target: &mut Vec<Style>, warnings: &mut Vec<String>) {
for st in pack {
match target.iter().find(|t| t.id == st.id) {
Some(existing) if existing.properties != st.properties => {
warnings.push(dependency_conflict("style", &st.id));
}
Some(_) => {}
None => target.push(st.clone()),
}
}
}
pub(super) fn copy_assets(
pack: &[AssetDecl],
target: &mut Vec<AssetDecl>,
warnings: &mut Vec<String>,
) {
for asset in pack {
match target.iter().find(|a| a.id == asset.id) {
Some(existing)
if existing.kind != asset.kind
|| existing.src != asset.src
|| existing.sha256 != asset.sha256 =>
{
warnings.push(dependency_conflict("asset", &asset.id));
}
Some(_) => {}
None => target.push(asset.clone()),
}
}
}
pub(super) fn dependency_conflict(kind: &str, id: &str) -> String {
format!(
"library.dependency_conflict: {} '{}' already exists in the target with a \
different definition; kept the existing one",
kind, id
)
}