Skip to main content

otter/
shapelib.rs

1// Copyright 2020-2021 Ian Jackson and contributors to Otter
2// SPDX-License-Identifier: AGPL-3.0-or-later
3// There is NO WARRANTY.
4
5use crate::prelude::*;
6use crate::*; // to get ambassador_impls, macro resolution trouble
7pub use crate::shapelib_toml::*;
8
9pub use crate::prelude::GoodItemName; // not sure why this is needed
10
11use parking_lot::{const_rwlock, RwLock};
12use parking_lot::RwLockReadGuard;
13
14use ShapelibConfig1 as Config1;
15use ShapelibExplicit1 as Explicit1;
16
17//==================== structs and definitions ====================
18
19// Naming convention:
20//  *Data, *List   from toml etc. (processed if need be)
21//  *Defn          raw read from library toml file (where different from Info)
22//  *Details       some shared structure
23//  Item           } once loaded and part of a game,
24//  Outline        }  no Arc's as we serialise/deserialize during save/load
25
26static GLOBAL_SHAPELIBS: RwLock<Option<Registry>> = const_rwlock(None);
27
28#[derive(Default)]
29pub struct Registry {
30  libs: HashMap<String, Vec<shapelib::Catalogue>>,
31}
32
33#[derive(Debug)]
34pub struct GroupData {
35  groupname: String,
36  d: GroupDetails,
37  #[allow(dead_code)] /*TODO*/ mformat: materials_format::Version,
38}
39
40#[derive(Debug,Clone,Copy)]
41pub struct ShapeCalculable { }
42
43#[derive(Debug)]
44pub struct Catalogue {
45  libname: String,
46  dirname: String,
47  bundle: Option<bundles::Id>,
48  items: HashMap<SvgBaseName<GoodItemName>, CatalogueEntry>,
49}
50
51#[derive(Debug,Clone)]
52#[derive(Serialize,Deserialize)]
53struct ItemDetails {
54  desc: Html,
55}
56
57#[derive(Debug,Clone)]
58enum CatalogueEntry {
59  Item(ItemData),
60  Magic { group: Arc<GroupData>, spec: Arc<dyn PieceSpec> },
61}
62use CatalogueEntry as CatEnt;
63
64#[derive(Debug,Clone)]
65struct ItemData {
66  d: Arc<ItemDetails>,
67  sort: Option<String>,
68  group: Arc<GroupData>,
69  occ: OccData,
70  shape_calculable: ShapeCalculable,
71}
72
73#[derive(Debug,Clone)]
74enum OccData {
75  None,
76  Internal(Arc<OccData_Internal>),
77  Back(OccultIlkName),
78}
79
80#[allow(non_camel_case_types)]
81#[derive(Debug)]
82struct OccData_Internal {
83  item_name: SvgBaseName<GoodItemName>,
84  desc: Html,
85  loaded: lazy_init::Lazy<Result<ImageLoaded,SpecError>>,
86}
87
88#[allow(non_camel_case_types)]
89#[derive(Debug,Clone)]
90struct ImageLoaded {
91  svgd: Html,
92  xform: FaceTransform,
93  outline: Outline,
94}
95
96#[derive(Error,Debug)]
97pub enum LibraryLoadError {
98  #[error(transparent)]
99  TomlParseError(#[from] toml::de::Error),
100  #[error("error reading/opening library file: {0}: {1}")]
101                                                  FileError(String, io::Error),
102  #[error("OS error globbing for files: {0}")]
103                                      GlobFileError(#[from] glob::GlobError),
104  #[error("internal error: {0}")]     InternalError(#[from] InternalError),
105  #[error("bad glob pattern: {pat:?} (near char {pos}): {msg}")]
106                 BadGlobPattern { pat: String, msg: &'static str, pos: usize },
107  #[error("glob pattern {pat:?} matched non-utf-8 filename {actual:?}")]
108                                  GlobNonUTF8 { pat: String, actual: PathBuf },
109  #[error("glob pattern {pat:?} matched filename with no extension {path:?}")]
110                                 GlobNoExtension { pat: String, path: String },
111  #[error("occultation colour missing: {0:?}")]
112                                            OccultationColourMissing(String),
113  #[error("back missing for occultation")]  BackMissingForOccultation,
114  #[error("expected TOML table: {0:?}")]    ExpectedTable(String),
115  #[error("expected TOML string: {0:?}")]   ExpectedString(String),
116  #[error("wrong number of size dimensions {got}, expected {expected:?}")]
117              WrongNumberOfSizeDimensions { got: usize, expected: [usize;2] },
118  #[error("group {0:?} inherits from nonexistent parent {1:?}")]
119                                         InheritMissingParent(String, String),
120  #[error("inheritance depth limit exceeded: {0:?}")]
121                                            InheritDepthLimitExceeded(String),
122  #[error("duplicate item {item:?} in groups {group1:?} and {group2:?}")]
123               DuplicateItem { item: String, group1: String, group2: String },
124  #[error("files list line {0} missing whitespace")]
125                                        FilesListLineMissingWhitespace(usize),
126  #[error("files list line {0}, field must be at start")]
127                                        FilesListFieldsMustBeAtStart(usize),
128  #[error("piece defines multiple faces in multiple ways")]
129                                        MultipleMultipleFaceDefinitions,
130  #[error("outline specified both size and numeric scale")]
131                                        OutlineContradictoryScale,
132  #[error("{0}")] CoordinateOverflow(#[from] CoordinateOverflow),
133
134  #[error("{0}")]
135  MaterialsFormatIncompat(#[from] materials_format::Incompat<
136    LibraryLoadMFIncompat
137  >),
138  #[error("{0}")]                       BadSubstitution(#[from] SubstError),
139  #[error("{0}")] UnsupportedColourSpec(#[from] UnsupportedColourSpec),
140  #[error("bad item name (invalid characters) in {0:?}")] BadItemName(String),
141  #[error("{0}")] MaterialsFormatVersionError(#[from] MFVE),
142
143  #[error("could not parse template-expaneded TOML: {error} (in {toml:?}")]
144  TemplatedTomlError { toml: String, error: toml_de::Error },
145
146  #[error("group {group}: {error}")]
147  InGroup { group: String, error: Box<LLE> },
148
149  #[error("library {lib}: {error}")]
150  InLibrary { lib: String, error: Box<LLE> },
151}
152
153#[derive(Error,Debug,Clone,Copy,Serialize,Deserialize)]
154pub enum LibraryLoadMFIncompat {
155  #[error("bad scale definition")] Scale,
156  #[error("size not specified")] SizeRequired,
157  #[error("orig_size no longer supported")] OrigSizeForbidden,
158  #[error("specified both size and numeric scale")] ContradictoryScale,
159}
160#[derive(Error,Clone,Debug)]
161pub enum SubstErrorKind {
162  #[error("missing or unrecognised token {0}")] MissingToken(Cow<'static,str>),
163  #[error("repeated token {0}")]               RepeatedToken(Cow<'static,str>),
164  #[error("internal logic error {0}")] Internal(#[from] InternalLogicError),
165}
166
167#[derive(Error,Clone,Debug)]
168#[error("bad substitution: {input:?} {kind}")]
169pub struct SubstError {
170  pub kind: SubstErrorKind,
171  pub input: String,
172}
173
174
175const INHERIT_DEPTH_LIMIT: u8 = 20;
176
177type TV = toml::Value;
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct MultiSpec {
181  pub lib: String,
182  #[serde(default)]
183  pub prefix: String,
184  #[serde(default)]
185  pub suffix: String,
186  pub items: Vec<String>,
187}
188
189define_index_type! { pub struct DescId = u8; }
190define_index_type! { pub struct SvgId = u8; }
191
192#[derive(Copy,Clone,Debug,Serialize,Deserialize)]
193struct ItemFace {
194  svg: SvgId,
195  desc: DescId,
196  #[serde(flatten)]
197  xform: FaceTransform,
198}
199
200#[derive(Copy,Clone,Debug,Serialize,Deserialize)]
201struct FaceTransform {
202  centre: [f64; 2],
203  scale: [f64; 2],
204}
205
206#[derive(Debug,Serialize,Deserialize)]
207pub struct Item {
208  itemname: String,
209  sort: Option<String>,
210  faces: IndexVec<FaceId, ItemFace>,
211  svgs: IndexVec<SvgId, Html>,
212  descs: IndexVec<DescId, Html>,
213  outline: Outline,
214  #[serde(default)]
215  back: Option<Arc<dyn InertPieceTrait>>,
216}
217
218#[derive(Debug,Serialize,Deserialize)]
219struct ItemInertForOcculted {
220  #[serde(default="dummy_item_name")] itemname: GoodItemName,
221  desc: Html,
222  svgd: Html,
223  xform: FaceTransform,
224  outline: Outline,
225}
226
227//==================== SvgBsseName ====================
228
229/// Represents a `T` which is an SVG basename which has been noted
230/// for processing during bundle load.
231#[derive(Debug,Copy,Clone,Hash,Eq,PartialEq,Ord,PartialOrd,Deref)]
232#[repr(transparent)]
233struct SvgBaseName<T:?Sized>(T);
234impl<T> Display for SvgBaseName<T> where T: Display + ?Sized {
235  #[throws(fmt::Error)]
236  fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "{}", &self.0)? }
237}
238impl<T> Borrow<str> for SvgBaseName<T> where T: Borrow<str> {
239  fn borrow(&self) -> &str { self.0.borrow() }
240}
241impl<T> SvgBaseName<T> {
242  fn into_inner(self) -> T { self.0 }
243}
244impl<T> SvgBaseName<T> where T: ?Sized {
245  fn as_str(&self) -> &str where T: Borrow<str> { self.0.borrow() }
246  fn unnest<'l,U>(&'l self) -> &SvgBaseName<U> where U: ?Sized, T: Borrow<U> {
247    let s: &'l U = self.0.borrow();
248    let u: &'l SvgBaseName<U> = unsafe { mem::transmute(s) };
249    u
250  }
251}
252impl<T> SvgBaseName<T> where T: Borrow<GoodItemName> {
253  #[throws(SubstError)]
254  fn note(src: &mut dyn LibrarySvgNoter, i: T,
255          src_name: Result<&str, &SubstError>) -> Self {
256    src.note_svg(i.borrow(), src_name)?;
257    SvgBaseName(i)
258  }
259}
260
261//==================== impls for ItemInertForOcculted ====================
262
263impl_via_ambassador!{
264  #[dyn_upcast]
265  impl OutlineTrait for ItemInertForOcculted { outline }
266}
267#[dyn_upcast]
268impl PieceBaseTrait for ItemInertForOcculted {
269  fn nfaces(&self) -> RawFaceId { 1 }
270  fn itemname(&self) -> &str { self.itemname.as_str() }
271}
272#[typetag::serde(name="Lib")]
273impl InertPieceTrait for ItemInertForOcculted {
274  #[throws(IE)]
275  fn svg(&self, f: &mut Html, _: VisiblePieceId, face: FaceId,
276         _: &PieceXDataState) {
277    if face != FaceId::default() {
278      throw!(internal_logic_error("ItemInertForOcculted non-default face"))
279    }
280    self.xform.write_svgd(f, &self.svgd)?;
281  }
282  #[throws(IE)]
283  fn describe_html(&self, _: FaceId) -> Html { self.desc.clone() }
284}
285
286fn dummy_item_name() -> GoodItemName {
287  "".to_string().try_into().unwrap()
288}
289
290#[test]
291fn test_dummy_item_name() {
292  let _ = dummy_item_name();
293}
294
295//---------- ItemEnquiryData, LibraryEnquiryData ----------
296
297#[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
298pub struct ItemEnquiryData {
299  pub lib: LibraryEnquiryData,
300  pub itemname: GoodItemName,
301  pub sortkey: Option<String>,
302  pub f0desc: Html,
303  pub f0bbox: Rect,
304}
305
306impl From<&ItemEnquiryData> for ItemSpec {
307  fn from(it: &ItemEnquiryData) -> ItemSpec {
308    ItemSpec {
309      lib: it.lib.libname.clone(),
310      item: it.itemname.as_str().to_owned(),
311    }
312  }
313}
314
315impl Display for ItemEnquiryData {
316  #[throws(fmt::Error)]
317  fn fmt(&self, f: &mut Formatter) {
318    write!(f, "{:<10} {:20}  {}", &self.lib, &self.itemname,
319           self.f0desc.as_html_str())?;
320  }
321}
322
323#[derive(Debug,Clone,Serialize,Deserialize,Eq,PartialEq,Ord,PartialOrd)]
324pub struct LibraryEnquiryData {
325  pub bundle: Option<bundles::Id>,
326  pub libname: String,
327}
328impl Display for LibraryEnquiryData {
329  #[throws(fmt::Error)]
330  fn fmt(&self, f: &mut Formatter) {
331    if let Some(id) = self.bundle.as_ref() {
332      write!(f, "[{}] ", id)?;
333    }
334    if self.libname.chars().all(|c| {
335      c.is_alphanumeric() || c=='-' || c=='_' || c=='.'
336    }) {
337      Display::fmt(&self.libname, f)?;
338    } else {
339      Debug::fmt(&self.libname, f)?;
340    }
341  }
342}
343
344//==================== Item ====================
345
346impl_via_ambassador!{
347  #[dyn_upcast]
348  impl OutlineTrait for Item { outline }
349}
350
351impl Item {
352  #[throws(IE)]
353  fn svg_face(&self, f: &mut Html, face: FaceId, vpid: VisiblePieceId,
354              xdata: &PieceXDataState) {
355    if let Some(face) = self.faces.get(face) {
356      let svgd = &self.svgs[face.svg];
357      face.xform.write_svgd(f, svgd)?;
358    } else if let Some(back) = &self.back {
359      back.svg(f, vpid, default(), xdata)?;
360    } else {
361      throw!(internal_error_bydebug(&(self, face)))
362    }
363  }
364
365  #[throws(IE)]
366  fn describe_face(&self, face: FaceId) -> Html {
367    self.descs[
368      if let Some(face) = self.faces.get(face) {
369        face.desc
370      } else if let Some(back) = &self.back {
371        return back.describe_html(default())?;
372      } else {
373        self.faces[0].desc
374      }
375    ].clone()
376  }
377}
378
379#[dyn_upcast]
380impl PieceBaseTrait for Item {
381  fn nfaces(&self) -> RawFaceId {
382    (self.faces.len()
383     + self.back.iter().count())
384      .try_into().unwrap()
385  }
386
387  fn itemname(&self) -> &str { &self.itemname }
388}
389
390#[typetag::serde(name="Lib")]
391impl PieceTrait for Item {
392  #[throws(IE)]
393  fn svg_piece(&self, f: &mut Html, gpc: &GPiece,
394               _gs: &GameState, vpid: VisiblePieceId) {
395    self.svg_face(f, gpc.face, vpid, &gpc.xdata)?;
396  }
397  #[throws(IE)]
398  fn describe_html(&self, gpc: &GPiece, _goccults: &GOccults) -> Html {
399    self.describe_face(gpc.face)?
400  }
401
402  fn sortkey(&self) -> Option<&str> { self.sort.as_ref().map(AsRef::as_ref) }
403}
404
405#[typetag::serde(name="LibItem")]
406impl InertPieceTrait for Item {
407  #[throws(IE)]
408  fn svg(&self, f: &mut Html, id: VisiblePieceId, face: FaceId,
409         xdata: &PieceXDataState) {
410    self.svg_face(f, face, id, xdata)?;
411  }
412  #[throws(IE)]
413  fn describe_html(&self, _: FaceId) -> Html {
414    self.describe_face(default())?
415  }
416}
417
418//==================== ItemSpec and item loading ====================
419
420//---------- ItemSpec, MultiSpec ----------
421
422type ItemSpecLoaded = (Box<Item>, PieceSpecLoadedOccultable);
423
424impl From<ItemSpecLoaded> for SpecLoaded {
425  fn from((p, occultable):  ItemSpecLoaded) -> SpecLoaded {
426    SpecLoaded {
427      p,
428      occultable,
429      special: default(),
430    }
431  }
432}
433
434impl ItemSpec {
435  #[throws(SpecError)]
436  fn find_then<F,T>(&self, ig: &Instance, then: F) -> T
437  where F: FnOnce(&Catalogue, &SvgBaseName<GoodItemName>, &CatalogueEntry)
438                  -> Result<T, SpecError>
439  {
440    let regs = ig.all_shapelibs();
441    let libs = regs.lib_name_lookup(&self.lib)?;
442    let (lib, (item, idata)) = libs.iter().rev().find_map(
443      |lib| Some((lib, lib.items.get_key_value(self.item.as_str())?))
444    )
445      .ok_or_else(|| SpE::LibraryItemNotFound(self.clone()))?;
446    then(lib, item, idata)?
447  }
448
449  #[throws(SpecError)]
450  fn find_load_general<MAG,MUN,T>(&self, ig: &Instance, depth: SpecDepth,
451                                      mundanef: MUN, magicf: MAG) -> T
452  where MUN: FnOnce(ItemSpecLoaded) -> Result<T, SpE>,
453        MAG: FnOnce(&Arc<dyn PieceSpec>) -> Result<T, SpE>,
454  {
455    self.find_then(ig, |lib, item, catent| Ok(match catent {
456      CatEnt::Item(idata) => {
457        let loaded = lib.load1(idata, &self.lib, item.unnest::<str>(),
458                               ig, depth)?;
459        mundanef(loaded)?
460      },
461      CatEnt::Magic { spec,.. } => {
462        magicf(spec)?
463      },
464    }))?
465  }
466
467  #[throws(SpecError)]
468  pub fn find_load_mundane(&self, ig: &Instance,
469                           depth: SpecDepth) -> ItemSpecLoaded {
470    self.find_load_general(
471      ig, depth, |loaded| Ok(loaded),
472      |_| Err(SpE::ComplexPieceWhereInertRequired)
473    )?
474  }
475
476  fn from_strs<L,I>(lib: &L, item: &I) -> Self
477    where L: ToOwned<Owned=String> + ?Sized,
478          I: ToOwned<Owned=String> + ?Sized,
479  {
480    let lib  = lib .to_owned();
481    let item = item.to_owned();
482    ItemSpec{ lib, item }
483  }
484}
485
486#[typetag::serde(name="Lib")]
487impl PieceSpec for ItemSpec {
488  #[throws(SpecError)]
489  fn load(&self, pla: PLA) -> SpecLoaded {
490    self.find_load_general(
491      pla.ig, pla.depth,
492      |loaded| Ok(loaded.into()),
493      |magic| magic.load(pla.recursing()?)
494    )?
495  }
496  #[throws(SpecError)]
497  fn load_inert(&self, ig: &Instance, depth: SpecDepth) -> SpecLoadedInert {
498    let (p, occultable) = self.find_load_mundane(ig,depth)?;
499    SpecLoadedInert { p: p as _, occultable }
500  }
501}
502
503#[typetag::serde(name="LibList")]
504impl PieceSpec for MultiSpec {
505  #[throws(SpecError)]
506  fn count(&self, _pcaliases: &PieceAliases) -> usize { self.items.len() }
507
508  #[throws(SpecError)]
509  fn load(&self, pla: PLA) -> SpecLoaded
510  {
511    let PLA { i,.. } = pla;
512    let item = self.items.get(i).ok_or_else(
513      || SpE::InternalError(format!("item {:?} from {:?}", i, &self))
514    )?;
515    let item = format!("{}{}{}", &self.prefix, item, &self.suffix);
516    let lib = self.lib.clone();
517    ItemSpec { lib, item }.load(pla)?
518  }
519}
520
521//---------- Loading ----------
522
523impl Catalogue {
524  #[throws(SpecError)]
525  fn load_image(&self, item_name: &SvgBaseName<str>,
526                lib_name_for: &str, item_for: &str,
527                group: &GroupData, shape_calculable: ShapeCalculable)
528              -> ImageLoaded {
529    let svg_path = format!("{}/{}.usvg", self.dirname, item_name);
530    let svg_data = fs::read_to_string(&svg_path)
531      .map_err(|e| if e.kind() == ErrorKind::NotFound {
532        warn!("library item lib={} itme={} for={:?} data file {:?} not found",
533              &self.libname, item_name, item_for, &svg_path);
534        let spec_for = ItemSpec::from_strs(lib_name_for, item_for);
535        SpE::LibraryItemNotFound(spec_for)
536      } else {
537        let m = "error accessing/reading library item data file";
538        error!("{}: {} {:?}: {}", &m, &svg_path, item_for, &e);
539        SpE::InternalError(m.to_string())
540      })?;
541
542    let svg_data = Html::from_html_string(svg_data);
543
544    let sz = svg_parse_size(&svg_data).map_err(|error| SpE::SVGError {
545      error,
546      item_name: item_name.as_str().into(),
547      item_for_lib: lib_name_for.into(),
548      item_for_item: item_for.into(),
549    })?;
550
551    let (xform, outline) = group.load_shape(sz)
552      .map_err(shape_calculable.err_mapper())?;
553
554    ImageLoaded {
555      svgd: svg_data,
556      outline,
557      xform,
558    }
559  }
560
561  #[throws(SpecError)]
562  fn load1(&self, idata: &ItemData, lib_name: &str,
563           name: &SvgBaseName<str>,
564           ig: &Instance, depth:SpecDepth)
565           -> ItemSpecLoaded {
566    let ImageLoaded { svgd: svg_data, outline, xform } =
567      self.load_image(name, lib_name, &**name,
568                       &idata.group, idata.shape_calculable)?;
569
570    let mut svgs = IndexVec::with_capacity(1);
571    let svg = svgs.push(svg_data);
572
573    let mut descs = index_vec![ ];
574    let desc = descs.push(idata.d.desc.clone());
575    descs.shrink_to_fit();
576
577    let mut face = ItemFace { svg, desc, xform };
578    let mut faces = index_vec![ face ];
579    let mut back = None::<Arc<dyn InertPieceTrait>>;
580    if idata.group.d.flip {
581      face.xform.scale[0] *= -1.;
582      faces.push(face);
583    } else if let Some(back_spec) = &idata.group.d.back {
584      match back_spec.load_inert(ig, depth) {
585        Err(SpecError::AliasNotFound) => { },
586        Err(e) => throw!(e),
587        Ok(p) => {
588          let p = p.p.into();
589          back = Some(p);
590        }
591      }
592    }
593    faces.shrink_to_fit();
594
595    let occultable = match &idata.occ {
596      OccData::None => None,
597      OccData::Back(ilk) => {
598        if let Some(back) = &back {
599          let back = back.clone();
600          Some((LOI::Mix(ilk.clone()), back))
601        } else {
602          None // We got AliasNotFound, ah well
603        }
604      },
605      OccData::Internal(occ) => {
606        let occ_name = occ.item_name.clone();
607        let ImageLoaded {
608          svgd, outline, xform
609        } = occ.loaded.get_or_create(
610          || self.load_image(
611            occ.item_name.unnest::<GoodItemName>().unnest(),
612            /* original: */ lib_name, name.as_str(),
613            &idata.group, idata.shape_calculable,
614          )
615        ).clone()?;
616
617        let it = Arc::new(ItemInertForOcculted {
618          svgd, outline, xform,
619          itemname: occ_name.clone().into_inner(),
620          desc: occ.desc.clone(),
621        }) as Arc<dyn InertPieceTrait>;
622        Some((LOI::Mix(occ_name.into_inner()), it))
623      },
624    };
625
626    let sort = idata.sort.clone();
627    let it = Item { faces, sort, descs, svgs, outline, back,
628                    itemname: name.to_string() };
629    (Box::new(it), occultable)
630  }
631}
632
633//==================== size handling, and outlines ====================
634
635impl FaceTransform {
636  #[throws(LLE)]
637  fn from_group_mf1(group: &GroupData) -> Self {
638    let d = &group.d;
639    // by this point d.size has already been scaled by scale
640    let scale = if ! d.orig_size.is_empty() && ! d.size.is_empty() {
641      izip!(&d.orig_size, &d.size)
642        .map(|(&orig_size, &target_size)| {
643          target_size / orig_size
644        })
645        .cycle()
646        .take(2)
647        .collect::<ArrayVec<_,2>>()
648        .into_inner()
649        .unwrap()
650    } else {
651      let s = group.d.scale_mf1(group.mformat)?;
652      [s,s]
653    };
654    let centre = d.centre.map(Ok).unwrap_or_else(|| Ok::<_,LLE>({
655      resolve_square_size(&d.size)?
656        .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?
657        .coords.iter().cloned().zip(&scale).map(|(size,scale)| {
658          size * 0.5 / scale
659        })
660        .collect::<ArrayVec<_,2>>()
661        .into_inner()
662        .unwrap()
663    }))?;
664    FaceTransform { centre, scale }
665  }
666
667  #[throws(IE)]
668  fn write_svgd(&self, f: &mut Html, svgd: &Html) {
669    hwrite!(f,
670           r##"<g transform="scale({} {}) translate({} {})">{}</g>"##,
671           self.scale[0], self.scale[1], -self.centre[0], -self.centre[1],
672           svgd)?;
673  }
674}
675
676#[throws(LLE)]
677fn resolve_square_size<T:Copy>(size: &[T]) -> Option<PosC<T>> {
678  Some(PosC{ coords: match size {
679    [] => return None,
680    &[s] => [s,s],
681    &[w,h] => [w,h],
682    _ => throw!(LLE::WrongNumberOfSizeDimensions
683                { got: size.len(), expected: [1,2]}),
684  } })
685}
686
687impl CatalogueEntry {
688  fn group(&self) -> &Arc<GroupData> { match self {
689    CatEnt::Item(item) => &item.group,
690    CatEnt::Magic{group,..} => group,
691  } }
692}
693
694//---------- Outlines ----------
695
696impl ShapeCalculable {
697  pub fn err_mapper(&self) -> impl Fn(LLE) -> IE + Copy {
698    |e| internal_logic_error(format!(
699      "outline calculable but failed {} {:?}",&e,&e
700    ))
701  }
702}
703
704impl GroupData {
705  #[throws(LibraryLoadError)]
706  fn check_shape(&self) -> ShapeCalculable {
707    let _ = self.load_shape(PosC::new(
708      1.,1. /* dummy value, suffices for error check */
709    ))?;
710    ShapeCalculable{}
711  }
712
713  #[throws(LibraryLoadError)]
714  /// As with OutlineDefn::load, success must not depend on svg_sz value
715  fn load_shape(&self, svg_sz: PosC<f64>) -> (FaceTransform, Outline) {
716    if self.mformat >= 2 {
717
718      if self.d.orig_size.len() > 0 {
719        throw!(self.mformat.incompat(LLMI::OrigSizeForbidden))
720      }
721
722      let centre: PosC<f64> = self.d.centre
723        .map(|coords| PosC { coords })
724        .unwrap_or_else(|| geometry::Mean::mean(&svg_sz, &PosC::zero()));
725
726      let size = resolve_square_size(&self.d.size)?;
727
728      use ScaleDetails as SD;
729      let scale = self.d.scale.unwrap_or(SD::Fit(ScaleFitDetails::Fit));
730
731      let of_stretch = |scale| {
732        let scale = PosC { coords: scale };
733        let size = pos_zip_map!( svg_sz, scale => |(sz,sc)| sz * sc );
734        (size, scale)
735      };
736
737      let (size, scale) = match (size, scale) {
738        (Some(size), SD::Fit(fit)) => {
739          let scale = pos_zip_map!( size, svg_sz => |(a,b)| a/b );
740          type Of = OrderedFloat<f64>;
741          let of = |minmax: fn(Of,Of) -> Of| {
742            let v = minmax(scale.coords[0].into(),
743                           scale.coords[1].into()).into_inner();
744            PosC::new(v,v)
745          };
746          let scale = match fit {
747            ScaleFitDetails::Fit => of(min),
748            ScaleFitDetails::Cover => of(max),
749            ScaleFitDetails::Stretch => scale,
750          };
751          (size, scale)
752        },
753        (Some(_), SD::Scale(_)) |
754        (Some(_), SD::Stretch(_))
755          => throw!(self.mformat.incompat(LLMI::ContradictoryScale)),
756        (None, SD::Fit(_)) => (svg_sz, PosC::new(1.,1.)),
757        (None, SD::Scale(s)) => of_stretch([s,s]),
758        (None, SD::Stretch(s)) => of_stretch(s),
759      };
760
761      let osize = {
762        let (osize, oscale) = self.d.outline.size_scale();
763        let osize = resolve_square_size(osize)?;
764        match (osize, oscale) {
765          (Some(osize), None         ) => osize,
766          (None,        Some(&oscale)) => (size * oscale)?,
767          (None,        None         ) => size,
768          (Some(_),     Some(_)      ) =>
769            throw!(LLE::OutlineContradictoryScale)
770        }
771      };
772
773      let outline = self.d.outline.shape().load(osize);
774      (FaceTransform { scale: scale.coords, centre: centre.coords }, outline)
775
776    } else {
777      let xform = FaceTransform::from_group_mf1(self)?;
778      let outline = self.d.outline.shape().load_mf1(self)?;
779      (xform, outline)
780    }
781  }
782}
783
784impl GroupDetails {
785  #[throws(materials_format::Incompat<LLMI>)]
786  fn scale_mf1(&self, mformat: materials_format::Version) -> f64 {
787    match self.scale {
788      None => 1.,
789      Some(ScaleDetails::Scale(s)) => s,
790      _ => throw!(mformat.incompat(LLMI::Scale)),
791    }
792  }
793}
794
795//---------- OutlineDefn etc. ----------
796
797#[ambassador::delegatable_trait]
798pub trait ShapeLoadableTrait: Debug + Sync + Send + 'static {
799  /// Success or failure must not depend on `svg_sz`
800  ///
801  /// Called to *check* the group configuration before load, but
802  /// with a dummy svg_gz of `[1,1]`.  That must correctly predict
803  /// success with other sizes.
804  fn load(&self, size: PosC<f64>) -> Outline {
805    RectOutline { xy: size }.into()
806  }
807
808  fn load_mf1(&self, group: &GroupData) -> Result<Outline,LLE>;
809}
810
811// We used to do shape deser via typetag and Box<dyn OutlineDefn>
812//
813// But I didnt manage to get typetag to deserialise the way I wanted.
814// Instead, we have the Shape enum and a cheesy macro to impl OutlineDefn
815// by delegating to a freshly made (static) unit struct value,
816// - see outline_defn in mod outline in spec.rs.
817impl_via_ambassador!{
818  impl ShapeLoadableTrait for Shape { shapelib_loadable() }
819}
820
821//---------- RectOutline ----------
822
823impl ShapeLoadableTrait for RectShapeIndicator {
824  fn load(&self, size: PosC<f64>) -> Outline {
825    RectOutline { xy: size }.into()
826  }
827
828  #[throws(LibraryLoadError)]
829  fn load_mf1(&self, group: &GroupData) -> Outline {
830    let size = resolve_square_size(&group.d.size)?
831        .ok_or_else(|| group.mformat.incompat(LLMI::SizeRequired))?;
832    self.load(size)
833  }
834}
835
836//---------- CircleOutline ----------
837
838impl ShapeLoadableTrait for CircleShapeIndicator {
839  fn load(&self, size: PosC<f64>) -> Outline {
840    let diam = size
841      .coords.into_iter()
842      .map(OrderedFloat)
843      .max().unwrap().
844      into_inner();
845    CircleOutline {
846      diam,
847    }.into()
848  }
849
850  #[throws(LibraryLoadError)]
851  fn load_mf1(&self, group: &GroupData) -> Outline {
852    let diam = match group.d.size.as_slice() {
853      &[c] => c,
854      size => throw!(LLE::WrongNumberOfSizeDimensions
855                     { got: size.len(), expected: [1,1] }),
856    };
857    CircleOutline {
858      diam,
859    }.into()
860  }
861}
862
863//==================== Catalogues ====================
864
865//---------- enquiries etc. ----------
866
867impl Catalogue {
868  pub fn enquiry(&self) -> LibraryEnquiryData {
869    LibraryEnquiryData {
870      libname: self.libname.clone(),
871      bundle: self.bundle,
872    }
873  }
874
875  #[throws(MgmtError)]
876  pub fn list_glob(&self, pat: &str) -> Vec<ItemEnquiryData> {
877    let pat = glob::Pattern::new(pat).map_err(|pe| ME::BadGlob {
878      pat: pat.to_string(), msg: pe.msg.to_string() })?;
879    let mut out = vec![];
880    let ig_dummy = Instance::dummy();
881    for (k,v) in &self.items {
882      if !pat.matches(k.as_str()) { continue }
883      let mut gpc = GPiece::dummy();
884      let loaded = match (|| Ok(match v {
885        CatEnt::Item(item) => {
886          let (loaded, _) =
887            self.load1(item, &self.libname, k.unnest(),
888                       &Instance::dummy(), SpecDepth::zero())?;
889          loaded as Box<dyn PieceTrait>
890        },
891        CatEnt::Magic{spec,..} => {
892          spec.load(PieceLoadArgs {
893            i: 0,
894            gpc: &mut gpc,
895            ig: &ig_dummy,
896            depth: SpecDepth::zero(),
897          })?.p
898        }
899      }))() {
900        Err(SpecError::LibraryItemNotFound(_)) => continue,
901        e@ Err(_) => e?,
902        Ok(r) => r,
903      };
904      let f0bbox = loaded.bbox_approx()?;
905      let ier = ItemEnquiryData {
906        lib: self.enquiry(),
907        itemname: (**k).to_owned(),
908        sortkey: loaded.sortkey().map(|s| s.to_owned()),
909        f0bbox,
910        f0desc: loaded.describe_html(&gpc, &default())?,
911      };
912      out.push(ier);
913    }
914    out
915  }
916}
917
918pub trait LibrarySvgNoter {
919  #[throws(SubstError)]
920  fn note_svg(&mut self, _basename: &GoodItemName,
921              _src_name: Result<&str, &SubstError>) { }
922}
923pub trait LibrarySource: LibrarySvgNoter {
924  fn catalogue_data(&self) -> &str;
925  fn svg_dir(&self) -> String;
926  fn bundle(&self) -> Option<bundles::Id>;
927
928  fn default_materials_format(&self)
929                              -> Result<materials_format::Version, MFVE>;
930
931  // Sadly dyn_upcast doesn't work because it doesn't support the
932  // non-'static lifetime on BuiltinLibrary
933  fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter;
934}
935
936pub struct NullLibrarySvgNoter;
937impl LibrarySvgNoter for NullLibrarySvgNoter { }
938
939struct BuiltinLibrary<'l> {
940  catalogue_data: &'l str,
941  dirname: &'l str,
942}
943
944impl LibrarySvgNoter for BuiltinLibrary<'_> {
945}
946impl<'l> LibrarySource for BuiltinLibrary<'l> {
947  fn catalogue_data(&self) -> &str { self.catalogue_data }
948  fn svg_dir(&self) -> String { self.dirname.to_string() }
949  fn bundle(&self) -> Option<bundles::Id> { None }
950
951  #[throws(materials_format::VersionError)]
952  fn default_materials_format(&self) -> materials_format::Version {
953    throw!(MFVE::Other("builtin libraries must have explicit version now!"));
954  }
955
956  fn svg_noter(&mut self) -> &mut dyn LibrarySvgNoter { self }
957}
958
959//---------- reading ----------
960
961#[throws(LibraryLoadError)]
962pub fn load_catalogue(libname: &str, src: &mut dyn LibrarySource)
963                      -> Catalogue {
964  (||{
965
966  let toplevel: toml::Value = src.catalogue_data().parse()?;
967  let toplevel = toplevel
968    .as_table().ok_or_else(|| LLE::ExpectedTable(format!("toplevel")))?;
969  let mformat = match toplevel.get("format") {
970    None => src.default_materials_format()?,
971    Some(v) => {
972      let v = v.as_integer().ok_or_else(|| MFVE::Other("not an integer"))?;
973      materials_format::Version::try_from_integer(v)?
974    },
975  };
976
977  let mut l = Catalogue {
978    bundle: src.bundle(),
979    libname: libname.to_string(),
980    items: HashMap::new(),
981    dirname: src.svg_dir(),
982  };
983  let empty_table = toml::value::Value::Table(default());
984  let groups = toplevel
985    .get("group").unwrap_or(&empty_table)
986    .as_table().ok_or_else(|| LLE::ExpectedTable(format!("group")))?;
987  for (groupname, gdefn) in groups {
988    (||{
989
990    let gdefn = resolve_inherit(INHERIT_DEPTH_LIMIT,
991                                groups, groupname, gdefn)?;
992    let gdefn: GroupDefn = TV::Table(gdefn.into_owned()).try_into()?;
993    let d = if mformat == 1 {
994      let scale = gdefn.d.scale_mf1(mformat)?;
995      GroupDetails {
996        size: gdefn.d.size.iter().map(|s| s * scale).collect(),
997        ..gdefn.d
998      }
999    } else {
1000      gdefn.d // v2 isn't going to do this, do this right now
1001    };
1002    let group = Arc::new(GroupData {
1003      groupname: groupname.clone(),
1004      d, mformat,
1005    });
1006
1007    // We do this here rather than in the files loop because
1008    //  1. we want to check it even if there are no files specified
1009    //  2. this is OK because the group doesn't change from here on
1010    let shape_calculable = group.check_shape()?;
1011
1012    if [
1013      group.d.flip,
1014      group.d.back.is_some(),
1015    ].iter().filter(|x|**x).count() > 1 {
1016      throw!(LLE::MultipleMultipleFaceDefinitions)
1017    }
1018
1019    for fe in gdefn.files.0 {
1020      process_files_entry(
1021        src.svg_noter(), &mut l,
1022        &gdefn.item_prefix, &gdefn.item_suffix, &gdefn.sort,
1023        &group, shape_calculable, fe
1024      )?;
1025    }
1026
1027    Ok(())
1028    })().map_err(|error| LLE::InGroup {
1029      group: groupname.to_string(),
1030      error: Box::new(error),
1031    })?
1032  }
1033
1034  Ok(l)
1035  })().map_err(|error| LLE::InLibrary {
1036    lib: libname.into(),
1037    error: Box::new(error),
1038  })?
1039}
1040
1041#[derive(Debug,Copy,Clone,Eq,PartialEq)]
1042pub enum Dollars { Text, Filename }
1043
1044#[derive(Debug,Clone)]
1045pub struct Substituting<'s> {
1046  s: Cow<'s, str>,
1047  mformat: materials_format::Version,
1048  dollars: Dollars,
1049}
1050
1051impl<'s> Substituting<'s> {
1052  pub fn new<S: Into<Cow<'s, str>>>(
1053    mformat: materials_format::Version,
1054    dollars: Dollars,
1055    s: S
1056  ) -> Self {
1057    Substituting { s: s.into(), mformat, dollars }
1058  }
1059
1060  #[throws(SubstError)]
1061  pub fn finish(self) -> String {
1062    if self.do_dollars() {
1063      self.subst_general_precisely("${$}", "$")?.0
1064    } else {
1065      self
1066    }.s.into()
1067  }
1068
1069  fn do_dollars(&self) -> bool { self.dollars.enabled(self.mformat) }
1070
1071  #[throws(SubstError)]
1072  /// Expand, but do not do final unescaping
1073  ///
1074  /// Used when we are expanding something that is going to be used
1075  /// as a replacement in a further expansion, which will do final unescaping.
1076  pub fn nest(self) -> String {
1077    self.s.into()
1078  }
1079
1080  fn err(&self, kind: SubstErrorKind) -> SubstError {
1081    SubstError { kind, input: (*self.s).to_owned() }
1082  }
1083
1084  fn internal_err(&self, msg: &'static str) -> SubstError {
1085    self.err(InternalLogicError::new(msg).into())
1086  }
1087}
1088
1089impl Dollars {
1090  fn enabled(self, mformat: materials_format::Version) -> bool {
1091    match self {
1092      Dollars::Filename => false,
1093      Dollars::Text => mformat >= 2,
1094    }
1095  }
1096}
1097
1098impl<'i> Substituting<'i> {
1099#[throws(SubstError)]
1100  fn subst_general_precisely(&self, needle: &str, replacement: &str)
1101                             -> (Substituting<'i>, usize) {
1102    let mut count = 0;
1103    let mut work = (*self.s).to_owned();
1104    for m in self.s.rmatch_indices(needle) {
1105      count += 1;
1106      let mut lhs = &work[0.. m.0];
1107      let mut rhs = &work[m.0 + m.1.len() ..];
1108      if replacement.is_empty() {
1109        let lhs_trimmed = lhs.trim_end();
1110        if lhs_trimmed.len() != lhs.len() {
1111          lhs = lhs_trimmed;
1112        } else {
1113          rhs = rhs.trim_start();
1114        } 
1115      }
1116      work = lhs
1117        .to_owned()
1118        + replacement
1119        + rhs
1120    }
1121    (Substituting{
1122      s: work.into(),
1123      mformat: self.mformat,
1124      dollars: self.dollars,
1125    }, count)
1126  }
1127
1128  #[throws(SubstError)]
1129  // This takes &Substituting.  The rest of the code uses subst or
1130  // substn, which takes Substituting, thus ensuring that at some future
1131  // time we might be able to accumulate all the substitutions in
1132  // Substituting and do them all at once.
1133  fn subst_general(&self, needle: Cow<'static, str>, replacement: &str)
1134                   -> (Substituting<'i>, usize, Cow<'static, str>) {
1135    match self.dollars {
1136      Dollars::Filename => if needle != "_c" {
1137        throw!(self.internal_err("long subst in filename"))
1138      },
1139      Dollars::Text => { },
1140    }
1141    let needle: Cow<str> = (move || Some({
1142      if let Some(rhs) = needle.strip_prefix("${") {
1143        let token = rhs.strip_suffix('}')?;
1144        if self.do_dollars() { needle }
1145        else { format!("_{}", token).into() }
1146      } else if let Some(token) = needle.strip_prefix('_') {
1147        if ! self.do_dollars() { needle }
1148        else { format!("${{{}}}", token).into() }
1149      } else {
1150        return None
1151      }
1152    }))()
1153      .ok_or_else(|| self.internal_err("needle has no '_'"))?;
1154
1155    let (r, count) = self.subst_general_precisely(&needle, replacement)?;
1156    (r, count, needle)
1157  }
1158}
1159
1160#[throws(SubstError)]
1161fn subst<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1162               -> Substituting<'i>
1163where N: Into<Cow<'static, str>>
1164{
1165  use SubstErrorKind as SEK;
1166  let needle = needle.into();
1167  let (out, count, needle) = before.subst_general(needle, replacement)?;
1168  if count == 0 { throw!(before.err(SEK::MissingToken(needle))) }
1169  if count > 1 { throw!(before.err(SEK::RepeatedToken(needle))) }
1170  out
1171}
1172
1173#[throws(SubstError)]
1174fn substn<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1175              -> Substituting<'i>
1176where N: Into<Cow<'static, str>>
1177{
1178  before.subst_general(needle.into(), replacement)?.0
1179}
1180
1181#[cfg(not(miri))]
1182#[test]
1183fn test_subst_mf1() {
1184  use SubstErrorKind as SEK;
1185
1186  let mformat = materials_format::Version::try_from_integer(1).unwrap();
1187  let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1188  let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1189
1190  assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1191             .unwrap().finish().unwrap(),
1192             "die-image-blue");
1193  assert_eq!(subst(s_t("a _colour die"), "_colour", "blue")
1194             .unwrap().finish().unwrap(),
1195             "a blue die");
1196  assert_eq!(subst(s_t("a _colour die"), "${colour}", "blue")
1197             .unwrap().finish().unwrap(),
1198             "a blue die");
1199  assert_eq!(subst(s_t("a _colour die"), "_colour", "")
1200             .unwrap().finish().unwrap(),
1201             "a die");
1202  assert!{matches!{
1203    dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1204    SEK::MissingToken(c) if c == "_colour",
1205  }}
1206  assert!{matches!{
1207    dbg!(subst(s_t("a _colour _colour die"), "_colour", "")).unwrap_err().kind,
1208    SEK::RepeatedToken(c) if c == "_colour",
1209  }}
1210
1211  assert_eq!(substn(s_t("a _colour die being _colour"), "_colour", "blue")
1212             .unwrap().finish().unwrap(),
1213             "a blue die being blue");
1214
1215  let (s, count, needle) = s_t("a _colour _colour die")
1216    .subst_general("_colour".into(), "")
1217    .unwrap();
1218  assert_eq!(s.finish().unwrap(), "a die".to_owned());
1219  assert_eq!(count, 2);
1220  assert_eq!(needle, "_colour");
1221}
1222
1223#[cfg(not(miri))]
1224#[test]
1225fn test_subst_mf2() {
1226  use SubstErrorKind as SEK;
1227
1228  let mformat = materials_format::Version::try_from_integer(2).unwrap();
1229  let s_t = |s| Substituting::new(mformat, Dollars::Text, s);
1230  let s_f = |s| Substituting::new(mformat, Dollars::Filename, s);
1231
1232  assert_eq!(subst(s_f("die-image-_c"), "_c", "blue")
1233             .unwrap().finish().unwrap(),
1234             "die-image-blue");
1235  assert!{matches!{
1236    dbg!(subst(s_f("die-image-_c"), "_colour", "")).unwrap_err().kind,
1237    SEK::Internal(_)
1238  }}
1239
1240  assert_eq!(subst(s_t("a ${colour} die"), "_colour", "blue")
1241             .unwrap().finish().unwrap(),
1242             "a blue die");
1243  assert_eq!(subst(s_t("a ${c} die"), "_c", "blue")
1244             .unwrap().finish().unwrap(),
1245             "a blue die");
1246  assert_eq!(subst(s_t("a ${colour} die"), "_colour", "")
1247             .unwrap().finish().unwrap(),
1248             "a die");
1249  assert_eq!(subst(s_t("a ${colour} die"), "${colour}", "")
1250             .unwrap().finish().unwrap(),
1251             "a die");
1252  assert!{matches!{
1253    dbg!(subst(s_t("a die"), "_colour", "")).unwrap_err().kind,
1254    SEK::MissingToken(c) if c == "${colour}",
1255  }}
1256  assert!{matches!{
1257    dbg!(subst(s_t("a ${colour} ${colour} die"), "_colour", ""))
1258      .unwrap_err().kind,
1259    SEK::RepeatedToken(c) if c == "${colour}",
1260  }}
1261
1262  assert_eq!(substn(s_t("a ${colour} die being ${colour}"), "_colour", "blue")
1263             .unwrap().finish().unwrap(),
1264             "a blue die being blue");
1265
1266  let (s, count, needle) = s_t("a ${colour} ${colour} die")
1267    .subst_general("_colour".into(), "")
1268    .unwrap();
1269  assert_eq!(s.finish().unwrap(), "a die".to_owned());
1270  assert_eq!(count, 2);
1271  assert_eq!(needle, "${colour}");
1272}
1273
1274#[throws(LibraryLoadError)]
1275fn format_item_name(mformat: materials_format::Version,
1276                    item_prefix: &str, fe: &FileData, item_suffix: &str)
1277                    -> Substituting<'static> {
1278  Substituting::new(
1279    mformat, Dollars::Filename,
1280    format!("{}{}{}", item_prefix, fe.item_spec, item_suffix)
1281  )
1282}
1283
1284#[throws(LibraryLoadError)]
1285fn process_files_entry(
1286  src: &mut dyn LibrarySvgNoter, l: &mut Catalogue,
1287  item_prefix: &str, item_suffix: &str, sort: &str,
1288  group: &Arc<GroupData>, shape_calculable: ShapeCalculable,
1289  fe: FileData
1290) {
1291  let mformat = group.mformat;
1292  let item_name = format_item_name(mformat, item_prefix, &fe, item_suffix)?;
1293
1294  let sort: Option<PerhapsSubst> = match (sort, fe.extra_fields.get("sort")) {
1295    ("", None) => None,
1296    (gd, None) => Some(gd.into()),
1297    ("", Some(ef)) => Some(ef.into()),
1298    (gd, Some(ef)) => {
1299      let sort = Substituting::new(mformat, Dollars::Text, gd);
1300      Some(subst(sort, "_s", ef)?.into())
1301    },
1302  };
1303
1304  let occ = match &group.d.occulted {
1305    None => OccData::None,
1306    Some(OccultationMethod::ByColour { colour }) => {
1307      if ! group.d.colours.contains_key(colour.0.as_str()) {
1308        throw!(LLE::OccultationColourMissing(colour.0.clone()));
1309      }
1310      let item_name = subst(item_name.clone(), "_c", &colour.0)?;
1311      let src_name = Substituting::new(mformat, Dollars::Filename,
1312                                       &fe.src_file_spec);
1313      let src_name  = subst(src_name, "_c", &colour.0)
1314        .and_then(|s| s.finish());
1315      let item_name: GoodItemName = item_name.finish()?.try_into()?;
1316      let item_name = SvgBaseName::note(
1317        src, item_name, src_name.as_deref(),
1318      )?;
1319      let desc = Substituting::new(mformat, Dollars::Text, &fe.desc);
1320      let desc = subst(desc, "${colour}", "")?.finish()?.to_html();
1321      OccData::Internal(Arc::new(OccData_Internal {
1322        item_name,
1323        loaded: default(),
1324        desc,
1325      }))
1326    },
1327    Some(OccultationMethod::ByBack { ilk }) => {
1328      if group.d.back.is_none() {
1329        throw!(LLE::BackMissingForOccultation)
1330      }
1331      OccData::Back(ilk.clone())
1332    },
1333  };
1334
1335  #[derive(Debug,From,Clone)]
1336  enum PerhapsSubst<'i> {
1337    Y(Substituting<'i>),
1338    N(&'i str),
1339  }
1340  impl<'i> From<&'i String> for PerhapsSubst<'i> {
1341    fn from(s: &'i String) -> Self { (&**s).into() }
1342  }
1343
1344  impl<'i> PerhapsSubst<'i> {
1345    #[throws(SubstError)]
1346    pub fn finish(self) -> String { match self {
1347      PerhapsSubst::N(s) => s.to_owned(),
1348      PerhapsSubst::Y(s) => s.finish()?,
1349    } }
1350    #[throws(SubstError)]
1351    pub fn nest(self) -> String { match self {
1352      PerhapsSubst::N(s) => s.to_owned(),
1353      PerhapsSubst::Y(s) => s.nest()?,
1354    } }
1355    pub fn mky(
1356      self,
1357      mformat: materials_format::Version,
1358      dollars: Dollars,
1359    ) -> Substituting<'i> { match self {
1360      PerhapsSubst::N(s) => Substituting::new(mformat, dollars, s),
1361      PerhapsSubst::Y(s) => s,
1362    } }
1363    #[throws(SubstError)]
1364    pub fn into_of_y(
1365      self,
1366    ) -> Substituting<'i> { match self {
1367      PerhapsSubst::Y(s) => s,
1368      PerhapsSubst::N(s) => throw!(SubstError {
1369        kind: InternalLogicError::new("expected Y").into(),
1370        input: s.into(),
1371      })
1372    } }
1373  }
1374
1375  fn colour_subst_1<'s, S>(
1376    mformat: materials_format::Version,
1377    dollars: Dollars,
1378    subst: S, kv: Option<(&'static str, &'s str)>
1379  )
1380    -> impl for <'i> Fn(PerhapsSubst<'i>)
1381                        -> Result<PerhapsSubst<'i>, SubstError>
1382                     + 's
1383  where S: for <'i> Fn(Substituting<'i>, &'static str, &str)
1384              -> Result<Substituting<'i>, SubstError> + 's
1385  {
1386    move |input| Ok(
1387      if let Some((keyword, val)) = kv {
1388        subst(input.mky(mformat, dollars), keyword, val)?.into()
1389      } else if dollars.enabled(mformat) {
1390        input.mky(mformat, dollars).into()
1391      } else {
1392        input
1393      }
1394    )
1395  }
1396
1397  let mut add1 = |
1398    c_colour: Option<(&'static str, &str)>,
1399    c_abbrev: Option<(&'static str, &str)>,
1400    c_substs: Option<&HashMap<String, String>>,
1401  | {
1402    let c_colour_all =colour_subst_1(mformat,Dollars::Text, substn, c_colour);
1403    let c_colour =    colour_subst_1(mformat,Dollars::Text, subst,  c_colour);
1404    let c_abbrev_t =  colour_subst_1(mformat,Dollars::Text, subst,  c_abbrev);
1405    let c_abbrev_f =colour_subst_1(mformat,Dollars::Filename,subst, c_abbrev);
1406
1407    let sort = sort.clone().map(|v| c_abbrev_t(v)).transpose()?;
1408    let sort = sort.map(|s| s.finish()).transpose()?;
1409
1410    let subst_item_name = |item_name: &Substituting| {
1411      let item_name = c_abbrev_f(item_name.clone().into())?;
1412      let item_name = item_name.finish()?.try_into()?;
1413      Ok::<_,LLE>(item_name)
1414    };
1415    let item_name = subst_item_name(&item_name)?;
1416
1417    let src_name = c_abbrev_f((&fe.src_file_spec).into())
1418      .and_then(|s| s.finish());
1419    let src_name = src_name.as_deref();
1420
1421    let desc = c_colour((&fe.desc).into())?;
1422
1423    let desc = if let Some(desc_template) = &group.d.desc_template {
1424      let desc_template = Substituting::new(
1425        mformat, Dollars::Text, desc_template);
1426      subst(desc_template, "${desc}", &desc.nest()?)?.finish()?.to_html()
1427    } else {
1428      desc.finish()?.to_html()
1429    };
1430
1431    let idata = ItemData {
1432      group: group.clone(),
1433      occ: occ.clone(),
1434      sort,
1435      shape_calculable,
1436      d: Arc::new(ItemDetails { desc }),
1437    };
1438    l.add_item(src, src_name, &item_name, CatEnt::Item(idata))?;
1439
1440    if let Some(magic) = &group.d.magic { 
1441      // Ideally the toml crate would have had let us build an inline
1442      // table.  But in fact it won't even toml-escape the strings without
1443      // a fuss, so we bodge it with strings:
1444      let image_table = format!(
1445        r#"{{ type="Lib", lib="{}", item="{}" }}"#,
1446        TomlQuote(&l.libname), TomlQuote(item_name.as_str())
1447      );
1448
1449      let item_name = subst_item_name(&format_item_name(
1450        mformat, &magic.item_prefix, &fe, &magic.item_suffix)?)?;
1451
1452      let mut spec = Substituting::new(mformat, Dollars::Text,
1453                                       &magic.template);
1454      for (k,v) in chain!{
1455        c_substs.into_iter().map(IntoIterator::into_iter).flatten(),
1456        &magic.substs,
1457        fe.extra_fields.iter().filter(|(k,_v)| k.starts_with('x')),
1458      } {
1459        spec = substn(spec, format!("${{{}}}", k), v)?;
1460      }
1461      let spec = substn(spec, "${image}", &image_table)?;
1462      let spec = c_colour_all(spec.into())?.into_of_y()?;
1463      let spec = spec.finish()?;
1464      trace!("magic item {}\n\n{}\n", &item_name, &spec);
1465
1466      let spec: Box<dyn PieceSpec> = toml_de::from_str(&spec)
1467        .map_err(|error| LLE::TemplatedTomlError {
1468          toml: spec,
1469          error,
1470        })?;
1471
1472      l.add_item(&mut NullLibrarySvgNoter, // there's no SVG for *this* item
1473                 src_name, &item_name, CatEnt::Magic {
1474        group: group.clone(),
1475        spec: spec.into(),
1476      })?;
1477    }
1478
1479    Ok::<_,LLE>(())
1480  };
1481
1482  if group.d.colours.is_empty() {
1483    add1(None, None, None)?;
1484  } else {
1485    for (colour, recolourdata) in &group.d.colours {
1486      add1(Some(("${colour}", colour)),
1487           Some(("_c", &recolourdata.abbrev)),
1488           Some(&recolourdata.substs))?;
1489    }
1490  }
1491}
1492
1493impl Catalogue {
1494  #[throws(LLE)]
1495  fn add_item(&mut self,
1496              src: &mut dyn LibrarySvgNoter,
1497              src_name: Result<&str,&SubstError>,
1498              item_name: &GoodItemName, catent: CatalogueEntry) {
1499    type H<'e,X,Y> = hash_map::Entry<'e,X,Y>;
1500
1501    let new_item = SvgBaseName::note(
1502      src, item_name.clone(), src_name.clone()
1503    )?;
1504
1505    match self.items.entry(new_item) {
1506      H::Occupied(oe) => throw!(LLE::DuplicateItem {
1507        item: item_name.as_str().to_owned(),
1508        group1: oe.get().group().groupname.clone(),
1509        group2: catent.group().groupname.clone(),
1510      }),
1511      H::Vacant(ve) => {
1512        debug!("loaded shape {} {}", &self.libname, item_name.as_str());
1513        ve.insert(catent);
1514      }
1515    };
1516  }
1517}
1518
1519//---------- reading, support functions ----------
1520
1521#[throws(LibraryLoadError)]
1522fn resolve_inherit<'r>(depth: u8, groups: &toml::value::Table,
1523                       group_name: &str, group: &'r toml::Value)
1524                       -> Cow<'r, toml::value::Table> {
1525  let gn = || format!("{}", group_name);
1526  let gp = || format!("group.{}", group_name);
1527
1528  let group = group.as_table().ok_or_else(|| LLE::ExpectedTable(gp()))?;
1529
1530  let parent_name = match group.get("inherit") {
1531    None => { return Cow::Borrowed(group) }
1532    Some(p) => p,
1533  };
1534  let parent_name = parent_name
1535    .as_str().ok_or_else(|| LLE::ExpectedString(format!("group.{}.inherit",
1536                                                        group_name)))?;
1537  let parent = groups.get(parent_name)
1538    .ok_or_else(|| LLE::InheritMissingParent(gn(), parent_name.to_string()))?;
1539
1540  let mut build = resolve_inherit(
1541    depth.checked_sub(1).ok_or_else(|| LLE::InheritDepthLimitExceeded(gn()))?,
1542    groups, parent_name, parent
1543  )?.into_owned();
1544
1545  build.extend(group.iter().map(|(k,v)| (k.clone(), v.clone())));
1546  Cow::Owned(build)
1547}
1548
1549impl TryFrom<String> for FileList {
1550  type Error = LLE;
1551//  #[throws(LLE)]
1552  fn try_from(s: String) -> Result<FileList,LLE> {
1553    let mut o = Vec::new();
1554    let mut xfields = Vec::new();
1555    for (lno,l) in s.lines().enumerate() {
1556      let l = l.trim();
1557      if l=="" || l.starts_with('#') { continue }
1558      if let Some(xfields_spec) = l.strip_prefix(':') {
1559        if ! (o.is_empty() && xfields.is_empty()) {
1560          throw!(LLE::FilesListFieldsMustBeAtStart(lno));
1561        }
1562        xfields = xfields_spec.split_ascii_whitespace()
1563          .filter(|s| !s.is_empty())
1564          .map(|s| s.to_owned())
1565          .collect::<Vec<_>>();
1566        continue;
1567      }
1568      let mut remain = &*l;
1569      let mut n = ||{
1570        let ws = remain.find(char::is_whitespace)
1571          .ok_or(LLE::FilesListLineMissingWhitespace(lno))?;
1572        let (l, r) = remain.split_at(ws);
1573        remain = r.trim_start();
1574        Ok::<_,LLE>(l.to_owned())
1575      };
1576      let item_spec = n()?;
1577      let src_file_spec = n()?;
1578      let extra_fields = xfields.iter()
1579        .map(|field| Ok::<_,LLE>((field.to_owned(), n()?)))
1580        .collect::<Result<_,_>>()?;
1581      let desc = remain.to_owned();
1582      o.push(FileData{ item_spec, src_file_spec, extra_fields, desc });
1583    }
1584    Ok(FileList(o))
1585  }
1586}
1587
1588//==================== Registry ====================
1589
1590impl Registry {
1591  pub fn add(&mut self, data: Catalogue) {
1592    self.libs
1593      .entry(data.libname.clone()).or_default()
1594      .push(data);
1595  }
1596
1597  pub fn clear(&mut self) {
1598    self.libs.clear()
1599  }
1600
1601  pub fn iter(&self) -> impl Iterator<Item=&[Catalogue]> {
1602    self.libs.values().map(|v| v.as_slice())
1603  }
1604}
1605
1606pub struct AllRegistries<'ig> {
1607  global: RwLockReadGuard<'static, Option<Registry>>,
1608  ig: &'ig Instance,
1609}
1610pub struct AllRegistriesIterator<'i> {
1611  regs: &'i AllRegistries<'i>,
1612  count: u8,
1613}
1614
1615impl<'i> Iterator for AllRegistriesIterator<'i> {
1616  type Item = &'i Registry;
1617  fn next(&mut self) -> Option<&'i Registry> {
1618    loop {
1619      let r = match self.count {
1620        0 => self.regs.global.as_ref(),
1621        1 => Some(&self.regs.ig.local_libs),
1622        _ => return None,
1623      };
1624      self.count += 1;
1625      if r.is_some() { return r }
1626    }
1627  }
1628}
1629
1630impl Instance {
1631  pub fn all_shapelibs(&self) -> AllRegistries<'_> {
1632    AllRegistries {
1633      global: GLOBAL_SHAPELIBS.read(),
1634      ig: self,
1635    }
1636  }
1637} 
1638impl<'ig> AllRegistries<'ig> {
1639  pub fn iter(&'ig self) -> AllRegistriesIterator<'ig> {
1640    AllRegistriesIterator {
1641      regs: self,
1642      count: 0,
1643    }
1644  }
1645}
1646
1647pub fn lib_name_list(ig: &Instance) -> Vec<String> {
1648  ig.all_shapelibs().iter().map(
1649    |reg| reg.libs.keys().cloned()
1650  ).flatten().collect()
1651}
1652
1653impl<'ig> AllRegistries<'ig> {
1654  pub fn all_libs(&self) -> impl Iterator<Item=&[Catalogue]> {
1655    self.iter().map(|reg| &reg.libs).flatten().map(
1656      |(_libname, lib)| lib.as_slice()
1657    )
1658  }
1659  pub fn lib_name_lookup(&self, libname: &str) -> Result<&[Catalogue], SpE> {
1660    for reg in self.iter() {
1661      if let Some(r) = reg.libs.get(libname) { return Ok(r) }
1662    }
1663    return Err(SpE::LibraryNotFound);
1664  }
1665}
1666
1667//==================== configu and loading global libs ====================
1668
1669#[throws(LibraryLoadError)]
1670pub fn load_1_global_library(l: &Explicit1) {
1671  let toml_path = &l.catalogue;
1672  let catalogue_data = {
1673    let ioe = |io| LLE::FileError(toml_path.to_string(), io);
1674    let f = File::open(toml_path).map_err(ioe)?;
1675    let mut f = BufReader::new(f);
1676    let mut s = String::new();
1677    f.read_to_string(&mut s).map_err(ioe)?;
1678    s
1679  };
1680
1681  let catalogue_data = catalogue_data.as_str();
1682  let mut src = BuiltinLibrary { dirname: &l.dirname, catalogue_data };
1683
1684  let data = load_catalogue(&l.name, &mut src)?;
1685  let count = data.items.len();
1686  GLOBAL_SHAPELIBS.write()
1687    .get_or_insert_with(default)
1688    .add(data);
1689  info!("loaded {} shapes in library {:?} from {:?} and {:?}",
1690        count, &l.name, &l.catalogue, &l.dirname);
1691}
1692
1693#[ext(pub)]
1694impl ShapelibConfig1 {
1695  fn resolve(&self) -> Result<Box<dyn ExactSizeIterator<Item=Explicit1>>, LibraryLoadError> {
1696    use Config1::*;
1697    Ok(match self {
1698      Explicit(e) => Box::new(iter::once(e.clone())),
1699      PathGlob(pat) => {
1700
1701        #[throws(LLE)]
1702        fn resolve_globresult(pat: &str, globresult: glob::GlobResult)
1703                              -> Explicit1 {
1704          let path = globresult?;
1705          let path = path.to_str().ok_or_else(
1706            || LLE::GlobNonUTF8
1707            { pat: pat.to_string(), actual: path.clone() })?
1708            .to_string();
1709
1710          let dirname = path.rsplitn(2,'.').nth(1).ok_or_else(
1711            || LLE::GlobNoExtension
1712            { pat: pat.to_string(), path: path.clone() })?;
1713
1714          let base = dirname.rsplit('/').next().unwrap();
1715
1716          Explicit1 {
1717            name: base.to_string(),
1718            dirname: dirname.to_string(),
1719            catalogue: path,
1720          }
1721        }
1722
1723        let results = glob::glob_with(pat, glob::MatchOptions {
1724          require_literal_separator: true,
1725          require_literal_leading_dot: true,
1726          ..default()
1727        })
1728          .map_err(
1729            |glob::PatternError { pos, msg, .. }|
1730            LLE::BadGlobPattern { pat: pat.clone(), pos, msg }
1731          )?
1732          .map(|globresult| resolve_globresult(pat, globresult))
1733          .collect::<Result<Vec<_>, LLE>>()?;
1734
1735        Box::new(results.into_iter())
1736
1737      }
1738    })
1739  }
1740}
1741
1742#[throws(LibraryLoadError)]
1743pub fn load_global_libs(libs: &[Config1]) {
1744  for l in libs {
1745    let libs = l.resolve()?;
1746    let n = libs.len();
1747    for e in libs {
1748      load_1_global_library(&e)?;
1749    }
1750    info!("loaded {} shape libraries from {:?}", n, &l);
1751          
1752  }
1753}