use std::path::{Path, PathBuf};
use zenith_core::{KdlAdapter, KdlSource, TokenType};
pub const EMBEDDED_PACKS: &[(&str, &str)] = &[
(
"@zenith/flowchart",
include_str!("../../assets/libraries/zenith-flowchart.zen"),
),
(
"@zenith/filters",
include_str!("../../assets/libraries/zenith-filters.zen"),
),
(
"@zenith/masks",
include_str!("../../assets/libraries/zenith-masks.zen"),
),
(
"@zenith/brand-kit",
include_str!("../../assets/libraries/zenith-brand-kit.zen"),
),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackSource {
Preset,
Project(PathBuf),
}
impl PackSource {
pub fn label(&self) -> &'static str {
match self {
PackSource::Preset => "preset",
PackSource::Project(_) => "project",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemKind {
Component,
Token,
Action,
}
impl ItemKind {
pub fn label(&self) -> &'static str {
match self {
ItemKind::Component => "component",
ItemKind::Token => "token",
ItemKind::Action => "action",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackItem {
pub id: String,
pub kind: ItemKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LibraryPack {
pub id: String,
pub version: Option<String>,
pub source: PackSource,
pub items: Vec<PackItem>,
}
pub(super) fn is_exportable_token(ty: &TokenType) -> bool {
matches!(ty, TokenType::Filter | TokenType::Mask)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackError {
pub message: String,
}
impl PackError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for PackError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for PackError {}
pub fn parse_pack(source: &str, source_kind: PackSource) -> Result<LibraryPack, PackError> {
let doc = KdlAdapter
.parse(source.as_bytes())
.map_err(|e| PackError::new(format!("parse error: {}", e)))?;
let project_id = doc.project.as_ref().map(|p| p.id.as_str());
let self_entry = project_id
.and_then(|pid| doc.libraries.iter().find(|lib| lib.id == pid))
.or(match doc.libraries.as_slice() {
[only] => Some(only),
_ => None,
});
let self_entry = self_entry.ok_or_else(|| {
PackError::new(
"pack has no identifying library self-entry (declare \
`libraries { library id=\"…\" version=\"…\" }`)",
)
})?;
let mut items: Vec<PackItem> = doc
.components
.iter()
.map(|c| PackItem {
id: c.id.clone(),
kind: ItemKind::Component,
})
.collect();
items.extend(
doc.tokens
.tokens
.iter()
.filter(|t| is_exportable_token(&t.token_type))
.map(|t| PackItem {
id: t.id.clone(),
kind: ItemKind::Token,
}),
);
items.extend(doc.actions.iter().map(|a| PackItem {
id: a.id.clone(),
kind: ItemKind::Action,
}));
Ok(LibraryPack {
id: self_entry.id.clone(),
version: self_entry.version.clone(),
source: source_kind,
items,
})
}
pub fn load_embedded_packs() -> Vec<LibraryPack> {
EMBEDDED_PACKS
.iter()
.filter_map(|(_, src)| parse_pack(src, PackSource::Preset).ok())
.collect()
}
pub fn load_project_packs(project_dir: &Path) -> Vec<LibraryPack> {
let libraries_dir = project_dir.join("libraries");
let entries = match std::fs::read_dir(&libraries_dir) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
let mut packs = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("zen") {
continue;
}
let source = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("note: skipping '{}': {}", path.display(), e);
continue;
}
};
match parse_pack(&source, PackSource::Project(path.clone())) {
Ok(pack) => packs.push(pack),
Err(e) => eprintln!("note: skipping '{}': {}", path.display(), e),
}
}
packs
}
pub fn resolve_packs(project_dir: Option<&Path>) -> Vec<LibraryPack> {
let mut packs = Vec::new();
if let Some(dir) = project_dir {
packs.extend(load_project_packs(dir));
}
packs.extend(load_embedded_packs());
packs.sort_by(|a, b| {
a.id.cmp(&b.id)
.then_with(|| source_rank(&a.source).cmp(&source_rank(&b.source)))
});
packs
}
fn source_rank(source: &PackSource) -> u8 {
match source {
PackSource::Project(_) => 0,
PackSource::Preset => 1,
}
}