chiark / gitweb /
shapelib: Fix a backward incompatible itemname field
[otter.git] / src / 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
5 use crate::prelude::*;
6 use crate::*; // to get ambassador_impls, macro resolution trouble
7 pub use crate::shapelib_toml::*;
8
9 pub use crate::prelude::GoodItemName; // not sure why this is needed
10
11 use parking_lot::{const_rwlock, RwLock};
12 use parking_lot::RwLockReadGuard;
13
14 use ShapelibConfig1 as Config1;
15 use 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
26 static GLOBAL_SHAPELIBS: RwLock<Option<Registry>> = const_rwlock(None);
27
28 #[derive(Default)]
29 pub struct Registry {
30   libs: HashMap<String, Vec<shapelib::Catalogue>>,
31 }
32
33 #[derive(Debug)]
34 pub struct GroupData {
35   groupname: String,
36   d: GroupDetails,
37   #[allow(dead_code)] /*TODO*/ mformat: materials_format::Version,
38 }
39
40 #[derive(Debug,Clone,Copy)]
41 pub struct ShapeCalculable { }
42
43 #[derive(Debug)]
44 pub 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)]
53 struct ItemDetails {
54   desc: Html,
55 }
56
57 #[derive(Debug,Clone)]
58 enum CatalogueEntry {
59   Item(ItemData),
60   Magic { group: Arc<GroupData>, spec: Arc<dyn PieceSpec> },
61 }
62 use CatalogueEntry as CatEnt;
63
64 #[derive(Debug,Clone)]
65 struct 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)]
74 enum OccData {
75   None,
76   Internal(Arc<OccData_Internal>),
77   Back(OccultIlkName),
78 }
79
80 #[allow(non_camel_case_types)]
81 #[derive(Debug)]
82 struct 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)]
90 struct ImageLoaded {
91   svgd: Html,
92   xform: FaceTransform,
93   outline: Outline,
94 }
95
96 #[derive(Error,Debug)]
97 pub 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)]
154 pub 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)]
161 pub 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}")]
169 pub struct SubstError {
170   pub kind: SubstErrorKind,
171   pub input: String,
172 }
173
174
175 const INHERIT_DEPTH_LIMIT: u8 = 20;
176
177 type TV = toml::Value;
178
179 #[derive(Debug, Clone, Serialize, Deserialize)]
180 pub 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
189 define_index_type! { pub struct DescId = u8; }
190 define_index_type! { pub struct SvgId = u8; }
191
192 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
193 struct ItemFace {
194   svg: SvgId,
195   desc: DescId,
196   #[serde(flatten)]
197   xform: FaceTransform,
198 }
199
200 #[derive(Copy,Clone,Debug,Serialize,Deserialize)]
201 struct FaceTransform {
202   centre: [f64; 2],
203   scale: [f64; 2],
204 }
205
206 #[derive(Debug,Serialize,Deserialize)]
207 pub 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)]
219 struct 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)]
233 struct SvgBaseName<T:?Sized>(T);
234 impl<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 }
238 impl<T> Borrow<str> for SvgBaseName<T> where T: Borrow<str> {
239   fn borrow(&self) -> &str { self.0.borrow() }
240 }
241 impl<T> SvgBaseName<T> {
242   fn into_inner(self) -> T { self.0 }
243 }
244 impl<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 }
252 impl<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
263 impl_via_ambassador!{
264   #[dyn_upcast]
265   impl OutlineTrait for ItemInertForOcculted { outline }
266 }
267 #[dyn_upcast]
268 impl PieceBaseTrait for ItemInertForOcculted {
269   fn nfaces(&self) -> RawFaceId { 1 }
270   fn itemname(&self) -> &str { self.itemname.as_str() }
271 }
272 #[typetag::serde(name="Lib")]
273 impl 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
286 fn dummy_item_name() -> GoodItemName {
287   "".to_string().try_into().unwrap()
288 }
289
290 #[test]
291 fn 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)]
298 pub 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
306 impl 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
315 impl 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)]
324 pub struct LibraryEnquiryData {
325   pub bundle: Option<bundles::Id>,
326   pub libname: String,
327 }
328 impl 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
346 impl_via_ambassador!{
347   #[dyn_upcast]
348   impl OutlineTrait for Item { outline }
349 }
350
351 impl 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]
380 impl 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")]
391 impl 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")]
406 impl 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
422 type ItemSpecLoaded = (Box<Item>, PieceSpecLoadedOccultable);
423
424 impl From<ItemSpecLoaded> for SpecLoaded {
425   fn from((p, occultable):  ItemSpecLoaded) -> SpecLoaded {
426     SpecLoaded {
427       p,
428       occultable,
429       special: default(),
430     }
431   }
432 }
433
434 impl 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")]
487 impl 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")]
504 impl 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
523 impl 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
635 impl 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)]
677 fn 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
687 impl 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
696 impl 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
704 impl 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
784 impl 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]
798 pub 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.
817 impl_via_ambassador!{
818   impl ShapeLoadableTrait for Shape { shapelib_loadable() }
819 }
820
821 //---------- RectOutline ----------
822
823 impl 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
838 impl 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
867 impl 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
918 pub trait LibrarySvgNoter {
919   #[throws(SubstError)]
920   fn note_svg(&mut self, _basename: &GoodItemName,
921               _src_name: Result<&str, &SubstError>) { }
922 }
923 pub 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
936 pub struct NullLibrarySvgNoter;
937 impl LibrarySvgNoter for NullLibrarySvgNoter { }
938
939 struct BuiltinLibrary<'l> {
940   catalogue_data: &'l str,
941   dirname: &'l str,
942 }
943
944 impl LibrarySvgNoter for BuiltinLibrary<'_> {
945 }
946 impl<'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)]
962 pub 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)]
1042 pub enum Dollars { Text, Filename }
1043
1044 #[derive(Debug,Clone)]
1045 pub struct Substituting<'s> {
1046   s: Cow<'s, str>,
1047   mformat: materials_format::Version,
1048   dollars: Dollars,
1049 }
1050
1051 impl<'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
1089 impl 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
1098 impl<'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)]
1161 fn subst<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1162                -> Substituting<'i>
1163 where 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)]
1174 fn substn<'i,N>(before: Substituting<'i>, needle: N, replacement: &str)
1175               -> Substituting<'i>
1176 where N: Into<Cow<'static, str>>
1177 {
1178   before.subst_general(needle.into(), replacement)?.0
1179 }
1180
1181 #[cfg(not(miri))]
1182 #[test]
1183 fn 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]
1225 fn 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)]
1275 fn 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)]
1285 fn 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
1493 impl 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)]
1522 fn 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
1549 impl 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
1590 impl 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
1606 pub struct AllRegistries<'ig> {
1607   global: RwLockReadGuard<'static, Option<Registry>>,
1608   ig: &'ig Instance,
1609 }
1610 pub struct AllRegistriesIterator<'i> {
1611   regs: &'i AllRegistries<'i>,
1612   count: u8,
1613 }
1614
1615 impl<'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
1630 impl Instance {
1631   pub fn all_shapelibs(&self) -> AllRegistries<'_> {
1632     AllRegistries {
1633       global: GLOBAL_SHAPELIBS.read(),
1634       ig: self,
1635     }
1636   }
1637
1638 impl<'ig> AllRegistries<'ig> {
1639   pub fn iter(&'ig self) -> AllRegistriesIterator<'ig> {
1640     AllRegistriesIterator {
1641       regs: self,
1642       count: 0,
1643     }
1644   }
1645 }
1646
1647 pub 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
1653 impl<'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)]
1670 pub 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)]
1694 impl 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)]
1743 pub 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 }