//! Library commands: publish this file as a library (to its file, or to a server for the team); //! enable, use and update other libraries (their components, styles and variables), swap one for //! another, and see how files use a team library. use std::collections::BTreeSet; use std::sync::Arc; use figcraft_doc::*; use serde_json::{Value, json}; use figcraft_collab::message::{LibraryReply, LibraryReq}; use super::CommandSpec; use crate::library::*; use crate::library_server::{archive, is_server_link, read_archive, shown_path}; use crate::live::{NetReply, NetRequest, PublishOptions}; use crate::params::*; use crate::{EngineError, Result, Session, bad, spec}; /// Longest library description kept (chars). const MAX_DESCRIPTION: usize = 2000; pub fn specs() -> Vec { vec![ spec!("libraries.publish", "Publish library…", "{} (opens the publish form in the Assets panel)", false, |s, _| { s.request_ui("panel.libraries", json!({"publish": true})); Ok(json!({"ok": true})) }), spec!( "library.publish", "Publish library", "{description?, include? [component/set ids, style ids, variable collection ids or names: exactly these], exclude? [same]} publishes this file's components, component sets, styles and variable collections (by default all but names starting with . or _, or what the last publish offered; variables hidden from publishing stay out unless something published uses them) as a new library version; save the file so other files see it", true, publish_cmd ), spec!( "library.list", "List libraries", "{assets? (also list each library's components, styles, variable collections and variables, and what this file can publish)} → enabled libraries (version, updates, errors) and this file's published library", false, list ), spec!( "library.publishToServer", "Publish library to server", "{server? (an edit link of this file on a figcraft-server; omit while this file is in a live session on a server, or give the server's address with its key to keep the file there first), key?, description?, include?, exclude? (as library.publish)} publishes the library as the next version of the one kept on the server under this file (per-asset versions as library.publish); files that enabled it with a link of the file see the update → {id, version, components, styles, collections, variables, file?, edit?, view? (links when the file was kept there now)}", false, publish_to_server ), spec!( "library.add", "Add library", "{path (.figcraft file published as a library, or a share link of a file on a server: a view link is enough)}", true, add ), spec!( "library.usage", "Library usage", "{library? (an enabled team library: id, name or link; omit for this file's library on its server)} → what files report: {id, name, instances, files: [{name, updated, instances}], components: [{id, name, instances, files}]}", false, usage_cmd ), spec!( "library.swap", "Swap library", "{from (an enabled library: id, name or path), to (another enabled library)} re-points every component instance, style and variable copied from `from` to the published one of the same name in `to` (instances keep their overrides) → {components, styles, variables, unmatched: [names still from `from`], removed (nothing of `from` is used any more: it was removed)}", true, swap_cmd ), spec!( "library.remove", "Remove library", "{id (library id, name or path)} (copies still in use stay; unused ones are dropped)", true, remove ), spec!( "library.updates", "Library updates", "{} → the components, styles, variable collections and variables with a newer published version, and variables new to a library collection this file extends (listed without an `id`: no copy yet)", false, updates ), spec!( "library.acceptUpdates", "Accept library updates", "{ids? [library ids; all when omitted]} pulls updated components, styles, variable collections and variables in (instances keep their overrides; bound layers follow)", true, accept ), spec!( "library.place", "Place library component", "{library (id, name or path), component (id or name of a published component, set or variant), x?, y? (page point; default the view's centre)}", true, place ), spec!( "library.swapAt", "Swap instance to library component", "{library, component (id or name of a published component, set or variant), x, y (page point)} copies the component in and swaps the instance there to it (as instance.swapAt)", true, |s, p| { let (_, lib, key) = component_arg(s, "library.swapAt", p)?; let map = import(s.doc_mut()?, &lib, &[key], false)?; let local = map.get(&key).copied().ok_or_else(|| EngineError::Other("the component didn't import".into()))?; super::component::swap_at(s, &with(p, &["x", "y"], json!({"component": local}))) } ), spec!( "library.import", "Import library component", "{library, component} → {component (local, read-only main to instantiate with instance.create)}", true, |s, p| { let (_, lib, key) = component_arg(s, "library.import", p)?; let map = import(s.doc_mut()?, &lib, &[key], false)?; let local = map.get(&key).copied().ok_or_else(|| EngineError::Other("the component didn't import".into()))?; Ok(json!({"component": local})) } ), spec!( "library.applyStyle", "Apply library style", "{library, style (id or name of a published style), ids?, target? (fill|stroke)} copies the style in (linked, read-only) and applies it", true, apply_style ), spec!( "library.importVariable", "Import library variable", "{library, variable (id or name of a published variable)} → {variable (local, read-only linked copy to use with variable.bind, or as an alias in variable.set)}", true, |s, p| { let (lib, key) = variable_arg(s, "library.importVariable", p, "variable")?; Ok(json!({"variable": import_variable(s, &lib, &key)?})) } ), spec!( "library.bindVariable", "Bind library variable", "{library, variable (id or name of a published variable), field (as variable.bind), ids?} copies the variable in (linked, read-only) and binds it", true, |s, p| { let (lib, key) = variable_arg(s, "library.bindVariable", p, "variable")?; let local = import_variable(s, &lib, &key)?; super::styles::bind(s, &with(p, &["ids", "id", "field"], json!({"variable": local})))?; Ok(json!({"variable": local})) } ), spec!( "library.aliasVariable", "Alias library variable", "{library, alias (id or name of a published variable of the same type), variable (a local variable), mode? (default first)} copies the library variable in (linked, read-only) and makes the local variable's value an alias of it", true, alias ), spec!( "library.setVariableMode", "Set library variable mode", "{library, collection (id or name of a published collection), mode (id or name), ids? (frames; the page when omitted)} copies the collection in (linked, read-only) and sets its mode on the frames", true, set_mode ), spec!( "library.extendCollection", "Extend library collection", "{library, collection (id or name of a published collection), name? (default \" (extended)\")} → {id}: a collection of this file with the library collection's modes and variables (they follow its updates); override their values per mode with variable.set (variable.resetOverride goes back to the inherited value) and add variables of its own", true, extend ), ] } /// `base` with `p`'s values of `keys` (those it has). fn with(p: &Value, keys: &[&str], mut base: Value) -> Value { for k in keys { if let Some(v) = get(p, k) { base[*k] = v.clone(); } } base } /// An enabled library of the active document (`library` param), read from its file. fn library(s: &mut Session, cmd: &str, p: &Value) -> Result<(LibraryLink, Arc)> { let key = req_string(cmd, p, "library")?; let link = find_link(s.doc()?, &key) .cloned() .ok_or_else(|| bad(cmd, format!("no enabled library `{key}` (library.list shows them; library.add enables one)")))?; let lib = s.load_library(&link.path)?; if lib.manifest.id != link.id { return Err(EngineError::Other(format!( "{} now holds another library (\"{}\"): remove \"{}\" and add the file again", shown_path(&link.path), lib.manifest.name, link.name ))); } Ok((link, lib)) } /// The `library` and published `component` params: (link, library, the component's snapshot id). fn component_arg(s: &mut Session, cmd: &str, p: &Value) -> Result<(LibraryLink, Arc, NodeId)> { let (link, lib) = library(s, cmd, p)?; let v = get(p, "component").ok_or_else(|| bad(cmd, "`component` (id or name) required"))?; let m = &lib.manifest; let offered = |id: NodeId| asset_root(&lib.doc, id).and_then(|r| m.component(r)).is_some_and(|a| a.published); let by_name = || v.as_str().and_then(|t| m.components.iter().find(|a| a.published && a.name == t)).map(|a| a.id); let key = id_of(v).filter(|i| offered(*i)).or_else(by_name).ok_or_else(|| bad(cmd, format!("\"{}\" publishes no component {v}", link.name)))?; Ok((link, lib, key)) } /// The `library` and a published variable named by `param` (id, else name): (library, the /// variable's id in the library). fn variable_arg(s: &mut Session, cmd: &str, p: &Value, param: &str) -> Result<(Arc, VariableId)> { let (link, lib) = library(s, cmd, p)?; let key = req_string(cmd, p, param)?; let id = published_variables(&lib).find(|v| v.id == key).or_else(|| published_variables(&lib).find(|v| v.name == key)).map(|v| v.id.clone()); let id = id.ok_or_else(|| bad(cmd, format!("\"{}\" publishes no variable `{key}`", link.name)))?; Ok((lib, id)) } /// Copy library variable `key` in (with what it aliases); its local id. fn import_variable(s: &mut Session, lib: &PublishedLibrary, key: &str) -> Result { let map = import_variables(s.doc_mut()?, lib, &BTreeSet::from([key.to_string()]), &BTreeSet::new(), false); map.variables.get(key).cloned().ok_or_else(|| EngineError::Other("the variable didn't import".into())) } fn alias(s: &mut Session, p: &Value) -> Result { let cmd = "library.aliasVariable"; let own = req_string(cmd, p, "variable")?; let doc = s.doc()?; let kind = doc.variables.get(&own).filter(|v| !is_remote_variable(doc, &v.id)).map(|v| v.kind); let kind = kind.ok_or_else(|| bad(cmd, format!("no local variable `{own}`")))?; let (lib, key) = variable_arg(s, cmd, p, "alias")?; if lib.doc.variables.get(&key).is_some_and(|v| v.kind != kind) { return Err(bad(cmd, "the library variable has another type")); } let local = import_variable(s, &lib, &key)?; super::styles::var_set(s, &with(p, &["variable", "mode"], json!({"value": {"alias": local}})))?; Ok(json!({"variable": local})) } fn set_mode(s: &mut Session, p: &Value) -> Result { let cmd = "library.setVariableMode"; let (link, lib) = library(s, cmd, p)?; let key = req_string(cmd, p, "collection")?; let coll = published_collections(&lib) .find(|c| c.id == key) .or_else(|| published_collections(&lib).find(|c| c.name == key)) .ok_or_else(|| bad(cmd, format!("\"{}\" publishes no variable collection `{key}`", link.name)))?; let m = req_string(cmd, p, "mode")?; let mode = coll.modes.iter().find(|x| x.id == m || x.name == m).map(|x| x.id.clone()).ok_or_else(|| bad(cmd, "no such mode"))?; let key = coll.id.clone(); let map = import_variables(s.doc_mut()?, &lib, &BTreeSet::new(), &BTreeSet::from([key.clone()]), false); let local = map.collections.get(&key).cloned().ok_or_else(|| EngineError::Other("the collection didn't import".into()))?; super::styles::set_mode(s, &with(p, &["ids", "id"], json!({"collection": local, "mode": mode})))?; Ok(json!({"collection": local})) } fn extend(s: &mut Session, p: &Value) -> Result { let cmd = "library.extendCollection"; let (link, lib) = library(s, cmd, p)?; let key = req_string(cmd, p, "collection")?; let coll = published_collections(&lib) .find(|c| c.id == key) .or_else(|| published_collections(&lib).find(|c| c.name == key)) .cloned() .ok_or_else(|| bad(cmd, format!("\"{}\" publishes no variable collection `{key}`", link.name)))?; let vars: BTreeSet = published_variables(&lib).filter(|v| v.collection == coll.id).map(|v| v.id.clone()).collect(); let doc = s.doc_mut()?; let map = import_variables(doc, &lib, &vars, &BTreeSet::from([coll.id.clone()]), false); let parent = map.collections.get(&coll.id).cloned().ok_or_else(|| EngineError::Other("the collection didn't import".into()))?; let base = string(p, "name").map(|n| n.trim().chars().take(200).collect::()).filter(|n| !n.is_empty()); let base = base.unwrap_or_else(|| format!("{} (extended)", coll.name)); let name = super::variables::free_collection_name(doc, &base); let id = doc.new_collection_id(); doc.variables.collections.push(Collection { id: id.clone(), name, modes: Vec::new(), extends: Some(parent) }); super::variables::sync_extensions(doc); Ok(json!({"id": id})) } /// What `p` asks `cmd` to publish of `doc` (`include`, else the default after `prev`; less /// `exclude`) and its description. fn publish_args(cmd: &str, doc: &Document, p: &Value, prev: Option<&LibraryManifest>) -> Result<(Selection, Option)> { let all = publishable(doc); let coll_named = |c: &String, t: &str| doc.variables.collections.iter().any(|x| x.id == *c && x.name == t); let count = |s: &Selection| s.components.len() + s.styles.len() + s.collections.len(); let pick = |key: &str| -> Result { let mut sel = Selection::default(); let Some(v) = get(p, key) else { return Ok(sel) }; let Value::Array(items) = v else { return Err(bad(cmd, format!("`{key}` must be an array of component ids and style or variable collection ids or names"))); }; for item in items { let text = item.as_str().map(str::trim); let before = count(&sel); if let Some(t) = text.filter(|t| all.styles.iter().any(|x| x == t)) { sel.styles.push(t.to_string()); } else if let Some(t) = text.filter(|t| all.collections.iter().any(|x| x == t)) { sel.collections.push(t.to_string()); } else if let Some(i) = id_of(item).filter(|i| all.components.contains(i)) { sel.components.push(i); } else if let Some(t) = text { sel.components.extend(all.components.iter().filter(|i| doc.node(**i).is_some_and(|n| n.name == t))); sel.styles.extend(all.styles.iter().filter(|x| doc.style(x).is_some_and(|y| y.name == t)).cloned()); sel.collections.extend(all.collections.iter().filter(|c| coll_named(c, t)).cloned()); } if count(&sel) == before { return Err(bad(cmd, format!("{item} is not a local component, component set, style or variable collection of this file"))); } } Ok(sel) }; let mut sel = if get(p, "include").is_some() { pick("include")? } else { default_selection(doc, prev) }; let x = pick("exclude")?; sel.components.retain(|c| !x.components.contains(c)); sel.styles.retain(|s| !x.styles.contains(s)); sel.collections.retain(|c| !x.collections.contains(c)); let description = string(p, "description").map(|d| d.trim().chars().take(MAX_DESCRIPTION).collect()); Ok((sel, description)) } /// What a publish answers. fn published_json(sel: &Selection, lib: &PublishedLibrary) -> Value { let m = &lib.manifest; json!({ "id": m.id, "version": m.version, "components": sel.components.len(), "styles": sel.styles.len(), "collections": sel.collections.len(), "variables": m.variables.iter().filter(|v| v.published).count(), }) } fn publish_cmd(s: &mut Session, p: &Value) -> Result { let doc = s.doc()?; let prev = doc.published.clone(); let (sel, description) = publish_args("library.publish", doc, p, prev.as_ref().map(|l| &l.manifest))?; let lib = publish(doc, prev.as_deref(), &sel, description, s.ds()?.uid)?; let out = published_json(&sel, &lib); s.doc_mut()?.published = Some(Arc::new(lib)); Ok(out) } /// A library request's answer that isn't the one expected. fn unexpected(r: &LibraryReply) -> EngineError { EngineError::Other(format!("the server answered {r:?}")) } fn publish_to_server(s: &mut Session, p: &Value) -> Result { let cmd = "library.publishToServer"; let key = string(p, "key").map(|k| k.trim().to_string()).filter(|k| !k.is_empty()); let server = string(p, "server").map(|v| v.trim().to_string()).filter(|v| !v.is_empty()); let (doc, uid) = s.ds().map(|ds| (ds.doc.clone(), ds.uid))?; let mut kept = None; let link = match server { // The server's address with its key: the file is kept there first. Some(server) if key.is_some() || (server.contains("key=") && !server.contains("token=")) => { let opts = PublishOptions { server, key, name: doc.name.clone(), me: String::new(), doc: doc.clone(), share: false }; let file = match s.net(NetRequest::Upload(opts))?.0 { NetReply::Upload(r) => r.map_err(|e| bad(cmd, e))?, _ => return Err(EngineError::Other("the server answered something else".into())), }; let edit = file.edit.clone(); kept = Some(file); edit } Some(link) => link, None => s.server_link().ok_or_else(|| { bad(cmd, "give `server` (an edit link of this file on a server, or the server's address with its key), or publish this file to a server first (Share ▸ Publish to server…)") })?, }; // The server's version is the one to follow (others may have published since). let current = match s.library_request(&link, LibraryReq::Fetch { have: None, usage: None })? { LibraryReply::Library { archive, .. } => Some(read_archive(&archive).map_err(EngineError::Other)?), LibraryReply::Empty => None, other => return Err(unexpected(&other)), }; let prev = current.as_ref().or(doc.published.as_deref()); let (sel, description) = publish_args(cmd, &doc, p, prev.map(|l| &l.manifest))?; let lib = match (publish(&doc, prev, &sel, description, uid), &doc.published) { (Ok(lib), _) => lib, // This file's library as last published here, not on the server yet: as it is. (Err(_), Some(own)) if current.is_none() && !sel.is_empty() => (**own).clone(), (Err(e), _) => return Err(e), }; let lib = match s.library_exchange(&link, LibraryReq::Publish { archive: archive(&lib)? })? { // Run again in the background: what the server keeps is what went out then (a new copy // differs at least in its time). (LibraryReply::Published { .. }, Some(LibraryReq::Publish { archive })) => read_archive(&archive).map_err(EngineError::Other)?, (LibraryReply::Published { .. }, _) => lib, (other, _) => return Err(unexpected(&other)), }; let mut out = published_json(&sel, &lib); s.libraries.insert(&link, Ok(Arc::new(lib))); if let Some(f) = kept { out["file"] = json!(f.file); out["edit"] = json!(f.edit); out["view"] = json!(f.view); } Ok(out) } fn usage_cmd(s: &mut Session, p: &Value) -> Result { let cmd = "library.usage"; let link = match string(p, "library") { Some(key) => { let l = find_link(s.doc()?, &key).ok_or_else(|| bad(cmd, format!("no enabled library `{key}`")))?; if !is_server_link(&l.path) { return Err(bad(cmd, format!("\"{}\" is a library file, not a team library: only a server knows which files use it", l.name))); } l.path.clone() } None => s.server_link().ok_or_else(|| bad(cmd, "name an enabled team library, or open this file from its server"))?, }; let lib = s.load_library(&link)?; let files = match s.library_request(&link, LibraryReq::Usage)? { LibraryReply::Usage(f) => f, other => return Err(unexpected(&other)), }; let m = &lib.manifest; // Component → (instances, files using it). let mut per: std::collections::BTreeMap = std::collections::BTreeMap::new(); let mut rows = Vec::new(); for f in &files { let mut n = 0u64; for (c, k) in &f.usage.components { let e = per.entry(*c).or_default(); e.0 += u64::from(*k); e.1 += 1; n += u64::from(*k); } rows.push(json!({"name": f.usage.file, "updated": f.updated, "instances": n})); } let mut comps: Vec<(NodeId, u64, u64)> = per.into_iter().map(|(c, (n, f))| (c, n, f)).collect(); comps.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); let components: Vec = comps.iter().map(|(c, n, f)| json!({"id": c, "name": m.component(*c).map(|a| a.name.clone()), "instances": n, "files": f})).collect(); Ok(json!({ "id": m.id, "name": m.name, "instances": comps.iter().map(|c| c.1).sum::(), "files": rows, "components": components, })) } fn swap_cmd(s: &mut Session, p: &Value) -> Result { let cmd = "library.swap"; let from = req_string(cmd, p, "from")?; let from = find_link(s.doc()?, &from).map(|l| l.id.clone()).ok_or_else(|| bad(cmd, format!("no enabled library `{from}`")))?; let to = req_string(cmd, p, "to")?; let (link, lib) = library(s, cmd, &json!({"library": to}))?; if link.id == from { return Err(bad(cmd, "`from` and `to` are the same library")); } let r = crate::library_swap::swap_library(s.doc_mut()?, &from, &lib)?; Ok(json!({"components": r.components, "styles": r.styles, "variables": r.variables, "unmatched": r.unmatched, "removed": r.removed})) } fn assets_json(lib: &PublishedLibrary) -> Value { let m = &lib.manifest; let modes = |c: &str| lib.doc.variables.collection(c).map(|c| c.modes.iter().map(|x| json!({"id": x.id, "name": x.name})).collect::>()); json!({ "components": m.components.iter().filter(|a| a.published).map(|a| json!({"id": a.id, "name": a.name, "set": a.set, "version": a.version})).collect::>(), "styles": m.styles.iter().filter(|x| x.published).map(|x| json!({"id": x.id, "name": x.name, "type": x.kind, "version": x.version})).collect::>(), "collections": m.collections.iter().filter(|x| x.published).map(|x| json!({"id": x.id, "name": x.name, "modes": modes(&x.id), "version": x.version})).collect::>(), "variables": m.variables.iter().filter(|x| x.published).map(|x| json!({"id": x.id, "name": x.name, "type": x.kind, "collection": x.collection, "version": x.version})).collect::>(), }) } fn list(s: &mut Session, p: &Value) -> Result { let with_assets = boolean(p, "assets").unwrap_or(false); let links = s.doc()?.libraries.enabled.clone(); let mut out = Vec::new(); for l in links { let mut e = json!({"id": l.id, "name": l.name, "path": l.path, "server": is_server_link(&l.path), "seenVersion": l.version}); match s.fetch_library(&l.path, true) { Ok(lib) if lib.manifest.id == l.id => { e["version"] = json!(lib.manifest.version); e["description"] = json!(lib.manifest.description); e["updates"] = json!(pending(s.doc()?, &lib).count()); if with_assets && let (Value::Object(e), Value::Object(a)) = (&mut e, assets_json(&lib)) { e.extend(a); } } Ok(lib) => e["error"] = json!(format!("the file now holds another library (\"{}\")", lib.manifest.name)), Err(err) => e["error"] = json!(err.to_string()), } out.push(e); } let doc = s.doc()?; let published = doc.published.as_ref().map(|lib| { let m = &lib.manifest; let mut v = json!({"id": m.id, "name": m.name, "version": m.version, "description": m.description, "time": m.time, "components": m.components.iter().filter(|a| a.published).count(), "styles": m.styles.iter().filter(|x| x.published).count(), "collections": m.collections.iter().filter(|x| x.published).count(), "variables": m.variables.iter().filter(|x| x.published).count()}); if with_assets { v["assets"] = assets_json(lib); } v }); let mut res = json!({"libraries": out, "published": published}); if with_assets { let (def, all) = (default_selection(doc, doc.published.as_ref().map(|l| &l.manifest)), publishable(doc)); res["publishable"] = json!({ "components": all.components.iter().filter_map(|c| doc.node(*c).map(|n| json!({"id": c, "name": n.name, "selected": def.components.contains(c)}))).collect::>(), "styles": all.styles.iter().filter_map(|x| doc.style(x).map(|st| json!({"id": st.id, "name": st.name, "selected": def.styles.contains(x)}))).collect::>(), "collections": all.collections.iter().filter_map(|x| doc.variables.collection(x).map(|c| json!({"id": c.id, "name": c.name, "selected": def.collections.contains(x)}))).collect::>(), }); } Ok(res) } fn add(s: &mut Session, p: &Value) -> Result { let cmd = "library.add"; let raw = req_string(cmd, p, "path")?; let path = if is_server_link(&raw) { raw.trim().to_string() } else { absolute_path(raw.trim()) }; let lib = s.load_library(&path)?; let m = &lib.manifest; let doc = s.doc()?; if doc.published.as_ref().is_some_and(|me| me.manifest.id == m.id) { return Err(EngineError::Disabled(cmd.into(), format!("this file is the library \"{}\"", m.name))); } let unchanged = doc.libraries.link(&m.id).is_some_and(|l| l.path == path && l.name == m.name); if !unchanged { let libs = s.doc_mut()?.libraries_mut(); match libs.enabled.iter_mut().find(|l| l.id == m.id) { // The same library from another place (the file moved): follow it. Some(l) => { l.path = path.clone(); l.name = m.name.clone(); } None => libs.enabled.push(LibraryLink { id: m.id.clone(), name: m.name.clone(), path: path.clone(), version: m.version }), } } let mut out = assets_json(&lib); out["id"] = json!(m.id); out["name"] = json!(m.name); out["version"] = json!(m.version); out["path"] = json!(path); Ok(out) } fn remove(s: &mut Session, p: &Value) -> Result { let cmd = "library.remove"; let key = req_string(cmd, p, "id")?; let id = find_link(s.doc()?, &key).map(|l| l.id.clone()).ok_or_else(|| bad(cmd, format!("no enabled library `{key}`")))?; let doc = s.doc_mut()?; doc.libraries_mut().enabled.retain(|l| l.id != id); prune(doc, &id)?; Ok(json!({"removed": id})) } fn updates(s: &mut Session, _p: &Value) -> Result { let links = s.doc()?.libraries.enabled.clone(); let (mut out, mut errors, mut count) = (Vec::new(), Vec::new(), 0); for l in links { let lib = match s.fetch_library(&l.path, true) { Ok(lib) if lib.manifest.id == l.id => lib, Ok(_) => { errors.push(json!({"id": l.id, "name": l.name, "error": "the file now holds another library"})); continue; } Err(e) => { errors.push(json!({"id": l.id, "name": l.name, "error": e.to_string()})); continue; } }; let p = pending(s.doc()?, &lib); if p.is_empty() { continue; } count += p.count(); let m = &lib.manifest; let name = |r: &ImportedComponent| m.component(r.key).map(|a| a.name.clone()).unwrap_or_default(); let rows = |list: &[ImportedRecord], name: &dyn Fn(&str) -> Option| { // A variable new to an extended collection has no copy yet: no `id`. list.iter().map(|r| json!({"id": (!r.local.is_empty()).then_some(&r.local), "key": r.key, "name": name(&r.key)})).collect::>() }; out.push(json!({ "id": l.id, "name": m.name, "version": m.version, "components": p.components.iter().map(|r| json!({"id": r.root, "key": r.key, "name": name(r)})).collect::>(), "styles": rows(&p.styles, &|k| m.style(k).map(|x| x.name.clone())), "collections": rows(&p.collections, &|k| m.collection(k).map(|x| x.name.clone())), "variables": rows(&p.variables, &|k| m.variable(k).map(|x| x.name.clone())), })); } Ok(json!({"count": count, "libraries": out, "errors": errors})) } fn accept(s: &mut Session, p: &Value) -> Result { let cmd = "library.acceptUpdates"; let named: Option> = match get(p, "ids") { Some(Value::Array(a)) => Some(a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()), Some(Value::String(one)) => Some(vec![one.clone()]), Some(_) => return Err(bad(cmd, "`ids` must be an array of library ids")), None => None, }; let links: Vec = match &named { Some(keys) => keys .iter() .map(|k| find_link(s.doc()?, k).cloned().ok_or_else(|| bad(cmd, format!("no enabled library `{k}`")))) .collect::>()?, None => s.doc()?.libraries.enabled.clone(), }; let (mut done, mut skipped) = (Pending::default(), Vec::new()); for l in links { let lib = match library(s, cmd, &json!({"library": l.id})) { Ok((_, lib)) => lib, Err(e) if named.is_none() => { skipped.push(json!({"id": l.id, "name": l.name, "error": e.to_string()})); continue; } Err(e) => return Err(e), }; let p = accept_updates(s.doc_mut()?, &lib)?; done.components.extend(p.components); done.styles.extend(p.styles); done.collections.extend(p.collections); done.variables.extend(p.variables); } Ok(json!({ "components": done.components.len(), "styles": done.styles.len(), "collections": done.collections.len(), "variables": done.variables.len(), "skipped": skipped, })) } fn place(s: &mut Session, p: &Value) -> Result { let cmd = "library.place"; let (_, lib, key) = component_arg(s, cmd, p)?; let (cx, cy) = s.ds().map(|d| (d.view.cx, d.view.cy))?; let (x, y) = (num(p, "x").unwrap_or(cx), num(p, "y").unwrap_or(cy)); let map = import(s.doc_mut()?, &lib, &[key], false)?; let local = map.get(&key).copied().ok_or_else(|| EngineError::Other("the component didn't import".into()))?; let mut out = super::component::place(s, &json!({"component": local, "x": x, "y": y}))?; out["component"] = json!(local); Ok(out) } fn apply_style(s: &mut Session, p: &Value) -> Result { let cmd = "library.applyStyle"; let (link, lib) = library(s, cmd, p)?; let key = req_string(cmd, p, "style")?; let m = &lib.manifest; let style = m .styles .iter() .filter(|x| x.published) .find(|x| x.id == key) .or_else(|| m.styles.iter().filter(|x| x.published).find(|x| x.name == key)) .map(|x| x.id.clone()) .ok_or_else(|| bad(cmd, format!("\"{}\" publishes no style `{key}`", link.name)))?; let local = import_styles(s.doc_mut()?, &lib, &[style].into_iter().collect(), false) .into_values() .next() .ok_or_else(|| EngineError::Other("the style didn't import".into()))?; super::styles::apply(s, &with(p, &["ids", "id", "target"], json!({"style": local})))?; Ok(json!({"style": local})) }